Commit 5e8d725e authored by Moody Salem's avatar Moody Salem Committed by GitHub

fix: split calls into more chunks if they fail due to out of gas errors (#2630)

* fix: split calls into more chunks if they fail due to out of gas errors

* set to 100m gas

* back to 25m so we batch fewer calls

* do not pass through gas limit, some simplification of the code

* unused import
parent c63482b6
......@@ -13,7 +13,8 @@ import { useActiveWeb3React } from './web3'
const QUOTE_GAS_OVERRIDES: { [chainId: number]: number } = {
[SupportedChainId.OPTIMISM]: 6_000_000,
[SupportedChainId.OPTIMISTIC_KOVAN]: 6_000_000,
[SupportedChainId.ARBITRUM_ONE]: 26_000_000,
[SupportedChainId.ARBITRUM_ONE]: 25_000_000,
[SupportedChainId.ARBITRUM_RINKEBY]: 25_000_000,
}
const DEFAULT_GAS_QUOTE = 2_000_000
......
......@@ -10,9 +10,9 @@ import { retry, RetryableError } from '../../utils/retry'
import { useBlockNumber } from '../application/hooks'
import { AppState } from '../index'
import { errorFetchingMulticallResults, fetchingMulticallResults, updateMulticallResults } from './actions'
import { Call, parseCallKey } from './utils'
import { Call, parseCallKey, toCallKey } from './utils'
const DEFAULT_GAS_REQUIRED = 1_000_000
const DEFAULT_CALL_GAS_REQUIRED = 1_000_000
/**
* Fetches a chunk of calls, enforcing a minimum block number constraint
......@@ -31,9 +31,12 @@ async function fetchChunk(
chunk.map((obj) => ({
target: obj.address,
callData: obj.callData,
gasLimit: obj.gasRequired ?? DEFAULT_GAS_REQUIRED,
gasLimit: obj.gasRequired ?? DEFAULT_CALL_GAS_REQUIRED,
})),
{ blockTag: blockNumber }
{
// we aren't passing through the block gas limit we used to create the chunk, because it causes a problem with the integ tests
blockTag: blockNumber,
}
)
if (process.env.NODE_ENV === 'development') {
......@@ -41,11 +44,11 @@ async function fetchChunk(
if (
!success &&
returnData.length === 2 &&
gasUsed.gte(Math.floor((chunk[i].gasRequired ?? DEFAULT_GAS_REQUIRED) * 0.95))
gasUsed.gte(Math.floor((chunk[i].gasRequired ?? DEFAULT_CALL_GAS_REQUIRED) * 0.95))
) {
console.warn(
`A call failed due to requiring ${gasUsed.toString()} vs. allowed ${
chunk[i].gasRequired ?? DEFAULT_GAS_REQUIRED
chunk[i].gasRequired ?? DEFAULT_CALL_GAS_REQUIRED
}`,
chunk[i]
)
......@@ -57,6 +60,18 @@ async function fetchChunk(
} catch (error) {
if (error.code === -32000 || error.message?.indexOf('header not found') !== -1) {
throw new RetryableError(`header not found for block number ${blockNumber}`)
} else if (error.code === -32603 || error.message?.indexOf('execution ran out of gas') !== -1) {
if (chunk.length > 1) {
if (process.env.NODE_ENV === 'development') {
console.debug('Splitting a chunk in 2', chunk)
}
const half = Math.floor(chunk.length / 2)
const [c0, c1] = await Promise.all([
fetchChunk(multicall, chunk.slice(0, half), blockNumber),
fetchChunk(multicall, chunk.slice(half, chunk.length), blockNumber),
])
return c0.concat(c1)
}
}
console.error('Failed to fetch chunk', error)
throw error
......@@ -151,6 +166,9 @@ export default function Updater(): null {
[unserializedOutdatedCallKeys]
)
// todo: consider getting this information from the node we are using, e.g. block.gaslimit
const chunkGasLimit = 100_000_000
useEffect(() => {
if (!latestBlockNumber || !chainId || !multicall2Contract) return
......@@ -158,7 +176,7 @@ export default function Updater(): null {
if (outdatedCallKeys.length === 0) return
const calls = outdatedCallKeys.map((key) => parseCallKey(key))
const chunkedCalls = chunkArray(calls)
const chunkedCalls = chunkArray(calls, chunkGasLimit)
if (cancellations.current && cancellations.current.blockNumber !== latestBlockNumber) {
cancellations.current.cancellations.forEach((c) => c())
......@@ -174,7 +192,7 @@ export default function Updater(): null {
cancellations.current = {
blockNumber: latestBlockNumber,
cancellations: chunkedCalls.map((chunk, index) => {
cancellations: chunkedCalls.map((chunk) => {
const { cancel, promise } = retry(() => fetchChunk(multicall2Contract, chunk, latestBlockNumber), {
n: Infinity,
minWait: 1000,
......@@ -182,22 +200,16 @@ export default function Updater(): null {
})
promise
.then((returnData) => {
// accumulates the length of all previous indices
const firstCallKeyIndex = chunkedCalls.slice(0, index).reduce<number>((memo, curr) => memo + curr.length, 0)
const lastCallKeyIndex = firstCallKeyIndex + returnData.length
const slice = outdatedCallKeys.slice(firstCallKeyIndex, lastCallKeyIndex)
// split the returned slice into errors and success
const { erroredCalls, results } = slice.reduce<{
// split the returned slice into errors and results
const { erroredCalls, results } = chunk.reduce<{
erroredCalls: Call[]
results: { [callKey: string]: string | null }
}>(
(memo, callKey, i) => {
(memo, call, i) => {
if (returnData[i].success) {
memo.results[callKey] = returnData[i].returnData ?? null
memo.results[toCallKey(call)] = returnData[i].returnData ?? null
} else {
memo.erroredCalls.push(parseCallKey(callKey))
memo.erroredCalls.push(call)
}
return memo
},
......@@ -216,7 +228,15 @@ export default function Updater(): null {
// dispatch any errored calls
if (erroredCalls.length > 0) {
console.debug('Calls errored in fetch', erroredCalls)
if (process.env.NODE_ENV === 'development') {
returnData.forEach((returnData, ix) => {
if (!returnData.success) {
console.debug('Call failed', chunk[ix], returnData)
}
})
} else {
console.debug('Calls errored in fetch', erroredCalls)
}
dispatch(
errorFetchingMulticallResults({
calls: erroredCalls,
......
const CONSERVATIVE_BLOCK_GAS_LIMIT = 10_000_000 // conservative, hard-coded estimate of the current block gas limit
export const DEFAULT_GAS_REQUIRED = 200_000 // the default value for calls that don't specify gasRequired
// chunks array into chunks
// evenly distributes items among the chunks
export default function chunkArray<T>(items: T[], gasLimit = CONSERVATIVE_BLOCK_GAS_LIMIT * 10): T[][] {
export default function chunkArray<T>(items: T[], chunkGasLimit: number): T[][] {
const chunks: T[][] = []
let currentChunk: T[] = []
let currentChunkCumulativeGas = 0
......@@ -16,7 +15,7 @@ export default function chunkArray<T>(items: T[], gasLimit = CONSERVATIVE_BLOCK_
// if the current chunk is empty, or the current item wouldn't push it over the gas limit,
// append the current item and increment the cumulative gas
if (currentChunk.length === 0 || currentChunkCumulativeGas + gasRequired < gasLimit) {
if (currentChunk.length === 0 || currentChunkCumulativeGas + gasRequired < chunkGasLimit) {
currentChunk.push(item)
currentChunkCumulativeGas += gasRequired
} else {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment