Skip to content

Commit 1e305f4

Browse files
authored
fix: always estimate relay gasLimit (#4679)
1 parent 8423515 commit 1e305f4

File tree

8 files changed

+127
-18
lines changed

8 files changed

+127
-18
lines changed

src/components/new-safe/create/logic/index.test.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
getRedirect,
1111
createNewUndeployedSafeWithoutSalt,
1212
} from '@/components/new-safe/create/logic/index'
13-
import { relayTransaction } from '@safe-global/safe-gateway-typescript-sdk'
13+
import * as relaying from '@/services/tx/relaying'
1414
import { toBeHex } from 'ethers'
1515
import {
1616
Gnosis_safe__factory,
@@ -21,7 +21,6 @@ import {
2121
getReadOnlyGnosisSafeContract,
2222
getReadOnlyProxyFactoryContract,
2323
} from '@/services/contracts/safeContracts'
24-
import * as gateway from '@safe-global/safe-gateway-typescript-sdk'
2524
import { FEATURES, getLatestSafeVersion } from '@/utils/chains'
2625
import { type FEATURES as GatewayFeatures } from '@safe-global/safe-gateway-typescript-sdk'
2726
import { chainBuilder } from '@/tests/builders/chains'
@@ -68,13 +67,14 @@ describe('create/logic', () => {
6867
})
6968

7069
it('returns taskId if create Safe successfully relayed', async () => {
70+
const gasLimit = faker.string.numeric()
7171
const mockSafeProvider = {
7272
getExternalProvider: jest.fn(),
7373
getExternalSigner: jest.fn(),
7474
getChainId: jest.fn().mockReturnValue(BigInt(1)),
7575
} as unknown as SafeProvider
7676

77-
jest.spyOn(gateway, 'relayTransaction').mockResolvedValue({ taskId: '0x123' })
77+
jest.spyOn(relaying, 'relayTransaction').mockResolvedValue({ taskId: '0x123' })
7878
jest.spyOn(sdkHelpers, 'getSafeProvider').mockImplementation(() => mockSafeProvider)
7979

8080
jest.spyOn(contracts, 'getReadOnlyFallbackHandlerContract').mockResolvedValue({
@@ -123,20 +123,22 @@ describe('create/logic', () => {
123123
expectedSaltNonce,
124124
])
125125

126-
const taskId = await relaySafeCreation(mockChainInfo, undeployedSafeProps)
126+
const taskId = await relaySafeCreation(mockChainInfo, undeployedSafeProps, gasLimit)
127127

128128
expect(taskId).toEqual('0x123')
129-
expect(relayTransaction).toHaveBeenCalledTimes(1)
130-
expect(relayTransaction).toHaveBeenCalledWith('1', {
129+
expect(relaying.relayTransaction).toHaveBeenCalledTimes(1)
130+
expect(relaying.relayTransaction).toHaveBeenCalledWith('1', {
131131
to: proxyFactoryAddress,
132132
data: expectedCallData,
133133
version: latestSafeVersion,
134+
gasLimit,
134135
})
135136
})
136137

137138
it('should throw an error if relaying fails', () => {
139+
const gasLimit = faker.string.numeric()
138140
const relayFailedError = new Error('Relay failed')
139-
jest.spyOn(gateway, 'relayTransaction').mockRejectedValue(relayFailedError)
141+
jest.spyOn(relaying, 'relayTransaction').mockRejectedValue(relayFailedError)
140142

141143
const undeployedSafeProps: ReplayedSafeProps = {
142144
safeAccountConfig: {
@@ -155,7 +157,7 @@ describe('create/logic', () => {
155157
saltNonce: '69',
156158
}
157159

158-
expect(relaySafeCreation(mockChainInfo, undeployedSafeProps)).rejects.toEqual(relayFailedError)
160+
expect(relaySafeCreation(mockChainInfo, undeployedSafeProps, gasLimit)).rejects.toEqual(relayFailedError)
159161
})
160162
})
161163
describe('getRedirect', () => {

src/components/new-safe/create/logic/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import type { SafeVersion } from '@safe-global/safe-core-sdk-types'
22
import { type Eip1193Provider, type Provider } from 'ethers'
33
import semverSatisfies from 'semver/functions/satisfies'
44

5-
import { getSafeInfo, type SafeInfo, type ChainInfo, relayTransaction } from '@safe-global/safe-gateway-typescript-sdk'
5+
import { getSafeInfo, type SafeInfo, type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'
6+
import { relayTransaction } from '@/services/tx/relaying'
67
import { getReadOnlyProxyFactoryContract } from '@/services/contracts/safeContracts'
78
import type { UrlObject } from 'url'
89
import { AppRoutes } from '@/config/routes'
@@ -178,14 +179,19 @@ export const getRedirect = (
178179
return redirectUrl + `${appendChar}safe=${address}`
179180
}
180181

181-
export const relaySafeCreation = async (chain: ChainInfo, undeployedSafeProps: UndeployedSafeProps) => {
182+
export const relaySafeCreation = async (
183+
chain: ChainInfo,
184+
undeployedSafeProps: UndeployedSafeProps,
185+
gasLimit: string | undefined,
186+
) => {
182187
const replayedSafeProps = assertNewUndeployedSafeProps(undeployedSafeProps, chain)
183188
const encodedSafeCreationTx = encodeSafeCreationTx(replayedSafeProps, chain)
184189

185190
const relayResponse = await relayTransaction(chain.chainId, {
186191
to: replayedSafeProps.factoryAddress,
187192
data: encodedSafeCreationTx,
188193
version: replayedSafeProps.safeVersion,
194+
gasLimit,
189195
})
190196

191197
return relayResponse.taskId

src/components/new-safe/create/steps/ReviewStep/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps<NewSafe
336336
}
337337

338338
if (willRelay) {
339-
const taskId = await relaySafeCreation(chain, props)
339+
const taskId = await relaySafeCreation(chain, props, gasLimit?.toString())
340340
onSubmitCallback(taskId)
341341
} else {
342342
await createNewSafe(

src/features/counterfactual/ActivateAccountFlow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ const ActivateAccountFlow = () => {
121121

122122
try {
123123
if (willRelay) {
124-
const taskId = await relaySafeCreation(chain, undeployedSafe.props)
124+
const taskId = await relaySafeCreation(chain, undeployedSafe.props, options?.gasLimit?.toString())
125125
safeCreationDispatch(SafeCreationEvent.RELAYING, { groupKey: CF_TX_GROUP_KEY, taskId, safeAddress })
126126

127127
onSubmit()
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { faker } from '@faker-js/faker'
2+
import { relayTransaction as _relayTransaction } from '@safe-global/safe-gateway-typescript-sdk'
3+
4+
import { relayTransaction } from '@/services/tx/relaying'
5+
import { getWeb3ReadOnly } from '@/hooks/wallets/web3'
6+
7+
jest.mock('@/hooks/wallets/web3')
8+
jest.mock('@safe-global/safe-gateway-typescript-sdk')
9+
10+
describe('relayTransaction', () => {
11+
let chainId: string
12+
let body: {
13+
version: string
14+
to: string
15+
data: string
16+
gasLimit: string | undefined
17+
}
18+
19+
beforeEach(() => {
20+
jest.clearAllMocks()
21+
22+
chainId = faker.string.numeric()
23+
body = {
24+
version: faker.system.semver(),
25+
to: faker.finance.ethereumAddress(),
26+
data: faker.string.hexadecimal(),
27+
gasLimit: undefined,
28+
}
29+
})
30+
31+
it('should relay with the correct parameters when gasLimit is provided', async () => {
32+
const bodyWithGasLimit = {
33+
...body,
34+
gasLimit: faker.string.numeric(),
35+
}
36+
37+
await relayTransaction(chainId, bodyWithGasLimit)
38+
39+
expect(_relayTransaction).toHaveBeenCalledWith(chainId, bodyWithGasLimit)
40+
})
41+
42+
it('should estimate gasLimit if not provided and relay with the estimated gasLimit', async () => {
43+
const gasLimit = faker.number.bigInt()
44+
const mockProvider = {
45+
estimateGas: jest.fn().mockResolvedValue(gasLimit),
46+
}
47+
;(getWeb3ReadOnly as jest.Mock).mockReturnValue(mockProvider)
48+
49+
await relayTransaction(chainId, body)
50+
51+
expect(mockProvider.estimateGas).toHaveBeenCalledWith(body)
52+
expect(_relayTransaction).toHaveBeenCalledWith(chainId, { ...body, gasLimit: gasLimit.toString() })
53+
})
54+
55+
it('should relay with undefined gasLimit if estimation fails', async () => {
56+
const mockProvider = {
57+
estimateGas: jest.fn().mockRejectedValue(new Error('Estimation failed')),
58+
}
59+
;(getWeb3ReadOnly as jest.Mock).mockReturnValue(mockProvider)
60+
61+
await relayTransaction(chainId, body)
62+
63+
expect(mockProvider.estimateGas).toHaveBeenCalledWith(body)
64+
expect(_relayTransaction).toHaveBeenCalledWith(chainId, { ...body, gasLimit: undefined })
65+
})
66+
})

src/services/tx/relaying.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { relayTransaction as _relayTransaction } from '@safe-global/safe-gateway-typescript-sdk'
2+
3+
import { getWeb3ReadOnly } from '@/hooks/wallets/web3'
4+
5+
export async function relayTransaction(
6+
chainId: string,
7+
body: { version: string; to: string; data: string; gasLimit: string | undefined },
8+
) {
9+
/**
10+
* Ensures `gasLimit` is estimate so 150k execution overhead buffer can be added by CGW.
11+
*
12+
* @see https://github.com/safe-global/safe-client-gateway/blob/b7640ed4bd7416ecb7696d21ba105fcb5af52a10/src/datasources/relay-api/gelato-api.service.ts#L62-L75
13+
*
14+
* - If provided, CGW adds a 150k buffer before submission to Gelato.
15+
* - If not provided, no buffer is added, and Gelato estimates the it.
16+
*
17+
* Note: particularly important on networks like Arbitrum, where estimation is unreliable.
18+
*/
19+
if (!body.gasLimit) {
20+
const provider = getWeb3ReadOnly()
21+
22+
body.gasLimit = await provider
23+
?.estimateGas(body)
24+
.then(String)
25+
.catch(() => undefined)
26+
}
27+
28+
return _relayTransaction(chainId, body)
29+
}

src/services/tx/tx-sender/__tests__/ts-sender.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
type JsonRpcProvider,
2323
type JsonRpcSigner,
2424
} from 'ethers'
25+
import { faker } from '@faker-js/faker'
2526
import * as safeContracts from '@/services/contracts/safeContracts'
2627

2728
import * as web3 from '@/hooks/wallets/web3'
@@ -515,6 +516,13 @@ describe('txSender', () => {
515516

516517
describe('dispatchBatchExecutionRelay', () => {
517518
it('should relay a batch execution', async () => {
519+
const gasLimit = faker.number.bigInt()
520+
jest.spyOn(web3, 'getWeb3ReadOnly').mockImplementation(() => {
521+
return {
522+
estimateGas: jest.fn(() => Promise.resolve(gasLimit)),
523+
} as unknown as JsonRpcProvider
524+
})
525+
518526
const mockMultisendAddress = zeroPadValue('0x1234', 20)
519527
const safeAddress = toBeHex('0x567', 20)
520528

src/services/tx/tx-sender/dispatch.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,8 @@ import type { ConnectedWallet } from '@/hooks/wallets/useOnboard'
22
import { isMultisigExecutionInfo } from '@/utils/transaction-guards'
33
import { isHardwareWallet, isSmartContractWallet } from '@/utils/wallets'
44
import type { MultiSendCallOnlyContractImplementationType } from '@safe-global/protocol-kit'
5-
import {
6-
type ChainInfo,
7-
relayTransaction,
8-
type SafeInfo,
9-
type TransactionDetails,
10-
} from '@safe-global/safe-gateway-typescript-sdk'
5+
import { type ChainInfo, type SafeInfo, type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk'
6+
import { relayTransaction } from '@/services/tx/relaying'
117
import type {
128
SafeSignature,
139
SafeTransaction,
@@ -559,6 +555,8 @@ export const dispatchBatchExecutionRelay = async (
559555
to,
560556
data,
561557
version: safeVersion,
558+
// We have no estimation in place
559+
gasLimit: undefined,
562560
})
563561
} catch (error) {
564562
txs.forEach(({ txId }) => {

0 commit comments

Comments
 (0)