Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fast-rules-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink-deployments-framework": minor
---

feat(evm): allow gas bumping on contract.NewWrite
2 changes: 1 addition & 1 deletion chain/evm/operations2/contract/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func NewDeploy[ARGS any](params DeployParams[ARGS]) *operations.Operation[Deploy
)
} else {
addr, tx, _, deployErr = deployEVMContract(
deployTransactOpts(chain.DeployerKey, input.GasLimit, input.GasPrice),
transactOptsWithGasOverrides(chain.DeployerKey, input.GasLimit, input.GasPrice),
*parsedABI,
bytecode.EVM,
chain.Client,
Expand Down
7 changes: 7 additions & 0 deletions chain/evm/operations2/contract/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,11 @@ package contract
type FunctionInput[ARGS any] struct {
// Args are the parameters passed to the contract call.
Args ARGS `json:"args"`
// GasLimit optionally overrides the deployer gas limit on EVM writes.
// Normally set by RetryWriteWithGasBoost after gas-related failures; may also be set manually.
GasLimit uint64 `json:"gasLimit,omitempty"`
// GasPrice optionally overrides the deployer legacy gas price on EVM writes (wei).
// When non-zero, the write uses a legacy fee transaction and clears any EIP-1559 GasFeeCap/GasTipCap.
// Normally set by RetryWriteWithGasBoost after gas-related failures; may also be set manually.
GasPrice uint64 `json:"gasPrice,omitempty"`
}
30 changes: 29 additions & 1 deletion chain/evm/operations2/contract/gas_boost.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type GasBoostConfig struct {
// The first execution attempt uses the chain deployer's default gas settings (auto-estimation).
// On ZkSync VM chains, gas fields are not adjusted; omit this option for ZkSync-only deploy flows.
// When cfg is nil, returns a no-op option and retry remains disabled (omit this option instead).
// Use operations.WithRetry for retry without gas adjustment.
func RetryDeployWithGasBoost[ARGS any](cfg *GasBoostConfig) operations.ExecuteOption[DeployInput[ARGS], evm.Chain] {
if cfg == nil {
return func(*operations.ExecuteConfig[DeployInput[ARGS], evm.Chain]) {}
Expand All @@ -56,6 +57,33 @@ func RetryDeployWithGasBoost[ARGS any](cfg *GasBoostConfig) operations.ExecuteOp
})
}

// RetryWriteWithGasBoost enables the default operation retry policy for contract writes and
// increases gas on EVM retries when the prior attempt failed with a gas-related error.
// The operation may retry on any failure (per the framework retry policy); gas limit and price are adjusted
// only when the prior attempt failed with a gas-related error.
// The first execution attempt uses the chain deployer's default gas settings.
// On ZkSync VM chains, gas fields are not adjusted; omit this option for ZkSync-only write flows.
// When cfg is nil, returns a no-op option and retry remains disabled (omit this option instead).
// Use operations.WithRetry for retry without gas adjustment.
func RetryWriteWithGasBoost[ARGS any](cfg *GasBoostConfig) operations.ExecuteOption[FunctionInput[ARGS], evm.Chain] {
if cfg == nil {
return func(*operations.ExecuteConfig[FunctionInput[ARGS], evm.Chain]) {}
}
Comment thread
graham-chainlink marked this conversation as resolved.
c := *cfg

return operations.WithRetryInput(func(attempt uint, err error, in FunctionInput[ARGS], deps evm.Chain) FunctionInput[ARGS] {
if deps.IsZkSyncVM || !isGasRetryableError(err) {
return in
}
Comment thread
graham-chainlink marked this conversation as resolved.

gasLimit, gasPrice := nextBoostedGas(c, attempt, in.GasLimit, in.GasPrice)
in.GasLimit = gasLimit
in.GasPrice = gasPrice

return in
})
}

func (cfg GasBoostConfig) gasLimitIncrement() uint64 {
if cfg.GasLimitIncrement == nil {
return defaultGasLimitIncrement
Expand Down Expand Up @@ -147,7 +175,7 @@ var gasRetryableErrorPatterns = []string{
"exceeds block gas limit",
}

func deployTransactOpts(base *bind.TransactOpts, gasLimit, gasPrice uint64) *bind.TransactOpts {
func transactOptsWithGasOverrides(base *bind.TransactOpts, gasLimit, gasPrice uint64) *bind.TransactOpts {
if base == nil {
return nil
}
Expand Down
142 changes: 136 additions & 6 deletions chain/evm/operations2/contract/gas_boost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func TestIsGasRetryableError(t *testing.T) {
require.True(t, isGasRetryableError(fmt.Errorf("deploy failed: %w", core.ErrFeeCapTooLow)))
}

func TestDeployTransactOpts(t *testing.T) {
func TestTransactOptsWithGasOverrides(t *testing.T) {
t.Parallel()

base := &bind.TransactOpts{
Expand All @@ -77,21 +77,21 @@ func TestDeployTransactOpts(t *testing.T) {
GasTipCap: big.NewInt(2),
}

require.Nil(t, deployTransactOpts(nil, 1, 1))
require.Same(t, base, deployTransactOpts(base, 0, 0))
require.Nil(t, transactOptsWithGasOverrides(nil, 1, 1))
require.Same(t, base, transactOptsWithGasOverrides(base, 0, 0))

limitOnly := deployTransactOpts(base, 500_000, 0)
limitOnly := transactOptsWithGasOverrides(base, 500_000, 0)
require.Equal(t, uint64(500_000), limitOnly.GasLimit)
require.Equal(t, big.NewInt(1), limitOnly.GasPrice)
require.Equal(t, big.NewInt(100), limitOnly.GasFeeCap)

priceOnly := deployTransactOpts(base, 0, 30_000_000_000)
priceOnly := transactOptsWithGasOverrides(base, 0, 30_000_000_000)
require.Equal(t, uint64(21_000), priceOnly.GasLimit)
require.Equal(t, uint64(30_000_000_000), priceOnly.GasPrice.Uint64())
require.Nil(t, priceOnly.GasFeeCap)
require.Nil(t, priceOnly.GasTipCap)

withGas := deployTransactOpts(base, 500_000, 30_000_000_000)
withGas := transactOptsWithGasOverrides(base, 500_000, 30_000_000_000)
require.Equal(t, uint64(500_000), withGas.GasLimit)
require.Equal(t, uint64(30_000_000_000), withGas.GasPrice.Uint64())
require.Nil(t, withGas.GasFeeCap)
Expand Down Expand Up @@ -236,3 +236,133 @@ func TestRetryDeployWithGasBoostSkipsZkSync(t *testing.T) {
func ptrUint64(v uint64) *uint64 {
return &v
}

func TestRetryWriteWithGasBoostRetriesOnGasError(t *testing.T) {
t.Parallel()

failures := 2
var gasLimits []uint64
op := operations.NewOperation(
"write-gas-boost-retry",
semver.MustParse("1.0.0"),
"test",
func(_ operations.Bundle, _ evm.Chain, input FunctionInput[struct{}]) (struct{}, error) {
gasLimits = append(gasLimits, input.GasLimit)
if failures > 0 {
failures--
return struct{}{}, errors.New("out of gas: gas required exceeds allowance")
}

return struct{}{}, nil
},
)

cfg := &GasBoostConfig{
InitialGasLimit: 1_000_000,
GasLimitIncrement: ptrUint64(100_000),
}
bundle := optest.NewBundle(t)
_, err := operations.ExecuteOperation(
bundle,
op,
evm.Chain{Selector: 1},
FunctionInput[struct{}]{},
RetryWriteWithGasBoost[struct{}](cfg),
)
require.NoError(t, err)
require.Equal(t, []uint64{0, 1_000_000, 1_100_000}, gasLimits)
}

func TestRetryWriteWithGasBoostNilConfig(t *testing.T) {
t.Parallel()

failures := 2
calls := 0
op := operations.NewOperation(
"write-gas-boost-nil",
semver.MustParse("1.0.0"),
"test",
func(_ operations.Bundle, _ evm.Chain, _ FunctionInput[struct{}]) (struct{}, error) {
calls++
if failures > 0 {
failures--
return struct{}{}, errors.New("out of gas")
}

return struct{}{}, nil
},
)

bundle := optest.NewBundle(t)
_, err := operations.ExecuteOperation(
bundle,
op,
evm.Chain{Selector: 1},
FunctionInput[struct{}]{},
RetryWriteWithGasBoost[struct{}](nil),
)
require.Error(t, err)
require.Equal(t, 1, calls)
}

func TestRetryWriteWithGasBoostSkipsNonGasErrors(t *testing.T) {
t.Parallel()

var gasLimits []uint64
op := operations.NewOperation(
"write-gas-boost-skip",
semver.MustParse("1.0.0"),
"test",
func(_ operations.Bundle, _ evm.Chain, input FunctionInput[struct{}]) (struct{}, error) {
gasLimits = append(gasLimits, input.GasLimit)
return struct{}{}, errors.New("revert: unauthorized")
},
)

bundle := optest.NewBundle(t)
_, err := operations.ExecuteOperation(
bundle,
op,
evm.Chain{Selector: 1},
FunctionInput[struct{}]{},
RetryWriteWithGasBoost[struct{}](&GasBoostConfig{}),
)
require.Error(t, err)
for _, limit := range gasLimits {
require.Equal(t, uint64(0), limit)
}
}

func TestRetryWriteWithGasBoostSkipsZkSync(t *testing.T) {
t.Parallel()

failures := 2
var gasLimits []uint64
op := operations.NewOperation(
"write-gas-boost-zksync",
semver.MustParse("1.0.0"),
"test",
func(_ operations.Bundle, _ evm.Chain, input FunctionInput[struct{}]) (struct{}, error) {
gasLimits = append(gasLimits, input.GasLimit)
if failures > 0 {
failures--
return struct{}{}, errors.New("out of gas")
}

return struct{}{}, nil
},
)

bundle := optest.NewBundle(t)
_, err := operations.ExecuteOperation(
bundle,
op,
evm.Chain{Selector: 1, IsZkSyncVM: true},
FunctionInput[struct{}]{},
RetryWriteWithGasBoost[struct{}](&GasBoostConfig{}),
)
require.NoError(t, err)
for _, limit := range gasLimits {
require.Equal(t, uint64(0), limit)
}
}
2 changes: 1 addition & 1 deletion chain/evm/operations2/contract/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func NewWrite[ARGS any, C interface{ Address() common.Address }](params WritePar
}
opts := deployment.SimTransactOpts()
if allowed {
opts = chain.DeployerKey
opts = transactOptsWithGasOverrides(chain.DeployerKey, input.GasLimit, input.GasPrice)
}
Comment thread
graham-chainlink marked this conversation as resolved.
var execInfo *ExecInfo
tx, callErr := params.CallContract(params.Contract, opts, input.Args)
Expand Down
Loading