diff --git a/packages/localnet/src/handleOnEVMCalled.ts b/packages/localnet/src/handleOnEVMCalled.ts index 1621071..f0736c5 100644 --- a/packages/localnet/src/handleOnEVMCalled.ts +++ b/packages/localnet/src/handleOnEVMCalled.ts @@ -62,7 +62,11 @@ export const handleOnEVMCalled = async ({ return await handleOnRevertEVM({ revertOptions, err, + amount: 0, + asset: ethers.ZeroAddress, tss, + isGas: true, + token: "", provider, protocolContracts, exitOnError, diff --git a/packages/localnet/src/handleOnEVMDeposited.ts b/packages/localnet/src/handleOnEVMDeposited.ts index 64f8c84..78c4e27 100644 --- a/packages/localnet/src/handleOnEVMDeposited.ts +++ b/packages/localnet/src/handleOnEVMDeposited.ts @@ -2,6 +2,8 @@ import { ethers, NonceManager } from "ethers"; import { handleOnRevertEVM } from "./handleOnRevertEVM"; import { log, logErr } from "./log"; import { deployOpts } from "./deployOpts"; +import * as ZRC20 from "@zetachain/protocol-contracts/abi/ZRC20.sol/ZRC20.json"; +import * as UniswapV2Router02 from "@uniswap/v2-periphery/build/UniswapV2Router02.json"; // event Deposited(address indexed sender, address indexed receiver, uint256 amount, address asset, bytes payload, RevertOptions revertOptions); export const handleOnEVMDeposited = async ({ @@ -24,26 +26,24 @@ export const handleOnEVMDeposited = async ({ exitOnError: boolean; }) => { log("EVM", "Gateway: 'Deposited' event emitted"); - try { - const receiver = args[1]; - const amount = args[2]; - const asset = args[3]; - const message = args[4]; - - let foreignCoin; - if (asset === ethers.ZeroAddress) { - foreignCoin = foreignCoins.find((coin) => coin.coin_type === "Gas"); - } else { - foreignCoin = foreignCoins.find((coin) => coin.asset === asset); - } - - if (!foreignCoin) { - logErr("ZetaChain", `Foreign coin not found for asset: ${asset}`); - return; - } + const receiver = args[1]; + const amount = args[2]; + const asset = args[3]; + const message = args[4]; + let foreignCoin; + if (asset === ethers.ZeroAddress) { + foreignCoin = foreignCoins.find((coin) => coin.coin_type === "Gas"); + } else { + foreignCoin = foreignCoins.find((coin) => coin.asset === asset); + } - const zrc20 = foreignCoin.zrc20_contract_address; + if (!foreignCoin) { + logErr("ZetaChain", `Foreign coin not found for asset: ${asset}`); + return; + } + const zrc20 = foreignCoin.zrc20_contract_address; + try { const context = { origin: protocolContracts.gatewayZEVM.target, sender: await fungibleModuleSigner.getAddress(), @@ -85,13 +85,145 @@ export const handleOnEVMDeposited = async ({ } catch (err) { logErr("ZetaChain", `Error depositing: ${err}`); const revertOptions = args[5]; + const zrc20Contract = new ethers.Contract(zrc20, ZRC20.abi, deployer); + const [gasZRC20, gasFee] = await zrc20Contract.withdrawGasFeeWithGasLimit( + revertOptions[4] + ); + let revertAmount; + let revertGasFee = gasFee; + let isGas = true; + let token = null; + if (zrc20 !== gasZRC20) { + token = foreignCoins.find( + (coin) => coin.zrc20_contract_address === zrc20 + )?.asset; + isGas = false; + revertGasFee = await swapToCoverGas( + deployer, + zrc20, + gasZRC20, + gasFee, + amount, + await fungibleModuleSigner.getAddress(), + zrc20Contract, + provider, + protocolContracts.wzeta.target, + protocolContracts.uniswapRouterInstance.target + ); + } + revertAmount = amount - revertGasFee; return await handleOnRevertEVM({ revertOptions, + asset, + amount: revertAmount, err, tss, + isGas, + token, provider, protocolContracts, exitOnError, }); } }; + +const swapToCoverGas = async ( + deployer: any, + zrc20: string, + gasZRC20: string, + gasFee: any, + amount: any, + fungibleModule: any, + zrc20Contract: any, + provider: any, + wzeta: string, + router: string +) => { + /** + * Retrieves the amounts for swapping tokens using UniswapV2. + * @param {"in" | "out"} direction - The direction of the swap ("in" or "out"). + * @param {any} provider - The ethers provider. + * @param {any} amount - The amount to swap. + * @param {string} tokenA - The address of token A. + * @param {string} tokenB - The address of token B. + * @returns {Promise} - The amounts for the swap. + * @throws Will throw an error if the UniswapV2 router address cannot be retrieved. + */ + const getAmounts = async ( + direction: "in" | "out", + provider: any, + amount: any, + tokenA: string, + tokenB: string, + routerAddress: any, + routerABI: any + ) => { + if (!routerAddress) { + throw new Error("Cannot get uniswapV2Router02 address"); + } + + const uniswapRouter = new ethers.Contract( + routerAddress, + routerABI.abi, + provider + ); + + const path = [tokenA, tokenB]; + + const amounts = + direction === "in" + ? await uniswapRouter.getAmountsIn(amount, path) + : await uniswapRouter.getAmountsOut(amount, path); + return amounts; + }; + + const uniswapV2Router = new ethers.Contract( + router, + UniswapV2Router02.abi, + deployer + ); + deployer.reset(); + const approvalTx = await zrc20Contract.approve(router, amount); + await approvalTx.wait(); + + const path = [zrc20, wzeta, gasZRC20]; + + const deadline = Math.floor(Date.now() / 1000) + 60 * 20; + const maxZRC20ToSpend = amount; + + try { + const swapTx = await uniswapV2Router.swapTokensForExactTokens( + gasFee, + maxZRC20ToSpend, + path, + fungibleModule, + deadline + ); + + await swapTx.wait(); + } catch (swapError) { + logErr("ZetaChain", `Error performing swap on Uniswap: ${swapError}`); + } + + const amountInZeta = await getAmounts( + "in", + provider, + gasFee, + wzeta, + gasZRC20, + router, + UniswapV2Router02 + ); + + const amountInZRC20 = await getAmounts( + "in", + provider, + amountInZeta[0], + zrc20, + wzeta, + router, + UniswapV2Router02 + ); + + return amountInZRC20[0]; +}; diff --git a/packages/localnet/src/handleOnRevertEVM.ts b/packages/localnet/src/handleOnRevertEVM.ts index 183fa96..21ac0bf 100644 --- a/packages/localnet/src/handleOnRevertEVM.ts +++ b/packages/localnet/src/handleOnRevertEVM.ts @@ -1,28 +1,36 @@ import { log, logErr } from "./log"; import { deployOpts } from "./deployOpts"; -import { ethers, NonceManager } from "ethers"; +import { NonceManager } from "ethers"; export const handleOnRevertEVM = async ({ revertOptions, + asset, + amount, err, provider, tss, protocolContracts, + isGas, + token, exitOnError = false, }: { revertOptions: any; err: any; + asset: any; + amount: any; provider: any; tss: any; protocolContracts: any; + isGas: boolean; + token: string; exitOnError: boolean; }) => { const callOnRevert = revertOptions[1]; const revertAddress = revertOptions[0]; const revertMessage = revertOptions[3]; const revertContext = { - asset: ethers.ZeroAddress, - amount: 0, + asset, + amount, revertMessage, }; if (callOnRevert) { @@ -34,9 +42,26 @@ export const handleOnRevertEVM = async ({ )})` ); (tss as NonceManager).reset(); - const tx = await protocolContracts.gatewayEVM - .connect(tss) - .executeRevert(revertAddress, "0x", revertContext, deployOpts); + let tx; + if (isGas) { + tx = await protocolContracts.gatewayEVM + .connect(tss) + .executeRevert(revertAddress, "0x", revertContext, { + value: amount, + deployOpts, + }); + } else { + tx = await protocolContracts.custody + .connect(tss) + .withdrawAndRevert( + revertAddress, + token, + amount, + "0x", + revertContext, + deployOpts + ); + } await tx.wait(); log("EVM", "Gateway: successfully called onRevert"); const logs = await provider.getLogs({ @@ -47,10 +72,9 @@ export const handleOnRevertEVM = async ({ logs.forEach((data: any) => { log("EVM", `Event from onRevert: ${JSON.stringify(data)}`); }); - } catch (err) { - const error = `Gateway: Call onRevert failed: ${err}`; - logErr("EVM", error); - if (exitOnError) throw new Error(error); + } catch (err: any) { + logErr("EVM", `Gateway: Call onRevert failed`, err); + if (exitOnError) throw new Error(err); } } else { const error = `Tx reverted without callOnRevert: ${err}`; diff --git a/packages/localnet/src/handleOnRevertZEVM.ts b/packages/localnet/src/handleOnRevertZEVM.ts index 3beab18..f722719 100644 --- a/packages/localnet/src/handleOnRevertZEVM.ts +++ b/packages/localnet/src/handleOnRevertZEVM.ts @@ -4,6 +4,8 @@ import { logErr } from "./log"; export const handleOnRevertZEVM = async ({ revertOptions, err, + asset, + amount, provider, tss, log, @@ -14,6 +16,8 @@ export const handleOnRevertZEVM = async ({ }: { revertOptions: any; err: any; + asset: any; + amount: any; provider: any; fungibleModuleSigner: any; tss: NonceManager; @@ -26,8 +30,8 @@ export const handleOnRevertZEVM = async ({ const revertAddress = revertOptions[0]; const revertMessage = revertOptions[3]; const revertContext = { - asset: ethers.ZeroAddress, - amount: 0, + asset, + amount, revertMessage, }; diff --git a/packages/localnet/src/handleOnZEVMCalled.ts b/packages/localnet/src/handleOnZEVMCalled.ts index 33885e8..bcf2aa3 100644 --- a/packages/localnet/src/handleOnZEVMCalled.ts +++ b/packages/localnet/src/handleOnZEVMCalled.ts @@ -45,6 +45,8 @@ export const handleOnZEVMCalled = async ({ return await handleOnRevertZEVM({ revertOptions, err, + amount: 0, + asset: ethers.ZeroAddress, provider, fungibleModuleSigner, tss, diff --git a/packages/localnet/src/handleOnZEVMWithdrawn.ts b/packages/localnet/src/handleOnZEVMWithdrawn.ts index eff273a..ded8934 100644 --- a/packages/localnet/src/handleOnZEVMWithdrawn.ts +++ b/packages/localnet/src/handleOnZEVMWithdrawn.ts @@ -25,26 +25,26 @@ export const handleOnZEVMWithdrawn = async ({ exitOnError: boolean; }) => { log("ZetaChain", "Gateway: 'Withdrawn' event emitted"); + const getERC20ByZRC20 = (zrc20: string) => { + const foreignCoin = foreignCoins.find( + (coin: any) => coin.zrc20_contract_address === zrc20 + ); + if (!foreignCoin) { + logErr("EVM", `Foreign coin not found for ZRC20 address: ${zrc20}`); + return; + } + return foreignCoin.asset; + }; + const zrc20 = args[3]; + const amount = args[4]; try { const receiver = args[2]; - const zrc20 = args[3]; - const amount = args[4]; const message = args[7]; (tss as NonceManager).reset(); const zrc20Contract = new ethers.Contract(zrc20, ZRC20.abi, deployer); const coinType = await zrc20Contract.COIN_TYPE(); const isGasToken = coinType === 1n; const isERC20orZETA = coinType === 2n; - const getERC20ByZRC20 = (zrc20: string) => { - const foreignCoin = foreignCoins.find( - (coin: any) => coin.zrc20_contract_address === zrc20 - ); - if (!foreignCoin) { - logErr("EVM", `Foreign coin not found for ZRC20 address: ${zrc20}`); - return; - } - return foreignCoin.asset; - }; if (message !== "0x") { // The message is not empty, so this is a withdrawAndCall operation log("EVM", `Calling ${receiver} with message ${message}`); @@ -54,7 +54,6 @@ export const handleOnZEVMWithdrawn = async ({ .execute(receiver, message, { value: amount, ...deployOpts }); await executeTx.wait(); } else { - console.log("!!!"); const erc20 = getERC20ByZRC20(zrc20); const executeTx = await protocolContracts.custody .connect(tss) @@ -102,6 +101,8 @@ export const handleOnZEVMWithdrawn = async ({ err, provider, tss, + asset: getERC20ByZRC20(zrc20), + amount, log, fungibleModuleSigner, protocolContracts, diff --git a/packages/localnet/src/index.ts b/packages/localnet/src/index.ts index 3ad06a7..be66be7 100755 --- a/packages/localnet/src/index.ts +++ b/packages/localnet/src/index.ts @@ -20,6 +20,11 @@ const FUNGIBLE_MODULE_ADDRESS = "0x735b14BB79463307AAcBED86DAf3322B1e6226aB"; const foreignCoins: any[] = []; +// A hack to make BigInt serializable +(BigInt as any).prototype["toJSON"] = function () { + return this.toString(); +}; + let protocolContracts: any; let deployer: Signer; let tss: Signer; @@ -195,7 +200,6 @@ const deployProtocolContracts = async ( const { uniswapFactoryInstance, uniswapRouterInstance } = await prepareUniswap(deployer, tss, wzeta); - const { systemContract, gatewayZEVM } = await prepareZetaChain( deployer, wzeta.target, diff --git a/packages/tasks/src/localnet.ts b/packages/tasks/src/localnet.ts index ff0d83e..3490674 100644 --- a/packages/tasks/src/localnet.ts +++ b/packages/tasks/src/localnet.ts @@ -121,6 +121,7 @@ const localnet = async (args: any) => { ZETA: addr.zetaZetaChain, "Fungible Module": addr.fungibleModuleZetaChain, "System Contract": addr.sytemContractZetaChain, + "Uniswap Router": addr.uniswapRouter, ...addr.foreignCoins.reduce((acc: any, coin: any) => { acc[`ZRC-20 ${coin.symbol}`] = coin.zrc20_contract_address; return acc;