Skip to content

Commit

Permalink
fix: split calls into more chunks if they fail due to out of gas erro…
Browse files Browse the repository at this point in the history
…rs (#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
  • Loading branch information
moodysalem authored Oct 19, 2021
1 parent c63482b commit 5e8d725
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 24 deletions.
3 changes: 2 additions & 1 deletion src/hooks/useClientSideV3Trade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 40 additions & 20 deletions src/state/multicall/updater.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,21 +31,24 @@ 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') {
returnData.forEach(({ gasUsed, returnData, success }, i) => {
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]
)
Expand All @@ -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
Expand Down Expand Up @@ -151,14 +166,17 @@ 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

const outdatedCallKeys: string[] = JSON.parse(serializedOutdatedCallKeys)
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())
Expand All @@ -174,30 +192,24 @@ 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,
maxWait: 2500,
})
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
},
Expand All @@ -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,
Expand Down
5 changes: 2 additions & 3 deletions src/utils/chunkArray.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
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
Expand All @@ -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 {
Expand Down

1 comment on commit 5e8d725

@vercel
Copy link

@vercel vercel bot commented on 5e8d725 Oct 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

Please sign in to comment.