Skip to content

Commit b7836e2

Browse files
feat(evm): add gas boost retry option for contract deploys
Enable RetryDeployWithGasBoost to raise gas limit and price on gas-related deploy failures, with optional overrides wired through DeployInput for EVM deployments.
1 parent e31c0ae commit b7836e2

4 files changed

Lines changed: 436 additions & 2 deletions

File tree

chain/evm/operations2/contract/deploy.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ type DeployInput[ARGS any] struct {
4040
Qualifier *string `json:"qualifier,omitempty"`
4141
// Args are the parameters passed to the contract constructor.
4242
Args ARGS `json:"args"`
43+
// GasLimit and GasPrice optionally override deployer transact opts on EVM chains.
44+
// Normally set by RetryDeployWithGasBoost after gas-related failures; may also be set manually.
45+
GasLimit uint64 `json:"gasLimit,omitempty"`
46+
GasPrice uint64 `json:"gasPrice,omitempty"`
4347
}
4448

4549
// Bytecode specifies the exact bytecode to deploy for each supported VM.
@@ -133,7 +137,7 @@ func NewDeploy[ARGS any](params DeployParams[ARGS]) *operations.Operation[Deploy
133137
)
134138
} else {
135139
addr, tx, _, deployErr = deployEVMContract(
136-
chain.DeployerKey,
140+
deployTransactOpts(chain.DeployerKey, input.GasLimit, input.GasPrice),
137141
*parsedABI,
138142
bytecode.EVM,
139143
chain.Client,

chain/evm/operations2/contract/deployment_test.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"math/big"
78
"testing"
89

910
"github.com/Masterminds/semver/v3"
@@ -77,6 +78,16 @@ func TestDeploy(t *testing.T) {
7778
},
7879
isZkSyncVM: false,
7980
},
81+
{
82+
desc: "evm deployment with gas overrides",
83+
input: DeployInput[ConstructorArgs]{
84+
Args: ConstructorArgs{Value: 2},
85+
TypeAndVersion: deployment.NewTypeAndVersion(testContractType, *semver.MustParse("1.0.0")),
86+
GasLimit: 750_000,
87+
GasPrice: 25_000_000_000,
88+
},
89+
isZkSyncVM: false,
90+
},
8091
}
8192

8293
for _, test := range tests { //nolint:paralleltest // subtests modify package-level deployZkContract/deployEVMContract vars
@@ -127,6 +138,12 @@ func TestDeploy(t *testing.T) {
127138
},
128139
IsZkSyncVM: test.isZkSyncVM,
129140
}
141+
if test.input.GasLimit > 0 || test.input.GasPrice > 0 {
142+
chain.DeployerKey = &bind.TransactOpts{
143+
GasFeeCap: big.NewInt(100),
144+
GasTipCap: big.NewInt(2),
145+
}
146+
}
130147

131148
deployZkContract = func(
132149
_ context.Context,
@@ -140,7 +157,7 @@ func TestDeploy(t *testing.T) {
140157
return address, nil
141158
}
142159
deployEVMContract = func(
143-
_ *bind.TransactOpts,
160+
opts *bind.TransactOpts,
144161
_ abi.ABI,
145162
_ []byte,
146163
_ bind.ContractBackend,
@@ -155,6 +172,13 @@ func TestDeploy(t *testing.T) {
155172
)),
156173
}
157174
}
175+
if test.input.GasLimit > 0 || test.input.GasPrice > 0 {
176+
require.NotNil(t, opts)
177+
require.Equal(t, test.input.GasLimit, opts.GasLimit)
178+
require.Equal(t, test.input.GasPrice, opts.GasPrice.Uint64())
179+
require.Nil(t, opts.GasFeeCap)
180+
require.Nil(t, opts.GasTipCap)
181+
}
158182

159183
return address, types.NewTx(&types.LegacyTx{
160184
To: &address,
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package contract
2+
3+
import (
4+
"errors"
5+
"math/big"
6+
"strings"
7+
8+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
9+
"github.com/ethereum/go-ethereum/core"
10+
"github.com/ethereum/go-ethereum/core/txpool"
11+
"github.com/ethereum/go-ethereum/core/vm"
12+
13+
"github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
14+
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
15+
)
16+
17+
const (
18+
defaultInitialGasLimit = uint64(5_000_000)
19+
defaultGasLimitIncrement = uint64(500_000)
20+
defaultInitialGasPrice = uint64(20_000_000_000)
21+
defaultGasPriceIncrement = uint64(10_000_000_000)
22+
)
23+
24+
// GasBoostConfig defines gas limit and price increments applied on deploy retries.
25+
// Increment fields use pointers so zero is a valid configured value (no increment).
26+
// A nil increment pointer uses the package default increment.
27+
type GasBoostConfig struct {
28+
InitialGasLimit uint64 `json:"initialGasLimit"`
29+
GasLimitIncrement *uint64 `json:"gasLimitIncrement,omitempty"`
30+
InitialGasPrice uint64 `json:"initialGasPrice"`
31+
GasPriceIncrement *uint64 `json:"gasPriceIncrement,omitempty"`
32+
}
33+
34+
// RetryDeployWithGasBoost enables deploy retries with increasing gas on EVM chains.
35+
// The first execution attempt uses the chain deployer's default gas settings (auto-estimation).
36+
// Gas overrides apply only after a gas-related failure on subsequent retry attempts.
37+
// On ZkSync VM chains, gas fields are not adjusted; omit this option for ZkSync-only deploy flows.
38+
// When cfg is nil, returns a no-op option and retry remains disabled (omit this option instead).
39+
func RetryDeployWithGasBoost[ARGS any](cfg *GasBoostConfig) operations.ExecuteOption[DeployInput[ARGS], evm.Chain] {
40+
if cfg == nil {
41+
return func(*operations.ExecuteConfig[DeployInput[ARGS], evm.Chain]) {}
42+
}
43+
c := *cfg
44+
45+
return operations.WithRetryInput(func(attempt uint, err error, in DeployInput[ARGS], deps evm.Chain) DeployInput[ARGS] {
46+
if deps.IsZkSyncVM || !isGasRetryableError(err) {
47+
return in
48+
}
49+
50+
gasLimit, gasPrice := nextBoostedGas(c, attempt, in.GasLimit, in.GasPrice)
51+
in.GasLimit = gasLimit
52+
in.GasPrice = gasPrice
53+
54+
return in
55+
})
56+
}
57+
58+
func (cfg GasBoostConfig) gasLimitIncrement() uint64 {
59+
if cfg.GasLimitIncrement == nil {
60+
return defaultGasLimitIncrement
61+
}
62+
63+
return *cfg.GasLimitIncrement
64+
}
65+
66+
func (cfg GasBoostConfig) gasPriceIncrement() uint64 {
67+
if cfg.GasPriceIncrement == nil {
68+
return defaultGasPriceIncrement
69+
}
70+
71+
return *cfg.GasPriceIncrement
72+
}
73+
74+
func resolveInitialGasLimit(cfg GasBoostConfig) uint64 {
75+
if cfg.InitialGasLimit > 0 {
76+
return cfg.InitialGasLimit
77+
}
78+
79+
return defaultInitialGasLimit
80+
}
81+
82+
func resolveInitialGasPrice(cfg GasBoostConfig) uint64 {
83+
if cfg.InitialGasPrice > 0 {
84+
return cfg.InitialGasPrice
85+
}
86+
87+
return defaultInitialGasPrice
88+
}
89+
90+
func nextBoostedGas(cfg GasBoostConfig, attempt uint, previousLimit, previousPrice uint64) (gasLimit uint64, gasPrice uint64) {
91+
initialGasLimit := resolveInitialGasLimit(cfg)
92+
gasLimitIncrement := cfg.gasLimitIncrement()
93+
initialGasPrice := resolveInitialGasPrice(cfg)
94+
gasPriceIncrement := cfg.gasPriceIncrement()
95+
96+
if previousLimit > 0 {
97+
gasLimit = previousLimit + gasLimitIncrement
98+
} else {
99+
gasLimit = initialGasLimit + uint64(attempt)*gasLimitIncrement
100+
}
101+
102+
if previousPrice > 0 {
103+
gasPrice = previousPrice + gasPriceIncrement
104+
} else {
105+
gasPrice = initialGasPrice + uint64(attempt)*gasPriceIncrement
106+
}
107+
108+
return gasLimit, gasPrice
109+
}
110+
111+
func isGasRetryableError(err error) bool {
112+
if err == nil {
113+
return false
114+
}
115+
116+
if errors.Is(err, vm.ErrOutOfGas) ||
117+
errors.Is(err, vm.ErrCodeStoreOutOfGas) ||
118+
errors.Is(err, core.ErrIntrinsicGas) ||
119+
errors.Is(err, core.ErrFeeCapTooLow) ||
120+
errors.Is(err, core.ErrGasLimitReached) ||
121+
errors.Is(err, txpool.ErrUnderpriced) ||
122+
errors.Is(err, txpool.ErrReplaceUnderpriced) ||
123+
errors.Is(err, txpool.ErrTxGasPriceTooLow) ||
124+
errors.Is(err, txpool.ErrGasLimit) {
125+
return true
126+
}
127+
128+
msg := strings.ToLower(err.Error())
129+
for _, pattern := range gasRetryableErrorPatterns {
130+
if strings.Contains(msg, pattern) {
131+
return true
132+
}
133+
}
134+
135+
return false
136+
}
137+
138+
// gasRetryableErrorPatterns covers RPC and wrapped errors that no longer carry geth sentinels.
139+
var gasRetryableErrorPatterns = []string{
140+
"out of gas",
141+
"gas required exceeds allowance",
142+
"intrinsic gas too low",
143+
"underpriced",
144+
"replacement transaction underpriced",
145+
"max fee per gas less than block base fee",
146+
"exceeds block gas limit",
147+
}
148+
149+
func deployTransactOpts(base *bind.TransactOpts, gasLimit, gasPrice uint64) *bind.TransactOpts {
150+
if base == nil {
151+
return nil
152+
}
153+
if gasLimit == 0 && gasPrice == 0 {
154+
return base
155+
}
156+
157+
opts := *base
158+
if gasLimit > 0 {
159+
opts.GasLimit = gasLimit
160+
}
161+
if gasPrice > 0 {
162+
opts.GasPrice = new(big.Int).SetUint64(gasPrice)
163+
opts.GasFeeCap = nil
164+
opts.GasTipCap = nil
165+
}
166+
167+
return &opts
168+
}

0 commit comments

Comments
 (0)