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' ...@@ -13,7 +13,8 @@ import { useActiveWeb3React } from './web3'
const QUOTE_GAS_OVERRIDES: { [chainId: number]: number } = { const QUOTE_GAS_OVERRIDES: { [chainId: number]: number } = {
[SupportedChainId.OPTIMISM]: 6_000_000, [SupportedChainId.OPTIMISM]: 6_000_000,
[SupportedChainId.OPTIMISTIC_KOVAN]: 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 const DEFAULT_GAS_QUOTE = 2_000_000
......
...@@ -10,9 +10,9 @@ import { retry, RetryableError } from '../../utils/retry' ...@@ -10,9 +10,9 @@ import { retry, RetryableError } from '../../utils/retry'
import { useBlockNumber } from '../application/hooks' import { useBlockNumber } from '../application/hooks'
import { AppState } from '../index' import { AppState } from '../index'
import { errorFetchingMulticallResults, fetchingMulticallResults, updateMulticallResults } from './actions' 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 * Fetches a chunk of calls, enforcing a minimum block number constraint
...@@ -31,9 +31,12 @@ async function fetchChunk( ...@@ -31,9 +31,12 @@ async function fetchChunk(
chunk.map((obj) => ({ chunk.map((obj) => ({
target: obj.address, target: obj.address,
callData: obj.callData, 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') { if (process.env.NODE_ENV === 'development') {
...@@ -41,11 +44,11 @@ async function fetchChunk( ...@@ -41,11 +44,11 @@ async function fetchChunk(
if ( if (
!success && !success &&
returnData.length === 2 && 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( console.warn(
`A call failed due to requiring ${gasUsed.toString()} vs. allowed ${ `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] chunk[i]
) )
...@@ -57,6 +60,18 @@ async function fetchChunk( ...@@ -57,6 +60,18 @@ async function fetchChunk(
} catch (error) { } catch (error) {
if (error.code === -32000 || error.message?.indexOf('header not found') !== -1) { if (error.code === -32000 || error.message?.indexOf('header not found') !== -1) {
throw new RetryableError(`header not found for block number ${blockNumber}`) 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) console.error('Failed to fetch chunk', error)
throw error throw error
...@@ -151,6 +166,9 @@ export default function Updater(): null { ...@@ -151,6 +166,9 @@ export default function Updater(): null {
[unserializedOutdatedCallKeys] [unserializedOutdatedCallKeys]
) )
// todo: consider getting this information from the node we are using, e.g. block.gaslimit
const chunkGasLimit = 100_000_000
useEffect(() => { useEffect(() => {
if (!latestBlockNumber || !chainId || !multicall2Contract) return if (!latestBlockNumber || !chainId || !multicall2Contract) return
...@@ -158,7 +176,7 @@ export default function Updater(): null { ...@@ -158,7 +176,7 @@ export default function Updater(): null {
if (outdatedCallKeys.length === 0) return if (outdatedCallKeys.length === 0) return
const calls = outdatedCallKeys.map((key) => parseCallKey(key)) const calls = outdatedCallKeys.map((key) => parseCallKey(key))
const chunkedCalls = chunkArray(calls) const chunkedCalls = chunkArray(calls, chunkGasLimit)
if (cancellations.current && cancellations.current.blockNumber !== latestBlockNumber) { if (cancellations.current && cancellations.current.blockNumber !== latestBlockNumber) {
cancellations.current.cancellations.forEach((c) => c()) cancellations.current.cancellations.forEach((c) => c())
...@@ -174,7 +192,7 @@ export default function Updater(): null { ...@@ -174,7 +192,7 @@ export default function Updater(): null {
cancellations.current = { cancellations.current = {
blockNumber: latestBlockNumber, blockNumber: latestBlockNumber,
cancellations: chunkedCalls.map((chunk, index) => { cancellations: chunkedCalls.map((chunk) => {
const { cancel, promise } = retry(() => fetchChunk(multicall2Contract, chunk, latestBlockNumber), { const { cancel, promise } = retry(() => fetchChunk(multicall2Contract, chunk, latestBlockNumber), {
n: Infinity, n: Infinity,
minWait: 1000, minWait: 1000,
...@@ -182,22 +200,16 @@ export default function Updater(): null { ...@@ -182,22 +200,16 @@ export default function Updater(): null {
}) })
promise promise
.then((returnData) => { .then((returnData) => {
// accumulates the length of all previous indices // split the returned slice into errors and results
const firstCallKeyIndex = chunkedCalls.slice(0, index).reduce<number>((memo, curr) => memo + curr.length, 0) const { erroredCalls, results } = chunk.reduce<{
const lastCallKeyIndex = firstCallKeyIndex + returnData.length
const slice = outdatedCallKeys.slice(firstCallKeyIndex, lastCallKeyIndex)
// split the returned slice into errors and success
const { erroredCalls, results } = slice.reduce<{
erroredCalls: Call[] erroredCalls: Call[]
results: { [callKey: string]: string | null } results: { [callKey: string]: string | null }
}>( }>(
(memo, callKey, i) => { (memo, call, i) => {
if (returnData[i].success) { if (returnData[i].success) {
memo.results[callKey] = returnData[i].returnData ?? null memo.results[toCallKey(call)] = returnData[i].returnData ?? null
} else { } else {
memo.erroredCalls.push(parseCallKey(callKey)) memo.erroredCalls.push(call)
} }
return memo return memo
}, },
...@@ -216,7 +228,15 @@ export default function Updater(): null { ...@@ -216,7 +228,15 @@ export default function Updater(): null {
// dispatch any errored calls // dispatch any errored calls
if (erroredCalls.length > 0) { if (erroredCalls.length > 0) {
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) console.debug('Calls errored in fetch', erroredCalls)
}
dispatch( dispatch(
errorFetchingMulticallResults({ errorFetchingMulticallResults({
calls: erroredCalls, 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 export const DEFAULT_GAS_REQUIRED = 200_000 // the default value for calls that don't specify gasRequired
// chunks array into chunks // chunks array into chunks
// evenly distributes items among the 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[][] = [] const chunks: T[][] = []
let currentChunk: T[] = [] let currentChunk: T[] = []
let currentChunkCumulativeGas = 0 let currentChunkCumulativeGas = 0
...@@ -16,7 +15,7 @@ export default function chunkArray<T>(items: T[], gasLimit = CONSERVATIVE_BLOCK_ ...@@ -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, // 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 // 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) currentChunk.push(item)
currentChunkCumulativeGas += gasRequired currentChunkCumulativeGas += gasRequired
} else { } 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