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: withdraw erc20 #149

Merged
merged 12 commits into from
Mar 18, 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@types/node": "^17.0.25",
"@typescript-eslint/eslint-plugin": "^5.20.0",
"@typescript-eslint/parser": "^5.20.0",
"@zetachain/toolkit": "^5.0.0",
"chai": "^4.3.6",
"dotenv": "^16.0.0",
"eslint": "^8.13.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/zeta-app-contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@
"@zetachain/protocol-contracts": "^4.0.1",
"ethers": "5.6.8"
}
}
}
17 changes: 16 additions & 1 deletion packages/zevm-app-contracts/contracts/shared/MockZRC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,22 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./BytesHelperLib.sol";

contract MockZRC20 is ERC20 {
address public gasFeeAddress;
uint256 public gasFee;

event Withdrawal(address indexed from, bytes to, uint256 value, uint256 gasfee, uint256 protocolFlatFee);

constructor(uint256 initialSupply, string memory name, string memory symbol) ERC20(name, symbol) {
_mint(msg.sender, initialSupply * (10 ** uint256(decimals())));
gasFeeAddress = address(this);
}

function setGasFeeAddress(address gasFeeAddress_) external {
gasFeeAddress = gasFeeAddress_;
}

function setGasFee(uint256 gasFee_) external {
gasFee = gasFee_;
}

function deposit(address to, uint256 amount) external returns (bool) {
Expand All @@ -24,10 +38,11 @@ contract MockZRC20 is ERC20 {

function withdraw(bytes calldata to, uint256 amount) external returns (bool) {
address toAddress = BytesHelperLib.bytesToAddress(to, 12);
emit Withdrawal(msg.sender, to, amount, gasFee, 0);
return transfer(toAddress, amount);
}

function withdrawGasFee() external view returns (address, uint256) {
return (address(this), 0);
return (gasFeeAddress, gasFee);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

import "@zetachain/protocol-contracts/contracts/zevm/SystemContract.sol";

import "@zetachain/toolkit/contracts/SwapHelperLib.sol";

contract WithdrawERC20 {
SystemContract public immutable systemContract;

error InsufficientInputAmount();

constructor(address systemContractAddress) {
systemContract = SystemContract(systemContractAddress);
}

function withdraw(address zrc20, uint256 amount, bytes memory to) external virtual {
IZRC20(zrc20).transferFrom(msg.sender, address(this), amount);

(address gasZRC20, uint256 gasFee) = IZRC20(zrc20).withdrawGasFee();

uint256 inputForGas = SwapHelperLib.swapTokensForExactTokens(
systemContract.wZetaContractAddress(),
systemContract.uniswapv2FactoryAddress(),
systemContract.uniswapv2Router02Address(),
zrc20,
gasFee,
gasZRC20,
amount
);

if (inputForGas > amount) revert InsufficientInputAmount();

IZRC20(gasZRC20).approve(zrc20, gasFee);
IZRC20(zrc20).withdraw(to, amount - inputForGas);
}
}
3 changes: 2 additions & 1 deletion packages/zevm-app-contracts/data/addresses.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"rewardDistributorFactory": "0xB9dc665610CF5109cE23aBBdaAc315B41FA094c1",
"zetaSwap": "0xA8168Dc495Ed61E70f5c1941e2860050AB902cEF",
"zetaSwapBtcInbound": "0x358E2cfC0E16444Ba7D3164Bbeeb6bEA7472c559",
"invitationManager": "0x3649C03C472B698213926543456E9c21081e529d"
"invitationManager": "0x3649C03C472B698213926543456E9c21081e529d",
"withdrawERC20": "0xa349B9367cc54b47CAb8D09A95836AE8b4D1d84E"
}
}
}
26 changes: 26 additions & 0 deletions packages/zevm-app-contracts/scripts/withdrawERC20/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { isProtocolNetworkName } from "@zetachain/protocol-contracts";
import { ethers, network } from "hardhat";

import { WithdrawERC20__factory } from "../../typechain-types/factories/contracts/withdrawErc20/withdrawErc20.sol";
import { getSystemContractAddress, saveAddress } from "../address.helpers";

const networkName = network.name;

const SYSTEM_CONTRACT = getSystemContractAddress();

async function main() {
if (!isProtocolNetworkName(networkName)) throw new Error("Invalid network name");

const WithdrawERC20Factory = (await ethers.getContractFactory("WithdrawERC20")) as WithdrawERC20__factory;

const withdrawERC20 = await WithdrawERC20Factory.deploy(SYSTEM_CONTRACT);
await withdrawERC20.deployed();

console.log("WithdrawERC20 deployed to:", withdrawERC20.address);
saveAddress("withdrawERC20", withdrawERC20.address, networkName);
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
33 changes: 33 additions & 0 deletions packages/zevm-app-contracts/scripts/withdrawERC20/withdraw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { isProtocolNetworkName } from "@zetachain/protocol-contracts";
import { ethers, network } from "hardhat";

import { ERC20__factory } from "../../typechain-types";
import { WithdrawERC20__factory } from "../../typechain-types/factories/contracts/withdrawErc20/withdrawErc20.sol";
import { getZEVMAppAddress } from "../address.helpers";

const networkName = network.name;

const ZUSDC_ADDRESS = "0x0cbe0dF132a6c6B4a2974Fa1b7Fb953CF0Cc798a";
const AMOUNT = ethers.utils.parseUnits("0.5", 6);

async function main() {
if (!isProtocolNetworkName(networkName)) throw new Error("Invalid network name");

const [signer] = await ethers.getSigners();
const withdrawERC20Address = getZEVMAppAddress("withdrawERC20");

const WithdrawERC20Factory = (await ethers.getContractFactory("WithdrawERC20")) as WithdrawERC20__factory;
const WithdrawERC20 = WithdrawERC20Factory.attach(withdrawERC20Address);

const ERC20Factory = (await ethers.getContractFactory("ERC20")) as ERC20__factory;
const erc20 = ERC20Factory.attach(ZUSDC_ADDRESS);

await erc20.approve(WithdrawERC20.address, AMOUNT);
const tx = await WithdrawERC20.withdraw(erc20.address, AMOUNT, signer.address);
console.log(`Sending transaction ${tx.hash}`);
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
93 changes: 93 additions & 0 deletions packages/zevm-app-contracts/test/Withdraw.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { parseUnits } from "@ethersproject/units";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { getNonZetaAddress } from "@zetachain/protocol-contracts";
import { expect } from "chai";
import { parseEther } from "ethers/lib/utils";
import { ethers, network } from "hardhat";

import { MockSystemContract, MockZRC20 } from "../typechain-types";
import { WithdrawalEvent } from "../typechain-types/contracts/shared/MockZRC20";
import { WithdrawERC20 } from "../typechain-types/contracts/withdrawErc20/withdrawErc20.sol";
import { WithdrawERC20__factory } from "../typechain-types/factories/contracts/withdrawErc20/withdrawErc20.sol";
import { evmSetup } from "./test.helpers";

const encodeToBytes = (destination: string) => {
return ethers.utils.hexlify(ethers.utils.zeroPad(destination, 32));
};

describe("Withdraw tests", () => {
let withdrawERC20Contract: WithdrawERC20;
let ZRC20Contracts: MockZRC20[];
let mockUSDCContracts: MockZRC20;
let systemContract: MockSystemContract;

let accounts: SignerWithAddress[];
let deployer: SignerWithAddress;
let fakeTSS: SignerWithAddress;

beforeEach(async () => {
[deployer, fakeTSS, ...accounts] = await ethers.getSigners();

await network.provider.send("hardhat_setBalance", [deployer.address, parseUnits("1000000").toHexString()]);

const uniswapRouterAddr = getNonZetaAddress("uniswapV2Router02", "eth_mainnet");

const uniswapFactoryAddr = getNonZetaAddress("uniswapV2Factory", "eth_mainnet");

const wGasToken = getNonZetaAddress("weth9", "eth_mainnet");

const evmSetupResult = await evmSetup(wGasToken, uniswapFactoryAddr, uniswapRouterAddr);
ZRC20Contracts = evmSetupResult.ZRC20Contracts;
systemContract = evmSetupResult.systemContract;
mockUSDCContracts = ZRC20Contracts[3];

const WithdrawERC20Factory = (await ethers.getContractFactory("WithdrawERC20")) as WithdrawERC20__factory;
withdrawERC20Contract = (await WithdrawERC20Factory.deploy(systemContract.address)) as WithdrawERC20;
await withdrawERC20Contract.deployed();
});

describe("WithdrawERC20", () => {
it("Should withdraw", async () => {
const encodedDestination = encodeToBytes(fakeTSS.address);
const INITIAL_AMOUNT = parseEther("10");
const gasFee = await mockUSDCContracts.gasFee();
const expectedAmount = INITIAL_AMOUNT.sub(gasFee);

await mockUSDCContracts.approve(withdrawERC20Contract.address, INITIAL_AMOUNT);
const tx = await withdrawERC20Contract.withdraw(mockUSDCContracts.address, INITIAL_AMOUNT, encodedDestination);
const receipt = await tx.wait();

expect(receipt.events).not.to.be.undefined;
expect(receipt.events?.length).to.be.above(1);

//@ts-ignore
const withdrawalEvent: WithdrawalEvent = receipt.events[receipt.events?.length - 2] as WithdrawalEvent;
const decodedEventData = mockUSDCContracts.interface.parseLog(withdrawalEvent);

expect(decodedEventData.args.from).to.equal(withdrawERC20Contract.address);
expect(decodedEventData.args.to).equal(encodedDestination.toLowerCase());

//@dev: We are assuming that the value is within 10% of the expected amount because we lost something on swap
expect(decodedEventData.args.value).to.be.lt(expectedAmount);
expect(decodedEventData.args.value).to.be.gt(expectedAmount.mul(9).div(10));

expect(decodedEventData.args.gasfee).to.equal(gasFee);
expect(decodedEventData.args.protocolFlatFee).to.eq(0);

const balance = await mockUSDCContracts.balanceOf(fakeTSS.address);
expect(balance).to.equal(decodedEventData.args.value);
});

it("Should revert if it's not enough", async () => {
const INITIAL_AMOUNT = parseEther("0.01");

await mockUSDCContracts.approve(withdrawERC20Contract.address, INITIAL_AMOUNT);
const tx = withdrawERC20Contract.withdraw(
mockUSDCContracts.address,
INITIAL_AMOUNT,
encodeToBytes(fakeTSS.address)
);
await expect(tx).to.be.reverted;
});
});
});
8 changes: 6 additions & 2 deletions packages/zevm-app-contracts/test/test.helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MaxUint256 } from "@ethersproject/constants";
import { parseUnits } from "@ethersproject/units";
import { parseEther, parseUnits } from "@ethersproject/units";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { ethers } from "hardhat";

Expand Down Expand Up @@ -48,8 +48,9 @@ export const evmSetup = async (
const token1Contract = (await ZRC20Factory.deploy(parseUnits("1000000"), "tBNB", "tBNB")) as MockZRC20;
const token2Contract = (await ZRC20Factory.deploy(parseUnits("1000000"), "gETH", "gETH")) as MockZRC20;
const token3Contract = (await ZRC20Factory.deploy(parseUnits("1000000"), "tMATIC", "tMATIC")) as MockZRC20;
const token4Contract = (await ZRC20Factory.deploy(parseUnits("1000000"), "USDC", "USDC")) as MockZRC20;

const ZRC20Contracts = [token1Contract, token2Contract, token3Contract];
const ZRC20Contracts = [token1Contract, token2Contract, token3Contract, token4Contract];

const SystemContractFactory = (await ethers.getContractFactory("MockSystemContract")) as MockSystemContract__factory;

Expand All @@ -62,10 +63,13 @@ export const evmSetup = async (
await systemContract.setGasCoinZRC20(97, ZRC20Contracts[0].address);
await systemContract.setGasCoinZRC20(5, ZRC20Contracts[1].address);
await systemContract.setGasCoinZRC20(80001, ZRC20Contracts[2].address);
await ZRC20Contracts[3].setGasFeeAddress(ZRC20Contracts[1].address);
await ZRC20Contracts[3].setGasFee(parseEther("0.01"));

await addZetaEthLiquidity(signer, ZRC20Contracts[0], uniswapRouterAddr);
await addZetaEthLiquidity(signer, ZRC20Contracts[1], uniswapRouterAddr);
await addZetaEthLiquidity(signer, ZRC20Contracts[2], uniswapRouterAddr);
await addZetaEthLiquidity(signer, ZRC20Contracts[3], uniswapRouterAddr);

return { ZRC20Contracts, systemContract };
};
Loading
Loading