Skip to content

Commit

Permalink
Merge branch 'main' into networks-v9-protocol-v9
Browse files Browse the repository at this point in the history
  • Loading branch information
fadeev authored Aug 8, 2024
2 parents fa75458 + f59445c commit fe18398
Show file tree
Hide file tree
Showing 7 changed files with 384 additions and 48 deletions.
84 changes: 84 additions & 0 deletions contracts/SwapHelperLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router01.sol";
import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IZRC20.sol";
import "@zetachain/protocol-contracts/contracts/zevm/SystemContract.sol";
import "./shared/libraries/UniswapV2Library.sol";

library SwapHelperLib {
uint16 internal constant MAX_DEADLINE = 200;
Expand All @@ -17,6 +18,8 @@ library SwapHelperLib {

error CantBeZeroAddress();

error InvalidPathLength();

// returns sorted token addresses, used to handle return values from pairs sorted in this order
function sortTokens(
address tokenA,
Expand Down Expand Up @@ -85,13 +88,77 @@ library SwapHelperLib {
IZRC20(zrc20B).balanceOf(uniswapPool) > 0;
}

function _isSufficientLiquidity(
address uniswapV2Factory,
uint256 amountIn,
uint256 minAmountOut,
address[] memory path
) internal view returns (bool) {
if (path.length != 2) revert InvalidPathLength();
bool existsPairPool = _existsPairPool(
uniswapV2Factory,
path[0],
path[1]
);
if (!existsPairPool) {
return false;
}
uint256[] memory amounts = UniswapV2Library.getAmountsOut(uniswapV2Factory, amountIn, path);
return amounts[amounts.length - 1] >= minAmountOut;
}

function swapExactTokensForTokens(
SystemContract systemContract,
address zrc20,
uint256 amount,
address targetZRC20,
uint256 minAmountOut
) internal returns (uint256) {

address[] memory path;
path = new address[](2);
path[0] = zrc20;
path[1] = targetZRC20;

bool isSufficientLiquidity = _isSufficientLiquidity(
systemContract.uniswapv2FactoryAddress(),
amount,
minAmountOut,
path
);

bool isZETA = targetZRC20 == systemContract.wZetaContractAddress() || zrc20 == systemContract.wZetaContractAddress();

if (!isSufficientLiquidity && !isZETA) {
path = new address[](3);
path[0] = zrc20;
path[1] = systemContract.wZetaContractAddress();
path[2] = targetZRC20;
}

IZRC20(zrc20).approve(
address(systemContract.uniswapv2Router02Address()),
amount
);
uint256[] memory amounts = IUniswapV2Router01(
systemContract.uniswapv2Router02Address()
).swapExactTokensForTokens(
amount,
minAmountOut,
path,
address(this),
block.timestamp + MAX_DEADLINE
);
return amounts[path.length - 1];
}

function swapExactTokensForTokensDirectly(
SystemContract systemContract,
address zrc20,
uint256 amount,
address targetZRC20,
uint256 minAmountOut
) internal returns (uint256) {
bool existsPairPool = _existsPairPool(
systemContract.uniswapv2FactoryAddress(),
zrc20,
Expand Down Expand Up @@ -166,4 +233,21 @@ library SwapHelperLib {
);
return amounts[0];
}

function getMinOutAmount(SystemContract systemContract, address zrc20, address target, uint256 amountIn) public view returns (uint256 minOutAmount) {
address[] memory path;

path = new address[](2);
path[0] = zrc20;
path[1] = target;
uint[] memory amounts1 = UniswapV2Library.getAmountsOut(systemContract.uniswapv2FactoryAddress(), amountIn, path);

path = new address[](3);
path[0] = zrc20;
path[1] = systemContract.wZetaContractAddress();
path[2] = target;
uint[] memory amounts2 = UniswapV2Library.getAmountsOut(systemContract.uniswapv2FactoryAddress(), amountIn, path);

minOutAmount = amounts1[amounts1.length - 1] > amounts2[amounts2.length - 1] ? amounts1[amounts1.length - 1] : amounts2[amounts2.length - 1];
}
}
39 changes: 25 additions & 14 deletions packages/client/src/getBalances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,21 @@ export const getBalances = async function (
}
});
supportedChains.forEach((chain: any) => {
const contract = getAddress("zetaToken", chain.chain_name as any);
if (contract) {
tokens.push({
chain_id: chain.chain_id,
coin_type: "ERC20",
contract,
decimals: 18,
symbol: "WZETA",
});
const chainLabel = Object.keys(this.getChains()).find(
(key) => this.getChains()[key].chain_id === parseInt(chain.chain_id)
);

if (chainLabel) {
const contract = getAddress("zetaToken", chainLabel as any);
if (contract) {
tokens.push({
chain_id: chain.chain_id,
coin_type: "ERC20",
contract,
decimals: 18,
symbol: "WZETA",
});
}
}
});
tokens.push({
Expand Down Expand Up @@ -217,11 +223,16 @@ export const getBalances = async function (
!["btc_testnet", "btc_mainnet"].includes(token.chain_name)
)
.map(async (token) => {
const rpc = await this.getEndpoint("evm", token.chain_name);
const provider = new ethers.providers.StaticJsonRpcProvider(rpc);
const balance = await provider.getBalance(evmAddress);
const formattedBalance = formatUnits(balance, token.decimals);
balances.push({ ...token, balance: formattedBalance });
const chainLabel = Object.keys(this.getChains()).find(
(key) => this.getChains()[key].chain_id === parseInt(token.chain_id)
);
if (chainLabel) {
const rpc = await this.getEndpoint("evm", chainLabel);
const provider = new ethers.providers.StaticJsonRpcProvider(rpc);
const balance = await provider.getBalance(evmAddress);
const formattedBalance = formatUnits(balance, token.decimals);
balances.push({ ...token, balance: formattedBalance });
}
})
);

Expand Down
134 changes: 103 additions & 31 deletions packages/client/src/getPools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import SystemContract from "@zetachain/protocol-contracts/abi/zevm/SystemContrac
import { ethers } from "ethers";

import { ZetaChainClient } from "./client";
import MULTICALL3_ABI from "./multicall3.json";

type Pair = {
key: string;
tokenA: string;
tokenB: string;
};

export const getPools = async function (this: ZetaChainClient) {
const rpc = this.getEndpoint("evm", `zeta_${this.network}`);
Expand Down Expand Up @@ -38,11 +45,11 @@ export const getPools = async function (this: ZetaChainClient) {
);
tokenAddresses.push(zetaTokenAddress);

const uniquePairs = tokenAddresses.reduce(
(pairs: any, tokenA: string, i: any) => {
tokenAddresses.slice(i + 1).forEach((tokenB: any) => {
const uniquePairs: Pair[] = tokenAddresses.reduce(
(pairs: Pair[], tokenA: string, i: number) => {
tokenAddresses.slice(i + 1).forEach((tokenB: string) => {
const pairKey = [tokenA, tokenB].sort().join("-");
if (!pairs.some((p: any) => p.key === pairKey)) {
if (!pairs.some((p: Pair) => p.key === pairKey)) {
pairs.push({ key: pairKey, tokenA, tokenB });
}
});
Expand All @@ -51,38 +58,103 @@ export const getPools = async function (this: ZetaChainClient) {
[]
);

const poolPromises = uniquePairs.map(async ({ tokenA, tokenB }: any) => {
const pair = await systemContract.uniswapv2PairFor(
const multicallAddress = "0xca11bde05977b3631167028862be2a173976ca11";
const multicallContract = new ethers.Contract(
multicallAddress,
MULTICALL3_ABI,
provider
);

const calls = uniquePairs.map(({ tokenA, tokenB }) => ({
callData: systemContract.interface.encodeFunctionData("uniswapv2PairFor", [
uniswapV2FactoryAddress,
tokenA,
tokenB
);
tokenB,
]),
target: systemContractAddress,
}));

const { returnData } = await multicallContract.callStatic.aggregate(calls);

const validPairs = returnData
.map((data: any, index: number) => {
try {
const pair = systemContract.interface.decodeFunctionResult(
"uniswapv2PairFor",
data
)[0];
return pair !== ethers.constants.AddressZero ? pair : null;
} catch {
return null;
}
})
.filter((pair: string | null) => pair !== null);

const pairCalls = validPairs
.map((pair: string) => [
{
callData: new ethers.utils.Interface(
UniswapV2Pair.abi
).encodeFunctionData("token0"),
target: pair,
},
{
callData: new ethers.utils.Interface(
UniswapV2Pair.abi
).encodeFunctionData("token1"),
target: pair,
},
{
callData: new ethers.utils.Interface(
UniswapV2Pair.abi
).encodeFunctionData("getReserves"),
target: pair,
},
])
.flat();

if (pair === ethers.constants.AddressZero) return null;
const pairReturnData = await multicallContract.callStatic.aggregate(
pairCalls
);

const pools = [];
const uniswapInterface = new ethers.utils.Interface(UniswapV2Pair.abi);

for (let i = 0; i < pairReturnData.returnData.length; i += 3) {
const pairIndex = Math.floor(i / 3);
const pair = validPairs[pairIndex];

if (
!pairReturnData.returnData[i] ||
!pairReturnData.returnData[i + 1] ||
!pairReturnData.returnData[i + 2]
) {
console.warn(`Missing data for pair ${pair} at index ${i}`);
continue;
}

const token0Data = pairReturnData.returnData[i];
const token1Data = pairReturnData.returnData[i + 1];
const reservesData = pairReturnData.returnData[i + 2];

// Check if data can be decoded
let token0, token1, reserves;
try {
const pairContract = new ethers.Contract(
pair,
UniswapV2Pair.abi,
provider
token0 = uniswapInterface.decodeFunctionResult("token0", token0Data)[0];
token1 = uniswapInterface.decodeFunctionResult("token1", token1Data)[0];
reserves = uniswapInterface.decodeFunctionResult(
"getReserves",
reservesData
);
const [token0, token1] = await Promise.all([
pairContract.token0(),
pairContract.token1(),
]);
const reserves = await pairContract.getReserves();

return {
pair,
t0: { address: token0, reserve: reserves[0] },
t1: { address: token1, reserve: reserves[1] },
};
} catch (error) {
return null;
} catch {
continue;
}
});

let pools = (await Promise.all(poolPromises)).filter((pool) => pool !== null);
pools.push({
pair,
t0: { address: token0, reserve: reserves._reserve0 },
t1: { address: token1, reserve: reserves._reserve1 },
});
}

const zrc20Details = foreignCoins.reduce((acc: any, coin: any) => {
acc[coin.zrc20_contract_address.toLowerCase()] = {
Expand All @@ -92,7 +164,7 @@ export const getPools = async function (this: ZetaChainClient) {
return acc;
}, {});

pools = pools.map((t: any) => {
const formattedPools = pools.map((t: any) => {
const zeta = { decimals: 18, symbol: "WZETA" };
const t0 = t.t0.address.toLowerCase();
const t1 = t.t1.address.toLowerCase();
Expand All @@ -111,5 +183,5 @@ export const getPools = async function (this: ZetaChainClient) {
};
});

return pools;
return formattedPools;
};
38 changes: 38 additions & 0 deletions packages/client/src/multicall3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "target",
"type": "address"
},
{
"internalType": "bytes",
"name": "callData",
"type": "bytes"
}
],
"internalType": "struct IMulticall3.Call[]",
"name": "calls",
"type": "tuple[]"
}
],
"name": "aggregate",
"outputs": [
{
"internalType": "uint256",
"name": "blockNumber",
"type": "uint256"
},
{
"internalType": "bytes[]",
"name": "returnData",
"type": "bytes[]"
}
],
"stateMutability": "view",
"type": "function"
}
]
2 changes: 1 addition & 1 deletion packages/tasks/src/cctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ export const cctxTask = task(
)
.addPositionalParam("tx", "Hash of an inbound or a cross-chain transaction")
.addFlag("json", "Output as JSON")
.addOptionalParam("type", "Testnet or mainnet", "testnet");
.addFlag("mainnet", "Run the task on mainnet");
Loading

0 comments on commit fe18398

Please sign in to comment.