diff --git a/balancer-js/examples/data/onchain-multicall3.ts b/balancer-js/examples/data/onchain-multicall3.ts new file mode 100644 index 000000000..790da602e --- /dev/null +++ b/balancer-js/examples/data/onchain-multicall3.ts @@ -0,0 +1,94 @@ +import { PoolsSubgraphRepository } from '@/modules/data/pool/subgraph' +import { getOnChainBalances as getOnChainBalances3 } from '@/modules/sor/pool-data/onChainData3' +import { SubgraphPoolBase } from '@/.' +import { getOnChainBalances } from '@/modules/sor/pool-data/onChainData' +import { JsonRpcProvider } from '@ethersproject/providers' +import _ from 'lodash' + +const pools = new PoolsSubgraphRepository({ + // url: 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-v2', + // chainId: 1, + // url: 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-polygon-v2', + // chainId: 137, + // url: 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-arbitrum-v2', + // chainId: 42161, + // url: 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-optimism-v2', + // chainId: 10, + // url: 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-gnosis-chain-v2', + // chainId: 100, + // url: 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-avalanche-v2', + // chainId: 43114, + url: 'https://api.studio.thegraph.com/query/24660/balancer-base-v2/version/latest', + chainId: 8453, + // url: 'https://api.thegraph.com/subgraphs/name/beethovenxfi/beethovenx-v2-fantom', + // chainId: 250, + // url: 'https://api.studio.thegraph.com/query/24660/balancer-polygon-zk-v2/version/latest', + // chainId: 1101, + query: { + args: { + first: 1000, + skip: 0, + orderBy: 'totalLiquidity', + orderDirection: 'desc', + // where: { + // poolType: { + // eq: "MetaStable" + // }, + // }, + }, + attrs: {}, + }, +}) + +// const provider = new JsonRpcProvider('https://eth-mainnet.alchemyapi.io/v2/7gYoDJEw6-QyVP5hd2UfZyelzDIDemGz') +// const provider = new JsonRpcProvider('https://polygon-mainnet.g.alchemy.com/v2/7gYoDJEw6-QyVP5hd2UfZyelzDIDemGz') +// const provider = new JsonRpcProvider('https://arb-mainnet.g.alchemy.com/v2/7gYoDJEw6-QyVP5hd2UfZyelzDIDemGz') +// const provider = new JsonRpcProvider('https://opt-mainnet.g.alchemy.com/v2/7gYoDJEw6-QyVP5hd2UfZyelzDIDemGz') +const provider = new JsonRpcProvider('https://rpc.ankr.com/base') +// const provider = new JsonRpcProvider('https://rpc.ankr.com/fantom') +// const provider = new JsonRpcProvider('http://127.0.0.1:8545') +// const provider = new JsonRpcProvider('https://polygonzkevm-mainnet.g.alchemy.com/v2/KPHdcks5jBh7RC54-QwlQbe3gtdgGxSD') + +function findNestedValueDifferences(object1: any, object2: any, path = ''): any { + const allKeys = _.union(Object.keys(object1), Object.keys(object2)) + + const differences = [] + + for (const key of allKeys) { + const newPath = path ? `${path}.${key}` : key + + if (_.isObject(object1[key]) && _.isObject(object2[key])) { + differences.push(...findNestedValueDifferences(object1[key], object2[key], newPath)) + } else if (!_.isEqual(object1[key], object2[key])) { + differences.push({ + path: newPath, + value1: object1[key], + value2: object2[key] + }) + } + } + + return differences +} + +async function main() { + const subgraph = await pools.fetch() as SubgraphPoolBase[]; + const onchain3 = await getOnChainBalances3(subgraph, '', '0xBA12222222228d8Ba445958a75a0704d566BF2C8', provider); + console.log(onchain3.length) + // const onchain = await getOnChainBalances(subgraph, '0xeefba1e63905ef1d7acba5a8513c70307c1ce441', '0xBA12222222228d8Ba445958a75a0704d566BF2C8', provider); + // console.log(onchain.length) + // for(const i in subgraph) { + // const one = onchain3.find((x) => x.id === subgraph[i].id) + // const two = onchain.find((x) => x.id === subgraph[i].id) + // console.log('Pool', subgraph[i].id) + // if (!two) { + // console.log('two missing') + // continue + // } + // console.log(JSON.stringify(findNestedValueDifferences(one, two), null, 2)); + // } +} + +main() + +// yarn example ./examples/data/onchain-multicall3.ts diff --git a/balancer-js/src/lib/abi/Multicall3.json b/balancer-js/src/lib/abi/Multicall3.json new file mode 100644 index 000000000..d9c5855e7 --- /dev/null +++ b/balancer-js/src/lib/abi/Multicall3.json @@ -0,0 +1,440 @@ +[ + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "aggregate", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "bytes[]", + "name": "returnData", + "type": "bytes[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bool", + "name": "allowFailure", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call3[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "aggregate3", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bool", + "name": "allowFailure", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call3Value[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "aggregate3Value", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "blockAndAggregate", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "getBasefee", + "outputs": [ + { + "internalType": "uint256", + "name": "basefee", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "name": "getBlockHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBlockNumber", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getChainId", + "outputs": [ + { + "internalType": "uint256", + "name": "chainid", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockCoinbase", + "outputs": [ + { + "internalType": "address", + "name": "coinbase", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockDifficulty", + "outputs": [ + { + "internalType": "uint256", + "name": "difficulty", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockGasLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "gaslimit", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockTimestamp", + "outputs": [ + { + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "getEthBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLastBlockHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "requireSuccess", + "type": "bool" + }, + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "tryAggregate", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "requireSuccess", + "type": "bool" + }, + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "tryBlockAndAggregate", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "payable", + "type": "function" + } +] \ No newline at end of file diff --git a/balancer-js/src/lib/utils/multiCaller3.ts b/balancer-js/src/lib/utils/multiCaller3.ts new file mode 100644 index 000000000..9588b4467 --- /dev/null +++ b/balancer-js/src/lib/utils/multiCaller3.ts @@ -0,0 +1,103 @@ +import { set } from 'lodash'; +import { Fragment, JsonFragment, Interface, Result } from '@ethersproject/abi'; +import { CallOverrides } from '@ethersproject/contracts'; +import { Multicall3, Multicall3__factory } from '@/contracts'; +import { Provider } from '@ethersproject/providers'; + +export class Multicaller3 { + private interface: Interface; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private calls: [string, string, any][] = []; + private paths: string[] = []; + address = '0xcA11bde05977b3631167028862bE2a173976CA11'; + multicall: Multicall3; + + constructor( + abi: string | Array, + provider: Provider, + private options: CallOverrides = {} + ) { + this.interface = new Interface(abi); + this.multicall = Multicall3__factory.connect(this.address, provider); + } + + call( + path: string, + address: string, + functionName: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + params?: any[] + ): Multicaller3 { + this.calls.push([address, functionName, params]); + this.paths.push(path); + return this; + } + + async execute( + from: Record = {}, + batchSize = 1024 // Define the number of function calls in each batch + ): Promise> { + const obj = from; + const results = await this.executeMulticall(batchSize); + results.forEach((result, i) => + set(obj, this.paths[i], result.length > 1 ? result : result[0]) + ); + this.calls = []; + this.paths = []; + return obj; + } + + private async executeMulticall(batchSize: number): Promise { + const numBatches = Math.ceil(this.calls.length / batchSize); + const results: Result[] = []; + + const batchPromises = []; + + for (let batchIndex = 0; batchIndex < numBatches; batchIndex++) { + const batchCalls = this.calls.slice( + batchIndex * batchSize, + (batchIndex + 1) * batchSize + ); + + const batchRequests = batchCalls.map( + ([address, functionName, params]) => ({ + target: address, + allowFailure: true, + callData: this.interface.encodeFunctionData(functionName, params), + }) + ); + + batchPromises.push( + this.multicall.callStatic.aggregate3(batchRequests, this.options) + ); + } + + const batchResults = await Promise.all(batchPromises); + + batchResults.forEach((res, batchIndex) => { + const offset = batchIndex * batchSize; + + for (let i = 0; i < res.length; i++) { + const callIndex = offset + i; + const { success, returnData } = res[i]; + + if (success) { + try { + const result = this.interface.decodeFunctionResult( + this.calls[callIndex][1], + returnData + ); + results[callIndex] = result; + } catch (e) { + console.error('Multicall error', this.paths[callIndex]); + results[callIndex] = []; + } + } else { + results[callIndex] = []; + } + } + }); + + return results; + } +} diff --git a/balancer-js/src/modules/sor/pool-data/onChainData3.ts b/balancer-js/src/modules/sor/pool-data/onChainData3.ts new file mode 100644 index 000000000..1232273cf --- /dev/null +++ b/balancer-js/src/modules/sor/pool-data/onChainData3.ts @@ -0,0 +1,228 @@ +import { Multicaller3 } from '@/lib/utils/multiCaller3'; +import { Provider } from '@ethersproject/providers'; +import { SubgraphPoolBase } from '@/.'; +import { formatFixed } from '@ethersproject/bignumber'; +import { SubgraphToken } from '@balancer-labs/sor'; + +const abi = [ + 'function getSwapFeePercentage() view returns (uint256)', + 'function percentFee() view returns (uint256)', + 'function protocolPercentFee() view returns (uint256)', + 'function getNormalizedWeights() view returns (uint256[])', + 'function totalSupply() view returns (uint256)', + 'function getVirtualSupply() view returns (uint256)', + 'function getActualSupply() view returns (uint256)', + 'function getTargets() view returns (uint256 lowerTarget, uint256 upperTarget)', + 'function getTokenRates() view returns (uint256, uint256)', + 'function getWrappedTokenRate() view returns (uint256)', + 'function getAmplificationParameter() view returns (uint256 value, bool isUpdating, uint256 precision)', + 'function getPausedState() view returns (bool)', + 'function inRecoveryMode() view returns (bool)', + 'function getRate() view returns (uint256)', + 'function getScalingFactors() view returns (uint256[] memory)', // do we need this here? + 'function getPoolTokens(bytes32) view returns (address[], uint256[])', +]; + +const getTotalSupplyFn = (poolType: string) => { + if (poolType.includes('Linear') || ['StablePhantom'].includes(poolType)) { + return 'getVirtualSupply'; + } else if (poolType === 'ComposableStable') { + return 'getActualSupply'; + } else { + return 'totalSupply'; + } +}; + +const getSwapFeeFn = (poolType: string) => { + if (poolType === 'Element') { + return 'percentFee'; + } else if (poolType === 'FX') { + return 'protocolPercentFee'; + } else { + return 'getSwapFeePercentage'; + } +}; + +interface OnchainData { + poolTokens: [string[], string[]]; + totalShares: string; + swapFee: string; + isPaused?: boolean; + inRecoveryMode?: boolean; + rate?: string; + scalingFactors?: string[]; + weights?: string[]; + targets?: [string, string]; + wrappedTokenRate?: string; + amp?: [string, boolean, string]; + tokenRates?: [string, string]; +} + +type GenericPool = SubgraphPoolBase; +// Omit & { +// tokens: (SubgraphToken | PoolToken)[]; +// }; + +const defaultCalls = ( + id: string, + address: string, + vaultAddress: string, + poolType: string, + multicaller: Multicaller3 +) => { + multicaller.call(`${id}.poolTokens`, vaultAddress, 'getPoolTokens', [id]); + multicaller.call(`${id}.totalShares`, address, getTotalSupplyFn(poolType)); + multicaller.call(`${id}.swapFee`, address, getSwapFeeFn(poolType)); + // multicaller.call(`${id}.isPaused`, address, 'getPausedState'); + // multicaller.call(`${id}.inRecoveryMode`, address, 'inRecoveryMode'); + // multicaller.call(`${id}.rate`, address, 'getRate'); + // multicaller.call(`${id}.scalingFactors`, address, 'getScalingFactors'); +}; + +const weightedCalls = ( + id: string, + address: string, + multicaller: Multicaller3 +) => { + multicaller.call(`${id}.weights`, address, 'getNormalizedWeights'); +}; + +const linearCalls = ( + id: string, + address: string, + multicaller: Multicaller3 +) => { + multicaller.call(`${id}.targets`, address, 'getTargets'); + multicaller.call(`${id}.wrappedTokenRate`, address, 'getWrappedTokenRate'); +}; + +const stableCalls = ( + id: string, + address: string, + multicaller: Multicaller3 +) => { + multicaller.call(`${id}.amp`, address, 'getAmplificationParameter'); +}; + +const gyroECalls = (id: string, address: string, multicaller: Multicaller3) => { + multicaller.call(`${id}.tokenRates`, address, 'getTokenRates'); +}; + +const poolTypeCalls = (poolType: string) => { + switch (poolType) { + case 'Weighted': + case 'LiquidityBootstrapping': + case 'Investment': + return weightedCalls; + case 'Stable': + case 'StablePhantom': + case 'MetaStable': + case 'ComposableStable': + return stableCalls; + case 'GyroE': + return gyroECalls; + default: + if (poolType.includes('Linear')) { + return linearCalls; + } else { + return () => ({}); // do nothing + } + } +}; + +const merge = (pool: GenericPool, result: OnchainData) => ({ + ...pool, + tokens: pool.tokens.map((token) => { + const idx = result.poolTokens[0] + .map((t) => t.toLowerCase()) + .indexOf(token.address); + const wrappedToken = + pool.wrappedIndex && pool.tokensList[pool.wrappedIndex]; + return { + ...token, + balance: formatFixed(result.poolTokens[1][idx], token.decimals || 18), + weight: + (result.weights && formatFixed(result.weights[idx], 18)) || + token.weight, + priceRate: + (result.wrappedTokenRate && + wrappedToken && + wrappedToken.toLowerCase() === token.address.toLowerCase() && + formatFixed(result.wrappedTokenRate, 18)) || + token.priceRate, + } as SubgraphToken; + }), + totalShares: result.totalShares + ? formatFixed(result.totalShares, 18) + : pool.totalShares, + swapFee: formatFixed(result.swapFee, 18), + amp: + (result.amp && + result.amp[0] && + formatFixed(result.amp[0], String(result.amp[2]).length - 1)) || + undefined, + lowerTarget: + (result.targets && formatFixed(result.targets[0], 18)) || undefined, + upperTarget: + (result.targets && formatFixed(result.targets[1], 18)) || undefined, + tokenRates: + (result.tokenRates && + result.tokenRates.map((rate) => formatFixed(rate, 18))) || + undefined, + // rate: result.rate, + // isPaused: result.isPaused, + // inRecoveryMode: result.inRecoveryMode, + // scalingFactors: result.scalingFactors, +}); + +export const fetchOnChainPoolData = async ( + pools: { + id: string; + address: string; + poolType: string; + }[], + vaultAddress: string, + provider: Provider +): Promise<{ [id: string]: OnchainData }> => { + if (pools.length === 0) { + return {}; + } + + const multicaller = new Multicaller3(abi, provider); + + pools.forEach(({ id, address, poolType }) => { + defaultCalls(id, address, vaultAddress, poolType, multicaller); + poolTypeCalls(poolType)(id, address, multicaller); + }); + + // ZkEVM needs a smaller batch size + const results = (await multicaller.execute({}, 128)) as { + [id: string]: OnchainData; + }; + + return results; +}; + +export async function getOnChainBalances( + subgraphPoolsOriginal: GenericPool[], + _multiAddress: string, + vaultAddress: string, + provider: Provider +): Promise { + if (subgraphPoolsOriginal.length === 0) return subgraphPoolsOriginal; + + const poolsWithOnchainData: GenericPool[] = []; + + const onchainData = (await fetchOnChainPoolData( + subgraphPoolsOriginal, + vaultAddress, + provider + )) as { [id: string]: OnchainData }; + + subgraphPoolsOriginal.forEach((pool) => { + const data = onchainData[pool.id]; + poolsWithOnchainData.push(merge(pool, data)); + }); + + return poolsWithOnchainData; +} diff --git a/balancer-js/src/modules/sor/pool-data/subgraphPoolDataService.ts b/balancer-js/src/modules/sor/pool-data/subgraphPoolDataService.ts index 86dbcef33..94c0b1af7 100644 --- a/balancer-js/src/modules/sor/pool-data/subgraphPoolDataService.ts +++ b/balancer-js/src/modules/sor/pool-data/subgraphPoolDataService.ts @@ -6,7 +6,7 @@ import { SubgraphClient, } from '@/modules/subgraph/subgraph'; import { parseInt } from 'lodash'; -import { getOnChainBalances } from './onChainData'; +import { getOnChainBalances } from './onChainData3'; import { Provider } from '@ethersproject/providers'; import { BalancerNetworkConfig,