diff --git a/omnichain/swap/contracts/shared/MockSystemContract.sol b/omnichain/swap/contracts/shared/MockSystemContract.sol new file mode 100644 index 00000000..3bbc02c4 --- /dev/null +++ b/omnichain/swap/contracts/shared/MockSystemContract.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.7; + +import "@zetachain/protocol-contracts/contracts/zevm/interfaces/zContract.sol"; +import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IZRC20.sol"; + +interface SystemContractErrors { + error CallerIsNotFungibleModule(); + + error InvalidTarget(); + + error CantBeIdenticalAddresses(); + + error CantBeZeroAddress(); +} + +contract MockSystemContract is SystemContractErrors { + error TransferFailed(); + + mapping(uint256 => uint256) public gasPriceByChainId; + mapping(uint256 => address) public gasCoinZRC20ByChainId; + mapping(uint256 => address) public gasZetaPoolByChainId; + + address public wZetaContractAddress; + address public immutable uniswapv2FactoryAddress; + address public immutable uniswapv2Router02Address; + + event SystemContractDeployed(); + event SetGasPrice(uint256, uint256); + event SetGasCoin(uint256, address); + event SetGasZetaPool(uint256, address); + event SetWZeta(address); + + constructor(address wzeta_, address uniswapv2Factory_, address uniswapv2Router02_) { + wZetaContractAddress = wzeta_; + uniswapv2FactoryAddress = uniswapv2Factory_; + uniswapv2Router02Address = uniswapv2Router02_; + emit SystemContractDeployed(); + } + + // fungible module updates the gas price oracle periodically + function setGasPrice(uint256 chainID, uint256 price) external { + gasPriceByChainId[chainID] = price; + emit SetGasPrice(chainID, price); + } + + function setGasCoinZRC20(uint256 chainID, address zrc20) external { + gasCoinZRC20ByChainId[chainID] = zrc20; + emit SetGasCoin(chainID, zrc20); + } + + function setWZETAContractAddress(address addr) external { + wZetaContractAddress = addr; + emit SetWZeta(wZetaContractAddress); + } + + // returns sorted token addresses, used to handle return values from pairs sorted in this order + function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { + if (tokenA == tokenB) revert CantBeIdenticalAddresses(); + (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + if (token0 == address(0)) revert CantBeZeroAddress(); + } + + function uniswapv2PairFor(address factory, address tokenA, address tokenB) public pure returns (address pair) { + (address token0, address token1) = sortTokens(tokenA, tokenB); + pair = address( + uint160( + uint256( + keccak256( + abi.encodePacked( + hex"ff", + factory, + keccak256(abi.encodePacked(token0, token1)), + hex"96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f" // init code hash + ) + ) + ) + ) + ); + } + + function onCrossChainCall(uint256 chainID, address target, address zrc20, uint256 amount, bytes calldata message) external { + zContext memory context = zContext({sender: msg.sender, origin: "", chainID: chainID}); + bool transfer = IZRC20(zrc20).transfer(target, amount); + if (!transfer) revert TransferFailed(); + zContract(target).onCrossChainCall(context, zrc20, amount, message); + } +} \ No newline at end of file diff --git a/omnichain/swap/contracts/shared/MockZRC20.sol b/omnichain/swap/contracts/shared/MockZRC20.sol new file mode 100644 index 00000000..ec8e29c1 --- /dev/null +++ b/omnichain/swap/contracts/shared/MockZRC20.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.7; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@zetachain/toolkit/contracts/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) { + return true; + } + + function bytesToAddress(bytes calldata data, uint256 offset, uint256 size) public pure returns (address output) { + bytes memory b = data[offset:offset + size]; + assembly { + output := mload(add(b, size)) + } + } + + function withdraw(bytes calldata to, uint256 amount) external returns (bool) { + address toAddress; + if (to.length < 32) { + toAddress = BytesHelperLib.bytesToAddress(to, 0); + } else { + 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 (gasFeeAddress, gasFee); + } +} diff --git a/omnichain/swap/contracts/shared/TestUniswapCore.sol b/omnichain/swap/contracts/shared/TestUniswapCore.sol new file mode 100644 index 00000000..601469d0 --- /dev/null +++ b/omnichain/swap/contracts/shared/TestUniswapCore.sol @@ -0,0 +1,10 @@ + +// SPDX-License-Identifier: MIT +pragma solidity 0.5.16; + +/** + * @dev Contracts that need to be compiled for testing purposes + */ + +import "@uniswap/v2-core/contracts/UniswapV2Factory.sol"; +import "@uniswap/v2-core/contracts/UniswapV2Pair.sol"; \ No newline at end of file diff --git a/omnichain/swap/contracts/shared/TestUniswapRouter.sol b/omnichain/swap/contracts/shared/TestUniswapRouter.sol new file mode 100644 index 00000000..ee4be5a4 --- /dev/null +++ b/omnichain/swap/contracts/shared/TestUniswapRouter.sol @@ -0,0 +1,222 @@ +pragma solidity =0.6.6; + +import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol'; +import '@uniswap/lib/contracts/libraries/TransferHelper.sol'; + +import './libraries/UniswapV2Library.sol'; +import './libraries/SafeMath.sol'; +import './interfaces/IERC20.sol'; +import './interfaces/IWETH.sol'; + +contract TestUniswapRouter { + using SafeMath for uint; + + address public immutable factory; + address public immutable WETH; + + modifier ensure(uint deadline) { + require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED'); + _; + } + + constructor(address _factory, address _WETH) public { + factory = _factory; + WETH = _WETH; + } + + receive() external payable { + assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract + } + + // **** ADD LIQUIDITY **** + function _addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin + ) internal returns (uint amountA, uint amountB) { + // create the pair if it doesn't exist yet + if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) { + address pair = IUniswapV2Factory(factory).createPair(tokenA, tokenB); + } + + (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB); + if (reserveA == 0 && reserveB == 0) { + (amountA, amountB) = (amountADesired, amountBDesired); + } else { + uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB); + if (amountBOptimal <= amountBDesired) { + require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); + (amountA, amountB) = (amountADesired, amountBOptimal); + } else { + uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA); + assert(amountAOptimal <= amountADesired); + require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); + (amountA, amountB) = (amountAOptimal, amountBDesired); + } + } + } + function addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) { + (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin); + address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); + TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA); + TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB); + liquidity = IUniswapV2Pair(pair).mint(to); + } + function addLiquidityETH( + address token, + uint amountTokenDesired, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) { + (amountToken, amountETH) = _addLiquidity( + token, + WETH, + amountTokenDesired, + msg.value, + amountTokenMin, + amountETHMin + ); + address pair = UniswapV2Library.pairFor(factory, token, WETH); + TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken); + IWETH(WETH).deposit{value: amountETH}(); + assert(IWETH(WETH).transfer(pair, amountETH)); + liquidity = IUniswapV2Pair(pair).mint(to); + // refund dust eth, if any + if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH); + + } + + // **** REMOVE LIQUIDITY **** + function removeLiquidity( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) public ensure(deadline) returns (uint amountA, uint amountB) { + address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); + IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair + (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to); + (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB); + (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); + require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); + require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); + } + function removeLiquidityETH( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) public ensure(deadline) returns (uint amountToken, uint amountETH) { + (amountToken, amountETH) = removeLiquidity( + token, + WETH, + liquidity, + amountTokenMin, + amountETHMin, + address(this), + deadline + ); + TransferHelper.safeTransfer(token, to, amountToken); + IWETH(WETH).withdraw(amountETH); + TransferHelper.safeTransferETH(to, amountETH); + } + + // **** SWAP **** + // requires the initial amount to have already been sent to the first pair + function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual { + for (uint i; i < path.length - 1; i++) { + (address input, address output) = (path[i], path[i + 1]); + (address token0,) = UniswapV2Library.sortTokens(input, output); + uint amountOut = amounts[i + 1]; + (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0)); + address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; + IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap( + amount0Out, amount1Out, to, new bytes(0) + ); + } + } + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external ensure(deadline) returns (uint[] memory amounts) { + amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); + require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] + ); + _swap(amounts, path, to); + } + function swapTokensForExactTokens( + uint amountOut, + uint amountInMax, + address[] calldata path, + address to, + uint deadline + ) external ensure(deadline) returns (uint[] memory amounts) { + amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); + require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] + ); + _swap(amounts, path, to); + } + + // **** LIBRARY FUNCTIONS **** + function quote(uint amountA, uint reserveA, uint reserveB) public pure returns (uint amountB) { + return UniswapV2Library.quote(amountA, reserveA, reserveB); + } + + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) + public + pure + returns (uint amountOut) + { + return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut); + } + + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) + public + pure + returns (uint amountIn) + { + return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut); + } + + function getAmountsOut(uint amountIn, address[] memory path) + public + view + returns (uint[] memory amounts) + { + return UniswapV2Library.getAmountsOut(factory, amountIn, path); + } + + function getAmountsIn(uint amountOut, address[] memory path) + public + view + returns (uint[] memory amounts) + { + return UniswapV2Library.getAmountsIn(factory, amountOut, path); + } +} diff --git a/omnichain/swap/contracts/shared/WZETA.sol b/omnichain/swap/contracts/shared/WZETA.sol new file mode 100644 index 00000000..b40d1677 --- /dev/null +++ b/omnichain/swap/contracts/shared/WZETA.sol @@ -0,0 +1,61 @@ +pragma solidity ^0.4.18; + +contract WZETA { + string public name = "Wrapped Zeta"; + string public symbol = "WZETA"; + uint8 public decimals = 18; + + event Approval(address indexed src, address indexed guy, uint wad); + event Transfer(address indexed src, address indexed dst, uint wad); + event Deposit(address indexed dst, uint wad); + event Withdrawal(address indexed src, uint wad); + + mapping(address => uint) public balanceOf; + mapping(address => mapping(address => uint)) public allowance; + + function() public payable { + deposit(); + } + + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + Deposit(msg.sender, msg.value); + } + + function withdraw(uint wad) public { + require(balanceOf[msg.sender] >= wad); + balanceOf[msg.sender] -= wad; + msg.sender.transfer(wad); + Withdrawal(msg.sender, wad); + } + + function totalSupply() public view returns (uint) { + return this.balance; + } + + function approve(address guy, uint wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + Approval(msg.sender, guy, wad); + return true; + } + + function transfer(address dst, uint wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom(address src, address dst, uint wad) public returns (bool) { + require(balanceOf[src] >= wad); + + if (src != msg.sender && allowance[src][msg.sender] != uint(-1)) { + require(allowance[src][msg.sender] >= wad); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + Transfer(src, dst, wad); + + return true; + } +} diff --git a/omnichain/swap/contracts/shared/interfaces/IERC20.sol b/omnichain/swap/contracts/shared/interfaces/IERC20.sol new file mode 100644 index 00000000..c1e8c3e6 --- /dev/null +++ b/omnichain/swap/contracts/shared/interfaces/IERC20.sol @@ -0,0 +1,17 @@ +pragma solidity >=0.5.0; + +interface IERC20 { + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + function totalSupply() external view returns (uint); + function balanceOf(address owner) external view returns (uint); + function allowance(address owner, address spender) external view returns (uint); + + function approve(address spender, uint value) external returns (bool); + function transfer(address to, uint value) external returns (bool); + function transferFrom(address from, address to, uint value) external returns (bool); +} diff --git a/omnichain/swap/contracts/shared/interfaces/IWETH.sol b/omnichain/swap/contracts/shared/interfaces/IWETH.sol new file mode 100644 index 00000000..e05fb770 --- /dev/null +++ b/omnichain/swap/contracts/shared/interfaces/IWETH.sol @@ -0,0 +1,7 @@ +pragma solidity >=0.5.0; + +interface IWETH { + function deposit() external payable; + function transfer(address to, uint value) external returns (bool); + function withdraw(uint) external; +} diff --git a/omnichain/swap/contracts/shared/libraries/SafeMath.sol b/omnichain/swap/contracts/shared/libraries/SafeMath.sol new file mode 100644 index 00000000..ba6fc21b --- /dev/null +++ b/omnichain/swap/contracts/shared/libraries/SafeMath.sol @@ -0,0 +1,17 @@ +pragma solidity =0.6.6; + +// a library for performing overflow-safe math, courtesy of DappHub (https://github.com/dapphub/ds-math) + +library SafeMath { + function add(uint x, uint y) internal pure returns (uint z) { + require((z = x + y) >= x, 'ds-math-add-overflow'); + } + + function sub(uint x, uint y) internal pure returns (uint z) { + require((z = x - y) <= x, 'ds-math-sub-underflow'); + } + + function mul(uint x, uint y) internal pure returns (uint z) { + require(y == 0 || (z = x * y) / y == x, 'ds-math-mul-overflow'); + } +} diff --git a/omnichain/swap/contracts/shared/libraries/UniswapV2Library.sol b/omnichain/swap/contracts/shared/libraries/UniswapV2Library.sol new file mode 100644 index 00000000..f6e48616 --- /dev/null +++ b/omnichain/swap/contracts/shared/libraries/UniswapV2Library.sol @@ -0,0 +1,85 @@ +pragma solidity >=0.5.0; + +import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol'; +import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol'; + +import "./SafeMath.sol"; + +library UniswapV2Library { + using SafeMath for uint; + + // returns sorted token addresses, used to handle return values from pairs sorted in this order + function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { + require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES'); + (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS'); + } + + // calculates the CREATE2 address for a pair without making any external calls + function pairFor(address factory, address tokenA, address tokenB) internal view returns (address pair) { + // Not working + // (address token0, address token1) = sortTokens(tokenA, tokenB); + // pair = address(uint(keccak256(abi.encodePacked( + // hex'ff', + // factory, + // keccak256(abi.encodePacked(token0, token1)), + // hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash + // )))); + pair = IUniswapV2Factory(factory).getPair(tokenA, tokenB); + } + + // fetches and sorts the reserves for a pair + function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) { + (address token0,) = sortTokens(tokenA, tokenB); + (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves(); + (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); + } + + // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset + function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) { + require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT'); + require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); + amountB = amountA.mul(reserveB) / reserveA; + } + + // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) { + require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT'); + require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); + uint amountInWithFee = amountIn.mul(997); + uint numerator = amountInWithFee.mul(reserveOut); + uint denominator = reserveIn.mul(1000).add(amountInWithFee); + amountOut = numerator / denominator; + } + + // given an output amount of an asset and pair reserves, returns a required input amount of the other asset + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) { + require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT'); + require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); + uint numerator = reserveIn.mul(amountOut).mul(1000); + uint denominator = reserveOut.sub(amountOut).mul(997); + amountIn = (numerator / denominator).add(1); + } + + // performs chained getAmountOut calculations on any number of pairs + function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) { + require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); + amounts = new uint[](path.length); + amounts[0] = amountIn; + for (uint i; i < path.length - 1; i++) { + (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]); + amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut); + } + } + + // performs chained getAmountIn calculations on any number of pairs + function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) { + require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); + amounts = new uint[](path.length); + amounts[amounts.length - 1] = amountOut; + for (uint i = path.length - 1; i > 0; i--) { + (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]); + amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut); + } + } +} diff --git a/omnichain/swap/hardhat.config.ts b/omnichain/swap/hardhat.config.ts index b28f6f15..1a7be100 100644 --- a/omnichain/swap/hardhat.config.ts +++ b/omnichain/swap/hardhat.config.ts @@ -10,7 +10,21 @@ const config: HardhatUserConfig = { networks: { ...getHardhatConfigNetworks(), }, - solidity: "0.8.7", + solidity: { + compilers: [ + { version: "0.5.10" /** For create2 factory */ }, + { version: "0.6.6" /** For uniswap v2 router*/ }, + { version: "0.5.16" /** For uniswap v2 core*/ }, + { version: "0.4.19" /** For weth*/ }, + { version: "0.8.7" }, + ], + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, }; export default config; diff --git a/omnichain/swap/test/Swap.spec.ts b/omnichain/swap/test/Swap.spec.ts new file mode 100644 index 00000000..28b2c0fa --- /dev/null +++ b/omnichain/swap/test/Swap.spec.ts @@ -0,0 +1,111 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { expect } from "chai"; +import { defaultAbiCoder, parseEther, parseUnits } from "ethers/lib/utils"; +import { ethers, network } from "hardhat"; + +import { + MockSystemContract, + MockZRC20, + Swap, + Swap__factory, + TestUniswapRouter, + UniswapV2Factory, + WZETA, +} from "../typechain-types"; +import { deployUniswap, deployWZETA, evmSetup } from "./test.helpers"; + +describe("Swap", function () { + let uniswapFactory: UniswapV2Factory; + let uniswapRouter: TestUniswapRouter; + let swap: Swap; + let accounts: SignerWithAddress[]; + let deployer: SignerWithAddress; + let systemContract: MockSystemContract; + let ZRC20Contracts: MockZRC20[]; + let wGasToken: WZETA; + + beforeEach(async function () { + accounts = await ethers.getSigners(); + [deployer] = accounts; + + const wZETA = await deployWZETA(deployer); + wGasToken = wZETA; + + const deployResult = await deployUniswap(deployer, wGasToken.address); + uniswapFactory = deployResult.uniswapFactory; + uniswapRouter = deployResult.uniswapRouter; + + const evmSetupResult = await evmSetup( + wGasToken.address, + uniswapFactory.address, + uniswapRouter.address + ); + ZRC20Contracts = evmSetupResult.ZRC20Contracts; + systemContract = evmSetupResult.systemContract; + + const SwapFactory = (await ethers.getContractFactory( + "Swap" + )) as Swap__factory; + + swap = (await SwapFactory.deploy(systemContract.address)) as Swap; + await swap.deployed(); + }); + + describe("Swap", function () { + it("Should do swap from EVM Chain", async function () { + const amount = parseEther("10"); + await ZRC20Contracts[0].transfer(systemContract.address, amount); + + const initBalance = await ZRC20Contracts[1].balanceOf(deployer.address); + + const recipient = ethers.utils.hexlify( + ethers.utils.zeroPad(deployer.address, 32) + ); + + const params = defaultAbiCoder.encode( + ["address", "bytes"], + [ZRC20Contracts[1].address, recipient] + ); + + await systemContract.onCrossChainCall( + 1, // ETH chain id + swap.address, + ZRC20Contracts[0].address, + amount, + params, + { gasLimit: 10_000_000 } + ); + + const endBalance = await ZRC20Contracts[1].balanceOf(deployer.address); + + expect(endBalance).to.be.gt(initBalance); + }); + + it("Should do swap from Bitcoin Chain", async function () { + const amount = parseEther("10"); + await ZRC20Contracts[0].transfer(systemContract.address, amount); + + const initBalance = await ZRC20Contracts[1].balanceOf(deployer.address); + + const rawMemo = `${ZRC20Contracts[1].address}${deployer.address.slice( + 2 + )}`; + const rawMemoBytes = ethers.utils.arrayify(rawMemo); + + const params = ethers.utils.solidityPack(["bytes"], [rawMemoBytes]); + + await systemContract.onCrossChainCall( + 18332, // Bitcoin chain id + swap.address, + ZRC20Contracts[0].address, + amount, + params, + { gasLimit: 10_000_000 } + ); + + const endBalance = await ZRC20Contracts[1].balanceOf(deployer.address); + + expect(endBalance).to.be.gt(initBalance); + }); + }); +}); diff --git a/omnichain/swap/test/test.helpers.ts b/omnichain/swap/test/test.helpers.ts new file mode 100644 index 00000000..be678710 --- /dev/null +++ b/omnichain/swap/test/test.helpers.ts @@ -0,0 +1,167 @@ +import { MaxUint256 } from "@ethersproject/constants"; +import { parseEther, parseUnits } from "@ethersproject/units"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { + MockSystemContract, + MockSystemContract__factory, + MockZRC20, + MockZRC20__factory, + TestUniswapRouter, + TestUniswapRouter__factory, + UniswapV2Factory, + UniswapV2Factory__factory, + WZETA, + WZETA__factory, +} from "../typechain-types"; + +export const deployWZETA = async ( + signer: SignerWithAddress +): Promise => { + const WZETAFactory = (await ethers.getContractFactory( + "WZETA" + )) as WZETA__factory; + const wZETAContract = (await WZETAFactory.deploy()) as WZETA; + await wZETAContract.deployed(); + await wZETAContract.deposit({ value: parseEther("10") }); + return wZETAContract; +}; + +interface UniswapDeployResult { + uniswapFactory: UniswapV2Factory; + uniswapRouter: TestUniswapRouter; +} + +export const deployUniswap = async ( + signer: SignerWithAddress, + wZETA: string +): Promise => { + const UniswapV2Factory = (await ethers.getContractFactory( + "UniswapV2Factory" + )) as UniswapV2Factory__factory; + const uniswapFactory = (await UniswapV2Factory.deploy( + signer.address + )) as UniswapV2Factory; + await uniswapFactory.deployed(); + + const UniswapRouter = (await ethers.getContractFactory( + "TestUniswapRouter" + )) as TestUniswapRouter__factory; + const uniswapRouter = (await UniswapRouter.deploy( + uniswapFactory.address, + wZETA + )) as TestUniswapRouter; + await uniswapRouter.deployed(); + + return { uniswapFactory, uniswapRouter }; +}; + +const addZetaEthLiquidity = async ( + signer: SignerWithAddress, + token: MockZRC20, + uniswapRouterAddr: string +) => { + const block = await ethers.provider.getBlock("latest"); + + const tx1 = await token.approve(uniswapRouterAddr, MaxUint256); + await tx1.wait(); + + const uniswapRouterFork = TestUniswapRouter__factory.connect( + uniswapRouterAddr, + signer + ); + + const tx2 = await uniswapRouterFork.addLiquidityETH( + token.address, + parseUnits("2000"), + 0, + 0, + signer.address, + block.timestamp + 360, + { + gasLimit: 10_000_000, + value: parseUnits("1000"), + } + ); + await tx2.wait(); +}; + +interface EvmSetupResult { + ZRC20Contracts: MockZRC20[]; + systemContract: MockSystemContract; +} + +export const evmSetup = async ( + gasTokenAddr: string, + uniswapFactoryAddr: string, + uniswapRouterAddr: string +): Promise => { + const [signer] = await ethers.getSigners(); + + const ZRC20Factory = (await ethers.getContractFactory( + "MockZRC20" + )) as MockZRC20__factory; + + 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, + token4Contract, + ]; + + const SystemContractFactory = (await ethers.getContractFactory( + "MockSystemContract" + )) as MockSystemContract__factory; + + const systemContract = (await SystemContractFactory.deploy( + gasTokenAddr, + uniswapFactoryAddr, + uniswapRouterAddr + )) as MockSystemContract; + + await systemContract.setGasCoinZRC20(97, ZRC20Contracts[0].address); + await systemContract.setGasCoinZRC20(5, ZRC20Contracts[1].address); + await systemContract.setGasCoinZRC20(80001, ZRC20Contracts[2].address); + + await ZRC20Contracts[0].setGasFeeAddress(ZRC20Contracts[0].address); + await ZRC20Contracts[0].setGasFee(parseEther("0.01")); + + await ZRC20Contracts[1].setGasFeeAddress(ZRC20Contracts[1].address); + await ZRC20Contracts[1].setGasFee(parseEther("0.01")); + + await ZRC20Contracts[2].setGasFeeAddress(ZRC20Contracts[2].address); + await ZRC20Contracts[2].setGasFee(parseEther("0.01")); + + 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 }; +};