From c78cd3d00020ef517322fd6d3bc883bef2cd831e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pr=C3=A9vost?= <998369+prevostc@users.noreply.github.com> Date: Fri, 19 Apr 2024 23:57:45 +0200 Subject: [PATCH] Add multicall --- abis/multicall/Multicall3.json | 440 +++++++++++++++++++++++++++++++++ config/arbitrum.json | 1 + config/base.json | 1 + config/optimism.json | 1 + src/config.template.ts | 3 +- src/utils/multicall.ts | 41 +++ src/utils/price.ts | 62 ++--- subgraph.template.yaml | 10 + 8 files changed, 510 insertions(+), 49 deletions(-) create mode 100644 abis/multicall/Multicall3.json create mode 100644 src/utils/multicall.ts diff --git a/abis/multicall/Multicall3.json b/abis/multicall/Multicall3.json new file mode 100644 index 0000000..0da96a5 --- /dev/null +++ b/abis/multicall/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/config/arbitrum.json b/config/arbitrum.json index 25aa174..8005826 100644 --- a/config/arbitrum.json +++ b/config/arbitrum.json @@ -1,6 +1,7 @@ { "network": "arbitrum-one", + "multicall3Address": "0xcA11bde05977b3631167028862bE2a173976CA11", "shareTokenMintAddress": "0x0000000000000000000000000000000000000000", "vaultFactoryAddress": "0xB45B92C318277d57328fE09DD5cF6Bd53F4F269B", diff --git a/config/base.json b/config/base.json index 78029cd..fefaea5 100644 --- a/config/base.json +++ b/config/base.json @@ -1,6 +1,7 @@ { "network": "base", + "multicall3Address": "0xcA11bde05977b3631167028862bE2a173976CA11", "shareTokenMintAddress": "0x0000000000000000000000000000000000000000", "vaultFactoryAddress": "0x30b94De3d18C9ab9fdf0dDF41077cc057F644E79", diff --git a/config/optimism.json b/config/optimism.json index e465a34..00c8c37 100644 --- a/config/optimism.json +++ b/config/optimism.json @@ -1,6 +1,7 @@ { "network": "optimism", + "multicall3Address": "0xcA11bde05977b3631167028862bE2a173976CA11", "shareTokenMintAddress": "0x0000000000000000000000000000000000000000", "vaultFactoryAddress": "0x45df9B06fB22eA5f89541F613ba0733c2FbFc625", diff --git a/src/config.template.ts b/src/config.template.ts index 9c2f246..3d559b7 100644 --- a/src/config.template.ts +++ b/src/config.template.ts @@ -5,4 +5,5 @@ export const WNATIVE_TOKEN_ADDRESS = Address.fromString("{{wrappedNativeAddress} export const WNATIVE_DECIMALS = BigInt.fromU32({{wrappedNativeDecimals}}) export const CHAINLINK_NATIVE_PRICE_FEED_ADDRESS = Address.fromString("{{chainlinkNativePriceFeedAddress}}") export const PRICE_FEED_DECIMALS = BigInt.fromU32({{chainlinkNativePriceFeedDecimals}}) -export const SHARE_TOKEN_MINT_ADDRESS = Address.fromString("{{shareTokenMintAddress}}") \ No newline at end of file +export const SHARE_TOKEN_MINT_ADDRESS = Address.fromString("{{shareTokenMintAddress}}") +export const MULTICALL3_ADDRESS = Address.fromString("{{multicall3Address}}") \ No newline at end of file diff --git a/src/utils/multicall.ts b/src/utils/multicall.ts new file mode 100644 index 0000000..5fb74a9 --- /dev/null +++ b/src/utils/multicall.ts @@ -0,0 +1,41 @@ +import { Bytes, ethereum, log, crypto, ByteArray, Address } from "@graphprotocol/graph-ts" +import { Multicall3 as Multicall3Contract } from "../../generated/templates/BeefyCLStrategy/Multicall3" +import { MULTICALL3_ADDRESS } from "../config" + +export class MulticallParams { + constructor(public contractAddress: Bytes, public functionSignature: string, public resultType: string) {} +} + +export function multicallRead(callParams: Array): Array { + const multicallContract = Multicall3Contract.bind(MULTICALL3_ADDRESS) + + let params: Array = [] + for (let i = 0; i < callParams.length; i++) { + const callParam = callParams[i] + const sig = Bytes.fromUint8Array(crypto.keccak256(ByteArray.fromUTF8(callParam.functionSignature)).slice(0, 4)) + params.push( + changetype([ + ethereum.Value.fromAddress(Address.fromBytes(callParam.contractAddress)), + ethereum.Value.fromBytes(sig), + ]), + ) + } + + // need a low level call, can't call aggregate due to typing issues + const callResult = multicallContract.tryCall("aggregate", "aggregate((address,bytes)[]):(uint256,bytes[])", [ + ethereum.Value.fromTupleArray(params), + ]) + if (callResult.reverted) { + log.error("Multicall failed", []) + throw Error("Multicall failed") + } + + const multiResults = callResult.value[1].toBytesArray() + let results: Array = [] + for (let i = 0; i < callParams.length; i++) { + const callParam = callParams[i] + results.push(ethereum.decode(callParam.resultType, multiResults[i])!) + } + + return results +} diff --git a/src/utils/price.ts b/src/utils/price.ts index 87c7985..854a0ae 100644 --- a/src/utils/price.ts +++ b/src/utils/price.ts @@ -4,6 +4,7 @@ import { BeefyStrategy as BeefyCLStrategyContract } from "../../generated/templa import { ZERO_BD, tokenAmountToDecimal } from "./decimal" import { ChainLinkPriceFeed } from "../../generated/templates/BeefyCLStrategy/ChainLinkPriceFeed" import { CHAINLINK_NATIVE_PRICE_FEED_ADDRESS, PRICE_FEED_DECIMALS, WNATIVE_DECIMALS } from "../config" +import { MulticallParams, multicallRead } from "./multicall" import { isNullToken } from "../entity/token" const nativePriceFeed = ChainLinkPriceFeed.bind(CHAINLINK_NATIVE_PRICE_FEED_ADDRESS) @@ -11,61 +12,26 @@ const nativePriceFeed = ChainLinkPriceFeed.bind(CHAINLINK_NATIVE_PRICE_FEED_ADDR export function fetchVaultPrices( vault: BeefyCLVault, strategy: BeefyCLStrategy, - token0: Token, + token0: Token, token1: Token, earnedToken: Token, ): VaultPrices { - log.debug("fetchVaultPrices: fetching data for vault {}", [vault.id.toHexString()]) - const strategyContract = BeefyCLStrategyContract.bind(Address.fromBytes(strategy.id)) - - const token0PriceInNativeRes = strategyContract.try_lpToken0ToNativePrice() - if (token0PriceInNativeRes.reverted) { - log.error("fetchVaultPrices: lpToken0ToNativePrice() of vault {} and strat {} reverted for token {} (token0)", [ - vault.id.toHexString(), - strategy.id.toHexString(), - token0.id.toHexString(), - ]) - throw Error("fetchVaultPrices: lpToken0ToNativePrice() reverted") - } - const token0PriceInNative = tokenAmountToDecimal(token0PriceInNativeRes.value, WNATIVE_DECIMALS) - log.debug("fetchVaultPrices: token0PriceInNative: {}", [token0PriceInNative.toString()]) - - const token1PriceInNativeRes = strategyContract.try_lpToken1ToNativePrice() - if (token1PriceInNativeRes.reverted) { - log.error("fetchVaultPrices: lpToken1ToNativePrice() of vault {} and strat {} reverted for token {} (token1)", [ - vault.id.toHexString(), - strategy.id.toHexString(), - token1.id.toHexString(), - ]) - throw Error("fetchVaultPrices: lpToken1ToNativePrice() reverted") + const signatures = [ + new MulticallParams(strategy.id, 'lpToken0ToNativePrice()', "uint256"), + new MulticallParams(strategy.id, 'lpToken1ToNativePrice()', "uint256"), + new MulticallParams(CHAINLINK_NATIVE_PRICE_FEED_ADDRESS,'latestRoundData()', "uint256"), + ] + if (!isNullToken(earnedToken)) { + signatures.push(new MulticallParams(strategy.id, 'ouptutToNativePrice()', "uint256"),) } - const token1PriceInNative = tokenAmountToDecimal(token1PriceInNativeRes.value, WNATIVE_DECIMALS) - log.debug("fetchVaultPrices: token1PriceInNative: {}", [token1PriceInNative.toString()]) - - // some vaults have an additional token that is not part of the LP + const results = multicallRead(signatures) + const token0PriceInNative = tokenAmountToDecimal(results[0].toBigInt(), WNATIVE_DECIMALS) + const token1PriceInNative = tokenAmountToDecimal(results[1].toBigInt(), WNATIVE_DECIMALS) + const nativePriceUSD = tokenAmountToDecimal(results[2].toBigInt(), PRICE_FEED_DECIMALS) let earnedTokenPriceInNative = ZERO_BD if (!isNullToken(earnedToken)) { - const earnedTokenPriceInNativeRes = strategyContract.try_ouptutToNativePrice() - if (earnedTokenPriceInNativeRes.reverted) { - log.error("fetchVaultPrices: ouptutToNativePrice() of vault {} and strat {} reverted for token {} (earned)", [ - vault.id.toHexString(), - strategy.id.toHexString(), - earnedToken.id.toHexString(), - ]) - throw Error("fetchVaultPrices: ouptutToNativePrice() reverted") - } - earnedTokenPriceInNative = tokenAmountToDecimal(earnedTokenPriceInNativeRes.value, WNATIVE_DECIMALS) - log.debug("fetchVaultPrices: earnedTokenPriceInNative: {}", [earnedTokenPriceInNative.toString()]) - } - - // fetch the native price in USD - const nativePriceUSDRes = nativePriceFeed.try_latestRoundData() - if (nativePriceUSDRes.reverted) { - log.error("fetchVaultPrices: latestRoundData() reverted for native token", []) - throw Error("fetchVaultPrices: latestRoundData() reverted") + earnedTokenPriceInNative = tokenAmountToDecimal(results[3].toBigInt(), WNATIVE_DECIMALS) } - const nativePriceUSD = tokenAmountToDecimal(nativePriceUSDRes.value.getAnswer(), PRICE_FEED_DECIMALS) - log.debug("fetchVaultPrices: nativePriceUSD: {}", [nativePriceUSD.toString()]) return new VaultPrices(token0PriceInNative, token1PriceInNative, earnedTokenPriceInNative, nativePriceUSD) } diff --git a/subgraph.template.yaml b/subgraph.template.yaml index ed7ed76..79ed1b0 100644 --- a/subgraph.template.yaml +++ b/subgraph.template.yaml @@ -90,6 +90,8 @@ dataSources: file: ./abis/chainlink/AggregatorV3Interface.json - name: IERC20 file: ./abis/IERC20/IERC20.json + - name: Multicall3 + file: ./abis/multicall/Multicall3.json #eventHandlers: # - event: NewRound(indexed uint256,indexed address,uint256) # handler: handleClockTick @@ -126,6 +128,8 @@ dataSources: file: ./abis/chainlink/AggregatorV3Interface.json - name: IERC20 file: ./abis/IERC20/IERC20.json + - name: Multicall3 + file: ./abis/multicall/Multicall3.json eventHandlers: - event: BoostDeployed(indexed address) handler: handleBoostCreated @@ -160,6 +164,8 @@ templates: file: ./abis/chainlink/AggregatorV3Interface.json - name: IERC20 file: ./abis/IERC20/IERC20.json + - name: Multicall3 + file: ./abis/multicall/Multicall3.json eventHandlers: - event: Initialized(uint8) handler: handleInitialized @@ -202,6 +208,8 @@ templates: file: ./abis/chainlink/AggregatorV3Interface.json - name: IERC20 file: ./abis/IERC20/IERC20.json + - name: Multicall3 + file: ./abis/multicall/Multicall3.json eventHandlers: - event: Initialized(uint8) handler: handleInitialized @@ -252,6 +260,8 @@ templates: file: ./abis/chainlink/AggregatorV3Interface.json - name: IERC20 file: ./abis/IERC20/IERC20.json + - name: Multicall3 + file: ./abis/multicall/Multicall3.json eventHandlers: - event: OwnershipTransferred(indexed address,indexed address) handler: handleOwnershipTransferred