Skip to content

Commit dcd1601

Browse files
feat(evm): allow gas bumping on contract.NewWrite
contract.NewWrite is used by operations-gen in the generated code, we also want to support gas bumping here
1 parent fc56aeb commit dcd1601

5 files changed

Lines changed: 171 additions & 1 deletion

File tree

.changeset/fast-rules-sneeze.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": minor
3+
---
4+
5+
feat(evm): allow gas bumping on contract.NewWrite

chain/evm/operations2/contract/function.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,11 @@ package contract
44
type FunctionInput[ARGS any] struct {
55
// Args are the parameters passed to the contract call.
66
Args ARGS `json:"args"`
7+
// GasLimit optionally overrides the deployer gas limit on EVM writes.
8+
// Normally set by RetryWriteWithGasBoost after gas-related failures; may also be set manually.
9+
GasLimit uint64 `json:"gasLimit,omitempty"`
10+
// GasPrice optionally overrides the deployer legacy gas price on EVM writes (wei).
11+
// When non-zero, the write uses a legacy fee transaction and clears any EIP-1559 GasFeeCap/GasTipCap.
12+
// Normally set by RetryWriteWithGasBoost after gas-related failures; may also be set manually.
13+
GasPrice uint64 `json:"gasPrice,omitempty"`
714
}

chain/evm/operations2/contract/gas_boost.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type GasBoostConfig struct {
3737
// The first execution attempt uses the chain deployer's default gas settings (auto-estimation).
3838
// On ZkSync VM chains, gas fields are not adjusted; omit this option for ZkSync-only deploy flows.
3939
// When cfg is nil, returns a no-op option and retry remains disabled (omit this option instead).
40+
// Use operations.WithRetry for retry without gas adjustment.
4041
func RetryDeployWithGasBoost[ARGS any](cfg *GasBoostConfig) operations.ExecuteOption[DeployInput[ARGS], evm.Chain] {
4142
if cfg == nil {
4243
return func(*operations.ExecuteConfig[DeployInput[ARGS], evm.Chain]) {}
@@ -56,6 +57,33 @@ func RetryDeployWithGasBoost[ARGS any](cfg *GasBoostConfig) operations.ExecuteOp
5657
})
5758
}
5859

60+
// RetryWriteWithGasBoost enables the default operation retry policy for contract writes and
61+
// increases gas on EVM retries when the prior attempt failed with a gas-related error.
62+
// The operation may retry on any failure (per the framework retry policy); gas limit and price are adjusted
63+
// only when the prior attempt failed with a gas-related error.
64+
// The first execution attempt uses the chain deployer's default gas settings.
65+
// On ZkSync VM chains, gas fields are not adjusted; omit this option for ZkSync-only write flows.
66+
// When cfg is nil, returns a no-op option and retry remains disabled (omit this option instead).
67+
// Use operations.WithRetry for retry without gas adjustment.
68+
func RetryWriteWithGasBoost[ARGS any](cfg *GasBoostConfig) operations.ExecuteOption[FunctionInput[ARGS], evm.Chain] {
69+
if cfg == nil {
70+
return func(*operations.ExecuteConfig[FunctionInput[ARGS], evm.Chain]) {}
71+
}
72+
c := *cfg
73+
74+
return operations.WithRetryInput(func(attempt uint, err error, in FunctionInput[ARGS], deps evm.Chain) FunctionInput[ARGS] {
75+
if deps.IsZkSyncVM || !isGasRetryableError(err) {
76+
return in
77+
}
78+
79+
gasLimit, gasPrice := nextBoostedGas(c, attempt, in.GasLimit, in.GasPrice)
80+
in.GasLimit = gasLimit
81+
in.GasPrice = gasPrice
82+
83+
return in
84+
})
85+
}
86+
5987
func (cfg GasBoostConfig) gasLimitIncrement() uint64 {
6088
if cfg.GasLimitIncrement == nil {
6189
return defaultGasLimitIncrement

chain/evm/operations2/contract/gas_boost_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,133 @@ func TestRetryDeployWithGasBoostSkipsZkSync(t *testing.T) {
236236
func ptrUint64(v uint64) *uint64 {
237237
return &v
238238
}
239+
240+
func TestRetryWriteWithGasBoostRetriesOnGasError(t *testing.T) {
241+
t.Parallel()
242+
243+
failures := 2
244+
var gasLimits []uint64
245+
op := operations.NewOperation(
246+
"write-gas-boost-retry",
247+
semver.MustParse("1.0.0"),
248+
"test",
249+
func(_ operations.Bundle, _ evm.Chain, input FunctionInput[struct{}]) (struct{}, error) {
250+
gasLimits = append(gasLimits, input.GasLimit)
251+
if failures > 0 {
252+
failures--
253+
return struct{}{}, errors.New("out of gas: gas required exceeds allowance")
254+
}
255+
256+
return struct{}{}, nil
257+
},
258+
)
259+
260+
cfg := &GasBoostConfig{
261+
InitialGasLimit: 1_000_000,
262+
GasLimitIncrement: ptrUint64(100_000),
263+
}
264+
bundle := optest.NewBundle(t)
265+
_, err := operations.ExecuteOperation(
266+
bundle,
267+
op,
268+
evm.Chain{Selector: 1},
269+
FunctionInput[struct{}]{},
270+
RetryWriteWithGasBoost[struct{}](cfg),
271+
)
272+
require.NoError(t, err)
273+
require.Equal(t, []uint64{0, 1_000_000, 1_100_000}, gasLimits)
274+
}
275+
276+
func TestRetryWriteWithGasBoostNilConfig(t *testing.T) {
277+
t.Parallel()
278+
279+
failures := 2
280+
calls := 0
281+
op := operations.NewOperation(
282+
"write-gas-boost-nil",
283+
semver.MustParse("1.0.0"),
284+
"test",
285+
func(_ operations.Bundle, _ evm.Chain, _ FunctionInput[struct{}]) (struct{}, error) {
286+
calls++
287+
if failures > 0 {
288+
failures--
289+
return struct{}{}, errors.New("out of gas")
290+
}
291+
292+
return struct{}{}, nil
293+
},
294+
)
295+
296+
bundle := optest.NewBundle(t)
297+
_, err := operations.ExecuteOperation(
298+
bundle,
299+
op,
300+
evm.Chain{Selector: 1},
301+
FunctionInput[struct{}]{},
302+
RetryWriteWithGasBoost[struct{}](nil),
303+
)
304+
require.Error(t, err)
305+
require.Equal(t, 1, calls)
306+
}
307+
308+
func TestRetryWriteWithGasBoostSkipsNonGasErrors(t *testing.T) {
309+
t.Parallel()
310+
311+
var gasLimits []uint64
312+
op := operations.NewOperation(
313+
"write-gas-boost-skip",
314+
semver.MustParse("1.0.0"),
315+
"test",
316+
func(_ operations.Bundle, _ evm.Chain, input FunctionInput[struct{}]) (struct{}, error) {
317+
gasLimits = append(gasLimits, input.GasLimit)
318+
return struct{}{}, errors.New("revert: unauthorized")
319+
},
320+
)
321+
322+
bundle := optest.NewBundle(t)
323+
_, err := operations.ExecuteOperation(
324+
bundle,
325+
op,
326+
evm.Chain{Selector: 1},
327+
FunctionInput[struct{}]{},
328+
RetryWriteWithGasBoost[struct{}](&GasBoostConfig{}),
329+
)
330+
require.Error(t, err)
331+
for _, limit := range gasLimits {
332+
require.Equal(t, uint64(0), limit)
333+
}
334+
}
335+
336+
func TestRetryWriteWithGasBoostSkipsZkSync(t *testing.T) {
337+
t.Parallel()
338+
339+
failures := 2
340+
var gasLimits []uint64
341+
op := operations.NewOperation(
342+
"write-gas-boost-zksync",
343+
semver.MustParse("1.0.0"),
344+
"test",
345+
func(_ operations.Bundle, _ evm.Chain, input FunctionInput[struct{}]) (struct{}, error) {
346+
gasLimits = append(gasLimits, input.GasLimit)
347+
if failures > 0 {
348+
failures--
349+
return struct{}{}, errors.New("out of gas")
350+
}
351+
352+
return struct{}{}, nil
353+
},
354+
)
355+
356+
bundle := optest.NewBundle(t)
357+
_, err := operations.ExecuteOperation(
358+
bundle,
359+
op,
360+
evm.Chain{Selector: 1, IsZkSyncVM: true},
361+
FunctionInput[struct{}]{},
362+
RetryWriteWithGasBoost[struct{}](&GasBoostConfig{}),
363+
)
364+
require.NoError(t, err)
365+
for _, limit := range gasLimits {
366+
require.Equal(t, uint64(0), limit)
367+
}
368+
}

chain/evm/operations2/contract/write.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func NewWrite[ARGS any, C interface{ Address() common.Address }](params WritePar
9494
}
9595
opts := deployment.SimTransactOpts()
9696
if allowed {
97-
opts = chain.DeployerKey
97+
opts = deployTransactOpts(chain.DeployerKey, input.GasLimit, input.GasPrice)
9898
}
9999
var execInfo *ExecInfo
100100
tx, callErr := params.CallContract(params.Contract, opts, input.Args)

0 commit comments

Comments
 (0)