Skip to content

Commit 106bfc7

Browse files
authored
Merge pull request #980 from ensdomains/fix/safe-gas-estimates
fix: safe (or other sca) gas estimates
2 parents 413bc43 + 2a43dd8 commit 106bfc7

File tree

10 files changed

+157
-137
lines changed

10 files changed

+157
-137
lines changed

src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.test.tsx

Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { useAddRecentTransaction } from '@app/hooks/transactions/useAddRecentTra
1414
import { useRecentTransactions } from '@app/hooks/transactions/useRecentTransactions'
1515
import { useIsSafeApp } from '@app/hooks/useIsSafeApp'
1616
import { GenericTransaction } from '@app/transaction-flow/types'
17+
import { createAccessList } from '@app/utils/query/createAccessList'
1718
import { checkIsSafeApp } from '@app/utils/safe'
1819

1920
import { makeMockIntersectionObserver } from '../../../../../test/mock/makeMockIntersectionObserver'
@@ -28,6 +29,7 @@ vi.mock('@app/hooks/transactions/useAddRecentTransaction')
2829
vi.mock('@app/hooks/transactions/useRecentTransactions')
2930
vi.mock('@app/hooks/chain/useInvalidateOnBlock')
3031
vi.mock('@app/utils/safe')
32+
vi.mock('@app/utils/query/createAccessList')
3133
vi.mock('@wagmi/core', async () => {
3234
const actual = await vi.importActual('@wagmi/core')
3335
return {
@@ -79,6 +81,7 @@ const mockUseClient = mockFunction(useClient)
7981
const mockUseConnectorClient = mockFunction(useConnectorClient)
8082

8183
const mockEstimateGas = mockFunction(estimateGas)
84+
const mockCreateAccessList = createAccessList as MockedFunctionDeep<typeof createAccessList>
8285
const mockPrepareTransactionRequest = prepareTransactionRequest as MockedFunctionDeep<
8386
typeof prepareTransactionRequest
8487
>
@@ -208,6 +211,7 @@ describe('TransactionStageModal', () => {
208211
})
209212

210213
it('should disable confirm button and re-estimate gas if a unique identifier is changed', async () => {
214+
mockCreateAccessList.mockResolvedValue({ accessList: [], gasUsed: '0x64' })
211215
mockEstimateGas.mockResolvedValue(1n)
212216
mockUseIsSafeApp.mockReturnValue({ data: false })
213217
mockUseSendTransaction.mockReturnValue({
@@ -236,6 +240,7 @@ describe('TransactionStageModal', () => {
236240
})
237241

238242
it('should only show confirm button as enabled if gas is estimated and sendTransaction func is defined', async () => {
243+
mockCreateAccessList.mockResolvedValue({ accessList: [], gasUsed: '0x64' })
239244
mockEstimateGas.mockResolvedValue(1n)
240245
mockUseSendTransaction.mockReturnValue({
241246
sendTransaction: () => Promise.resolve(),
@@ -246,6 +251,7 @@ describe('TransactionStageModal', () => {
246251
)
247252
})
248253
it('should run set sendTransaction on action click', async () => {
254+
mockCreateAccessList.mockResolvedValue({ accessList: [], gasUsed: '0x64' })
249255
mockEstimateGas.mockResolvedValue(1n)
250256
const mockSendTransaction = vi.fn()
251257
mockUseSendTransaction.mockReturnValue({
@@ -286,6 +292,7 @@ describe('TransactionStageModal', () => {
286292
})
287293
it('should pass the request to send transaction', async () => {
288294
mockUseIsSafeApp.mockReturnValue({ data: false })
295+
mockCreateAccessList.mockResolvedValue({ accessList: [], gasUsed: '0x64' })
289296
mockEstimateGas.mockResolvedValue(1n)
290297
const mockSendTransaction = vi.fn()
291298
mockUseSendTransaction.mockReturnValue({
@@ -298,7 +305,7 @@ describe('TransactionStageModal', () => {
298305
expect.objectContaining({
299306
...mockTransactionRequest,
300307
gas: 1n,
301-
accessList: undefined,
308+
accessList: [],
302309
}),
303310
),
304311
)
@@ -500,59 +507,37 @@ describe('calculateGasLimit', () => {
500507
data: '0x12345678',
501508
} as const
502509
const mockTransactionName = 'registerName'
503-
const mockIsSafeApp = false
510+
const mockAccessListResponse = {
511+
accessList: [
512+
{
513+
address: '0x1234567890123456789012345678901234567890' as const,
514+
storageKeys: [
515+
'0x1234567890123456789012345678901234567890123456789012345678901234' as const,
516+
],
517+
},
518+
],
519+
gasUsed: '0x64' as const,
520+
}
504521

505522
beforeEach(() => {
506523
vi.clearAllMocks()
507524
})
508525

509-
it('should calculate gas limit for non-safe apps', async () => {
526+
it('should calculate gas limit', async () => {
510527
mockEstimateGas.mockResolvedValueOnce(100000n)
528+
mockCreateAccessList.mockResolvedValueOnce(mockAccessListResponse)
511529
const result = await calculateGasLimit({
512-
isSafeApp: mockIsSafeApp,
513530
txWithZeroGas: mockTxWithZeroGas,
514531
transactionName: mockTransactionName,
515532
client: mockClient as any,
516533
connectorClient: mockConnectorClient as any,
517534
})
518535
expect(result.gasLimit).toEqual(105000n)
519-
expect(result.accessList).toBeUndefined()
536+
expect(result.accessList).toEqual(mockAccessListResponse.accessList)
520537
expect(mockEstimateGas).toHaveBeenCalledWith(mockClient, {
521538
...mockTxWithZeroGas,
539+
accessList: mockAccessListResponse.accessList,
522540
account: mockConnectorClient.account,
523541
})
524542
})
525-
526-
it('should calculate gas limit for safe apps', async () => {
527-
const mockAccessListResponse = {
528-
gasUsed: '0x64',
529-
accessList: [
530-
{
531-
address: '0x1234567890123456789012345678901234567890',
532-
storageKeys: ['0x1234567890123456789012345678901234567890123456789012345678901234'],
533-
},
534-
],
535-
}
536-
mockClient.request.mockResolvedValueOnce(mockAccessListResponse)
537-
const result = await calculateGasLimit({
538-
isSafeApp: true,
539-
txWithZeroGas: mockTxWithZeroGas,
540-
transactionName: mockTransactionName,
541-
client: mockClient as any,
542-
connectorClient: mockConnectorClient as any,
543-
})
544-
expect(result.gasLimit).toEqual(5100n)
545-
expect(result.accessList).toEqual(mockAccessListResponse.accessList)
546-
expect(mockClient.request).toHaveBeenCalledWith({
547-
method: 'eth_createAccessList',
548-
params: [
549-
{
550-
from: mockConnectorClient.account.address,
551-
...mockTxWithZeroGas,
552-
value: '0x0',
553-
},
554-
'latest',
555-
],
556-
})
557-
})
558543
})

src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -296,13 +296,24 @@ const getPreTransactionError = ({
296296
return match({ stage, err: transactionError || requestError })
297297
.with({ stage: P.union('complete', 'sent') }, () => null)
298298
.with({ err: P.nullish }, () => null)
299-
.with({ err: P.not(P.instanceOf(BaseError)) }, ({ err }) => ({
300-
message: 'message' in err! ? err.message : 'transaction.error.unknown',
301-
type: 'unknown' as const,
302-
}))
299+
.with({ err: P.not(P.instanceOf(BaseError)) }, ({ err }) => {
300+
return {
301+
message: 'message' in err! ? err.message : 'transaction.error.unknown',
302+
type: 'unknown' as const,
303+
}
304+
})
303305
.otherwise(({ err }) => {
304306
const readableError = getReadableError(err)
305-
return readableError || { message: (err as BaseError).shortMessage, type: 'unknown' as const }
307+
const error = readableError || {
308+
message: (err as BaseError).shortMessage,
309+
type: 'unknown' as const,
310+
}
311+
// TODO: Remove this when the following issue is fixed: https://github.com/paradigmxyz/reth/issues/15762
312+
// Cause reth throws an error on eth_createAccessList when `sender is not an EOA`
313+
if (error.message === 'Transaction creation failed.') {
314+
return null
315+
}
316+
return error
306317
})
307318
}
308319

@@ -395,7 +406,7 @@ export const TransactionStageModal = ({
395406

396407
const preparedOptions = queryOptions({
397408
queryKey: initialOptions.queryKey,
398-
queryFn: initialOptions.queryFn({ connectorClient, isSafeApp, connections }),
409+
queryFn: initialOptions.queryFn({ connectorClient, connections }),
399410
})
400411

401412
const transactionRequestQuery = useQuery({

src/components/@molecules/TransactionDialogManager/stage/query.ts

Lines changed: 12 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
11
import { QueryFunctionContext } from '@tanstack/react-query'
22
import { CallParameters, getFeeHistory, SendTransactionReturnType } from '@wagmi/core'
33
import { Dispatch } from 'react'
4-
import {
5-
Address,
6-
BlockTag,
7-
Hash,
8-
Hex,
9-
PrepareTransactionRequestRequest,
10-
toHex,
11-
Transaction,
12-
TransactionRequest,
13-
} from 'viem'
4+
import { Hash, PrepareTransactionRequestRequest, toHex, Transaction } from 'viem'
145
import { call, estimateGas, getTransaction, prepareTransactionRequest } from 'viem/actions'
156
import { useConnections } from 'wagmi'
167

@@ -33,18 +24,10 @@ import {
3324
CreateQueryKey,
3425
} from '@app/types'
3526
import { getReadableError } from '@app/utils/errors'
27+
import { createAccessList } from '@app/utils/query/createAccessList'
3628
import { wagmiConfig } from '@app/utils/query/wagmi'
37-
import { CheckIsSafeAppReturnType } from '@app/utils/safe'
3829
import { hasParaConnection } from '@app/utils/utils'
3930

40-
type AccessListResponse = {
41-
accessList: {
42-
address: Address
43-
storageKeys: Hex[]
44-
}[]
45-
gasUsed: Hex
46-
}
47-
4831
export const getUniqueTransaction = ({
4932
txKey,
5033
currentStep,
@@ -120,47 +103,30 @@ export const registrationGasFeeModifier = (gasLimit: bigint, transactionName: Tr
120103
export const calculateGasLimit = async ({
121104
client,
122105
connectorClient,
123-
isSafeApp,
124106
txWithZeroGas,
125107
transactionName,
126108
}: {
127109
client: ClientWithEns
128110
connectorClient: ConnectorClientWithEns
129-
isSafeApp: boolean
130111
txWithZeroGas: BasicTransactionRequest
131112
transactionName: TransactionName
132113
}) => {
133-
if (isSafeApp) {
134-
const accessListResponse = await client.request<{
135-
Method: 'eth_createAccessList'
136-
Parameters: [tx: TransactionRequest<Hex>, blockTag: BlockTag]
137-
ReturnType: AccessListResponse
138-
}>({
139-
method: 'eth_createAccessList',
140-
params: [
141-
{
142-
to: txWithZeroGas.to,
143-
data: txWithZeroGas.data,
144-
from: connectorClient.account!.address,
145-
value: toHex(txWithZeroGas.value ? txWithZeroGas.value + 1000000n : 0n),
146-
},
147-
'latest',
148-
],
149-
})
150-
151-
return {
152-
gasLimit: registrationGasFeeModifier(BigInt(accessListResponse.gasUsed), transactionName),
153-
accessList: accessListResponse.accessList,
154-
}
155-
}
114+
const accessListResponse = await createAccessList(client, {
115+
to: txWithZeroGas.to,
116+
data: txWithZeroGas.data,
117+
from: connectorClient.account!.address,
118+
value: toHex(txWithZeroGas.value ? txWithZeroGas.value + 1000000n : 0n),
119+
})
156120

157121
const gasEstimate = await estimateGas(client, {
158122
...txWithZeroGas,
159-
account: connectorClient.account!,
123+
accessList: accessListResponse.accessList,
124+
account: connectorClient.account,
160125
})
126+
161127
return {
162128
gasLimit: registrationGasFeeModifier(gasEstimate, transactionName),
163-
accessList: undefined,
129+
accessList: accessListResponse.accessList,
164130
}
165131
}
166132

@@ -195,7 +161,6 @@ export type CreateTransactionRequestQueryKey = CreateQueryKey<
195161
type CreateTransactionRequestUnsafeParameters = {
196162
client: ClientWithEns
197163
connectorClient: ConnectorClientWithEns
198-
isSafeApp: CheckIsSafeAppReturnType | undefined
199164
params: UniqueTransaction
200165
chainId: SupportedChain['id']
201166
connections: any
@@ -204,7 +169,6 @@ type CreateTransactionRequestUnsafeParameters = {
204169
export const createTransactionRequestUnsafe = async ({
205170
client,
206171
connectorClient,
207-
isSafeApp,
208172
params,
209173
chainId,
210174
connections,
@@ -225,7 +189,6 @@ export const createTransactionRequestUnsafe = async ({
225189
const { gasLimit, accessList } = await calculateGasLimit({
226190
client,
227191
connectorClient,
228-
isSafeApp: !!isSafeApp,
229192
txWithZeroGas,
230193
transactionName: params.name,
231194
})
@@ -261,11 +224,9 @@ export const createTransactionRequestQueryFn =
261224
(config: ConfigWithEns) =>
262225
({
263226
connectorClient,
264-
isSafeApp,
265227
connections,
266228
}: {
267229
connectorClient: ConnectorClientWithEns | undefined
268-
isSafeApp: CheckIsSafeAppReturnType | undefined
269230
connections: ReturnType<typeof useConnections>
270231
}) =>
271232
async ({
@@ -282,7 +243,6 @@ export const createTransactionRequestQueryFn =
282243
data: await createTransactionRequestUnsafe({
283244
client,
284245
connectorClient,
285-
isSafeApp,
286246
params,
287247
chainId,
288248
connections,

src/hooks/chain/useEstimateGasWithStateOverride.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
parseEther,
1515
RpcTransactionRequest,
1616
toHex,
17-
TransactionRequest,
1817
} from 'viem'
1918
import { useConnectorClient } from 'wagmi'
2019

@@ -31,8 +30,10 @@ import {
3130
Prettify,
3231
QueryConfig,
3332
} from '@app/types'
33+
import { emptyAddress } from '@app/utils/constants'
3434
import { getIsCachedData } from '@app/utils/getIsCachedData'
3535
import { prepareQueryOptions } from '@app/utils/prepareQueryOptions'
36+
import { createAccessList } from '@app/utils/query/createAccessList'
3637
import { useQuery } from '@app/utils/query/useQuery'
3738

3839
import { useGasPrice } from './useGasPrice'
@@ -164,10 +165,28 @@ const estimateIndividualGas = async <TName extends TransactionName>({
164165
name,
165166
})
166167

168+
// SCAs use >21k gas to execute a transfer (most of the time, but depends on implementation)
169+
// For any SCA transaction involving a transfer, the transaction will probably fail by default due to the extra gas.
170+
// To fix this, we can use an access list of what storage slots are accessed in a transfer to pre-pay the cold gas,
171+
// then when the transfer is done, only warm gas is used and the transfer is under the limit.
172+
// Normally, you can just pull the estimated gas via `eth_getAccessList`, since it returns the access list + gas used,
173+
// but since we're using a state override and that method doesn't support it, we have to do it manually.
174+
// (Technically geth now has an implementation, but it's very new and only in geth, see https://github.com/ethereum/go-ethereum/issues/27630)
175+
//
176+
// To get the access list, we're executing the bytecode of this Yul code: https://gist.github.com/TateB/777287c9a63d5f02fcd905232ce5748a
177+
// (note: 0xed3869F3020315C839b2f4E9a73bEbE9a9670534 is replaced with `connectorClient.account.address`)
178+
// It does a simple transfer, and accesses any storage slot that would be accessed by any other transfer.
179+
const accessList = await createAccessList(client, {
180+
from: emptyAddress,
181+
data: concatHex(['0x5f808080600173', connectorClient.account.address, '0x5af100']),
182+
value: '0x1',
183+
})
184+
167185
const formattedRequest = formatTransactionRequest({
168186
...generatedRequest,
169187
from: connectorClient.account.address,
170-
} as TransactionRequest)
188+
accessList: accessList.accessList,
189+
})
171190

172191
const stateOverrideWithBalance = stateOverride?.find(
173192
(s) => s.address === connectorClient.account.address,

src/hooks/transactions/transactionStore.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,8 @@ describe('transactionStore', () => {
5656
setTimeout(() => {
5757
onReplaced!({
5858
reason: 'repriced',
59-
transaction: {
60-
hash: newHash,
61-
},
62-
} as any)
59+
transactionHash: newHash,
60+
})
6361
}, 0)
6462

6563
return {

0 commit comments

Comments
 (0)