Skip to content

Commit 4a08bb3

Browse files
authored
Merge pull request #1903 from AmbireTech/fix/bad-pending-state
Fix: add checks for bad pending state
2 parents e4dd28e + 36eb2fb commit 4a08bb3

5 files changed

Lines changed: 112 additions & 10 deletions

File tree

src/controllers/accounts/accounts.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,53 @@ export class AccountsController extends EventEmitter implements IAccountsControl
179179
networkAccountStates.forEach((accountState) => {
180180
const addr = accountState.accountAddr
181181
if (!this.accountStates[addr]) this.accountStates[addr] = {}
182+
183+
// if the block tag is pending & we have a fetched latest state,
184+
// we check each nonce against the latest state.
185+
// if the pending nonce is lower than the latest state, we override the res
186+
// also, we check isDeployed, isSmarterEoa & isEOA
187+
// we do this because rogue RPCs sometimes mess up the pending state
188+
if (blockTag === 'pending') {
189+
const current = this.accountStates[addr][network.chainId.toString()]
190+
if (current) {
191+
const account = accounts.find((acc) => acc.addr === addr)
192+
if (account) {
193+
if (accountState.erc4337Nonce < current.erc4337Nonce) {
194+
// eslint-disable-next-line no-param-reassign
195+
accountState.erc4337Nonce = current.erc4337Nonce
196+
}
197+
if (accountState.nonce < current.nonce) {
198+
// eslint-disable-next-line no-param-reassign
199+
accountState.nonce = current.nonce
200+
}
201+
if (
202+
accountState.eoaNonce !== null &&
203+
current.eoaNonce !== null &&
204+
accountState.eoaNonce < current.eoaNonce
205+
) {
206+
// eslint-disable-next-line no-param-reassign
207+
accountState.eoaNonce = current.eoaNonce
208+
}
209+
if (!accountState.isDeployed && current.isDeployed) {
210+
// eslint-disable-next-line no-param-reassign
211+
accountState.isDeployed = current.isDeployed
212+
}
213+
if (!accountState.isErc4337Enabled && current.isErc4337Enabled) {
214+
// eslint-disable-next-line no-param-reassign
215+
accountState.isErc4337Enabled = current.isErc4337Enabled
216+
}
217+
if (!accountState.isSmarterEoa && current.isSmarterEoa) {
218+
// eslint-disable-next-line no-param-reassign
219+
accountState.isSmarterEoa = current.isSmarterEoa
220+
}
221+
if (!accountState.isEOA && current.isEOA) {
222+
// eslint-disable-next-line no-param-reassign
223+
accountState.isEOA = current.isEOA
224+
}
225+
}
226+
}
227+
}
228+
182229
this.accountStates[addr][network.chainId.toString()] = accountState
183230
})
184231
} catch (err: any) {

src/libs/estimate/estimateBundler.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22
/* eslint-disable no-continue */
33
/* eslint-disable no-constant-condition */
44

5-
import { Contract, Interface, toBeHex } from 'ethers'
5+
import { Interface, toBeHex } from 'ethers'
66

77
import AmbireAccount from '../../../contracts/compiled/AmbireAccount.json'
8-
import entryPointAbi from '../../../contracts/compiled/EntryPoint.json'
98
import { EIP7702Auth } from '../../consts/7702'
10-
import { ERC_4337_ENTRYPOINT } from '../../consts/deploy'
119
import { AccountOnchainState } from '../../interfaces/account'
1210
import { Network } from '../../interfaces/network'
1311
import { RPCProvider } from '../../interfaces/provider'
@@ -20,8 +18,10 @@ import { AccountOp, getSignableCalls } from '../accountOp/accountOp'
2018
import { SubmittedAccountOp } from '../accountOp/submittedAccountOp'
2119
import { AccountOpStatus } from '../accountOp/types'
2220
import { PaymasterEstimationData } from '../erc7677/types'
21+
import { DecodedError } from '../errorDecoder/types'
2322
import { getHumanReadableEstimationError } from '../errorHumanizer'
2423
import { TokenResult } from '../portfolio'
24+
import { fetchNonce } from '../userOperation/fetchEntryPointNonce'
2525
import { UserOperation } from '../userOperation/types'
2626
import { getSigForCalculations, getUserOperation } from '../userOperation/userOperation'
2727
import { estimateWithRetries } from './estimateWithRetries'
@@ -119,7 +119,12 @@ async function estimate(
119119

120120
const nonFatalErrors: Error[] = []
121121
const estimateErrorCallback = (e: Error) => {
122-
const decodedError = bundler.decodeBundlerError(e)
122+
let decodedError: Error | DecodedError = e
123+
try {
124+
decodedError = bundler.decodeBundlerError(e)
125+
} catch (err) {
126+
// failed to decode the error, move on with the original one
127+
}
123128

124129
// if the bundler estimation fails, add a nonFatalError so we can react to
125130
// it on the FE. The BE at a later stage decides if this error is actually fatal
@@ -202,6 +207,7 @@ export async function bundlerEstimate(
202207
}
203208

204209
const flags: EstimationFlags = {}
210+
flags.invalid4337NonceCounter = 0
205211
while (true) {
206212
// estimate
207213
const estimations = await estimate(
@@ -235,14 +241,20 @@ export async function bundlerEstimate(
235241
estimations.nonFatalErrors.find((err) => err.cause === '4337_INVALID_NONCE')
236242
) {
237243
flags.has4337NonceDiscrepancy = true
244+
flags.invalid4337NonceCounter += 1
245+
246+
// prevent infinite loops in the event of a terrible rpc malfunction
247+
if (flags.invalid4337NonceCounter >= 3) {
248+
return estimations.nonFatalErrors.find((err) => err.cause === '4337_INVALID_NONCE')!
249+
}
238250

239251
// wait a bit to allow the state to sync
240252
await wait(2000)
241-
const ep = new Contract(ERC_4337_ENTRYPOINT, entryPointAbi, provider)
242-
const accountNonce = await ep
243-
.getNonce(account.addr, 0, { blockTag: 'pending' })
244-
.catch(() => null)
245-
if (!accountNonce) continue
253+
const accountNonce = await fetchNonce(account, provider)
254+
if (accountNonce === null || accountNonce === 0n) {
255+
// RPC serious malfunction
256+
return estimations.nonFatalErrors.find((err) => err.cause === '4337_INVALID_NONCE')!
257+
}
246258

247259
if (network.chainId === 1n && BigInt(userOp.nonce) === BigInt(accountNonce)) {
248260
return estimations.nonFatalErrors.find((err) => err.cause === '4337_INVALID_NONCE')!

src/libs/estimate/interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface EstimationFlags {
2424
hasNonceDiscrepancy?: boolean
2525
has4337NonceDiscrepancy?: boolean
2626
hasInitialGasLimitFailed?: boolean
27+
invalid4337NonceCounter?: number
2728
}
2829

2930
export interface Erc4337GasLimits {

src/libs/relayerCall/relayerCall.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe('Relayer', () => {
3939
expect(true).toBe(false)
4040
} catch (e: any) {
4141
expect(e.message).toBe(
42-
'Broadcast failed: INVALID_SIGNATURE. Please try again or contact support'
42+
'Signature validation was not successful. Please try again or contact support'
4343
)
4444
}
4545
})
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Interface } from 'ethers'
2+
3+
import entryPointAbi from '../../../contracts/compiled/EntryPoint.json'
4+
import { ERC_4337_ENTRYPOINT } from '../../consts/deploy'
5+
import { Account } from '../../interfaces/account'
6+
import { RPCProvider } from '../../interfaces/provider'
7+
8+
export async function fetchNonce(account: Account, provider: RPCProvider): Promise<bigint | null> {
9+
const epInterface = new Interface(entryPointAbi)
10+
const failure = () => {
11+
// eslint-disable-next-line no-console
12+
console.error('unable to fetch the entry point nonce, estimateBundler')
13+
return null
14+
}
15+
const [accountNonceHexLatest, accountNonceHexPending] = await Promise.all([
16+
provider
17+
.call({
18+
to: ERC_4337_ENTRYPOINT,
19+
data: epInterface.encodeFunctionData('getNonce', [account.addr, 0]),
20+
blockTag: 'latest'
21+
})
22+
.catch(failure),
23+
provider
24+
.call({
25+
to: ERC_4337_ENTRYPOINT,
26+
data: epInterface.encodeFunctionData('getNonce', [account.addr, 0]),
27+
blockTag: 'pending'
28+
})
29+
.catch(failure)
30+
])
31+
32+
// if there's an RPC problem and we can't fetch the nonce, we return an error
33+
if (accountNonceHexLatest === null && accountNonceHexPending === null) return null
34+
if (accountNonceHexLatest === null) return BigInt(accountNonceHexPending as string) // shouldn't happen
35+
if (accountNonceHexPending === null) return BigInt(accountNonceHexLatest as string)
36+
37+
const accountNonceLatest = BigInt(accountNonceHexLatest)
38+
const accountNoncePending = BigInt(accountNonceHexPending)
39+
40+
// always trust latest except the time when pending is higher
41+
return accountNoncePending > accountNonceLatest ? accountNoncePending : accountNonceLatest
42+
}

0 commit comments

Comments
 (0)