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/dry-paws-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink-deployments-framework": minor
---

feat(operation): support gas bumping option for NewDeploy
9 changes: 8 additions & 1 deletion chain/evm/operations2/contract/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ type DeployInput[ARGS any] struct {
Qualifier *string `json:"qualifier,omitempty"`
// Args are the parameters passed to the contract constructor.
Args ARGS `json:"args"`
// GasLimit optionally overrides the deployer gas limit on EVM chains.
// Normally set by RetryDeployWithGasBoost after gas-related failures; may also be set manually.
GasLimit uint64 `json:"gasLimit,omitempty"`
// GasPrice optionally overrides the deployer legacy gas price on EVM chains (wei).
// When non-zero, deploy uses a legacy fee transaction and clears any EIP-1559 GasFeeCap/GasTipCap.
Comment thread
graham-chainlink marked this conversation as resolved.
// Normally set by RetryDeployWithGasBoost after gas-related failures; may also be set manually.
GasPrice uint64 `json:"gasPrice,omitempty"`
}

// Bytecode specifies the exact bytecode to deploy for each supported VM.
Expand Down Expand Up @@ -133,7 +140,7 @@ func NewDeploy[ARGS any](params DeployParams[ARGS]) *operations.Operation[Deploy
)
} else {
addr, tx, _, deployErr = deployEVMContract(
chain.DeployerKey,
deployTransactOpts(chain.DeployerKey, input.GasLimit, input.GasPrice),
*parsedABI,
bytecode.EVM,
chain.Client,
Expand Down
26 changes: 25 additions & 1 deletion chain/evm/operations2/contract/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"math/big"
"testing"

"github.com/Masterminds/semver/v3"
Expand Down Expand Up @@ -77,6 +78,16 @@ func TestDeploy(t *testing.T) {
},
isZkSyncVM: false,
},
{
desc: "evm deployment with gas overrides",
input: DeployInput[ConstructorArgs]{
Args: ConstructorArgs{Value: 2},
TypeAndVersion: deployment.NewTypeAndVersion(testContractType, *semver.MustParse("1.0.0")),
GasLimit: 750_000,
GasPrice: 25_000_000_000,
},
isZkSyncVM: false,
},
}

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

deployZkContract = func(
_ context.Context,
Expand All @@ -140,7 +157,7 @@ func TestDeploy(t *testing.T) {
return address, nil
}
deployEVMContract = func(
_ *bind.TransactOpts,
opts *bind.TransactOpts,
_ abi.ABI,
_ []byte,
_ bind.ContractBackend,
Expand All @@ -155,6 +172,13 @@ func TestDeploy(t *testing.T) {
)),
}
}
if test.input.GasLimit > 0 || test.input.GasPrice > 0 {
require.NotNil(t, opts)
require.Equal(t, test.input.GasLimit, opts.GasLimit)
require.Equal(t, test.input.GasPrice, opts.GasPrice.Uint64())
require.Nil(t, opts.GasFeeCap)
require.Nil(t, opts.GasTipCap)
}

return address, types.NewTx(&types.LegacyTx{
To: &address,
Expand Down
169 changes: 169 additions & 0 deletions chain/evm/operations2/contract/gas_boost.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package contract

import (
"errors"
"math/big"
"strings"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/vm"

"github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
)

const (
defaultInitialGasLimit = uint64(5_000_000)
defaultGasLimitIncrement = uint64(500_000)
defaultInitialGasPrice = uint64(20_000_000_000)
defaultGasPriceIncrement = uint64(10_000_000_000)
)

// GasBoostConfig defines gas limit and price increments applied on deploy retries.
// Increment fields use pointers so zero is a valid configured value (no increment).
// A nil increment pointer uses the package default increment.
type GasBoostConfig struct {
InitialGasLimit uint64 `json:"initialGasLimit"`
GasLimitIncrement *uint64 `json:"gasLimitIncrement,omitempty"`
InitialGasPrice uint64 `json:"initialGasPrice"`
GasPriceIncrement *uint64 `json:"gasPriceIncrement,omitempty"`
}

// RetryDeployWithGasBoost enables the default operation retry policy and increases gas on EVM deploy retries.
// 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 (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).
func RetryDeployWithGasBoost[ARGS any](cfg *GasBoostConfig) operations.ExecuteOption[DeployInput[ARGS], evm.Chain] {
if cfg == nil {
return func(*operations.ExecuteConfig[DeployInput[ARGS], evm.Chain]) {}
}
c := *cfg

return operations.WithRetryInput(func(attempt uint, err error, in DeployInput[ARGS], deps evm.Chain) DeployInput[ARGS] {
if deps.IsZkSyncVM || !isGasRetryableError(err) {
return in
}

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
}

return *cfg.GasLimitIncrement
}

func (cfg GasBoostConfig) gasPriceIncrement() uint64 {
if cfg.GasPriceIncrement == nil {
return defaultGasPriceIncrement
}

return *cfg.GasPriceIncrement
}

func resolveInitialGasLimit(cfg GasBoostConfig) uint64 {
if cfg.InitialGasLimit > 0 {
return cfg.InitialGasLimit
}

return defaultInitialGasLimit
}

func resolveInitialGasPrice(cfg GasBoostConfig) uint64 {
if cfg.InitialGasPrice > 0 {
return cfg.InitialGasPrice
}

return defaultInitialGasPrice
}

func nextBoostedGas(cfg GasBoostConfig, attempt uint, previousLimit, previousPrice uint64) (gasLimit uint64, gasPrice uint64) {
initialGasLimit := resolveInitialGasLimit(cfg)
gasLimitIncrement := cfg.gasLimitIncrement()
initialGasPrice := resolveInitialGasPrice(cfg)
gasPriceIncrement := cfg.gasPriceIncrement()

if previousLimit > 0 {
gasLimit = previousLimit + gasLimitIncrement
} else {
gasLimit = initialGasLimit + uint64(attempt)*gasLimitIncrement
}

if previousPrice > 0 {
gasPrice = previousPrice + gasPriceIncrement
} else {
gasPrice = initialGasPrice + uint64(attempt)*gasPriceIncrement
}

return gasLimit, gasPrice
}

func isGasRetryableError(err error) bool {
if err == nil {
return false
}

if errors.Is(err, vm.ErrOutOfGas) ||
errors.Is(err, vm.ErrCodeStoreOutOfGas) ||
errors.Is(err, core.ErrIntrinsicGas) ||
errors.Is(err, core.ErrFeeCapTooLow) ||
errors.Is(err, core.ErrGasLimitReached) ||
errors.Is(err, txpool.ErrUnderpriced) ||
errors.Is(err, txpool.ErrReplaceUnderpriced) ||
errors.Is(err, txpool.ErrTxGasPriceTooLow) ||
errors.Is(err, txpool.ErrGasLimit) {
return true
}

msg := strings.ToLower(err.Error())
for _, pattern := range gasRetryableErrorPatterns {
if strings.Contains(msg, pattern) {
return true
}
}

return false
}

// gasRetryableErrorPatterns covers RPC and wrapped errors that no longer carry geth sentinels.
var gasRetryableErrorPatterns = []string{
"out of gas",
"gas required exceeds allowance",
"intrinsic gas too low",
"underpriced",
"replacement transaction underpriced",
"max fee per gas less than block base fee",
"exceeds block gas limit",
}
Comment on lines +140 to +148

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit(noaction): I wonder if we can find better ways to detect retryable errors. I've seen this pattern in gauntlet too, but I've never been happy about it. Wondering if there's better mechanisms nowadays

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i know! I dont like this too, currently i am just using a hybrid approach of hardcoded strings plus sentinel errors that we know exist


func deployTransactOpts(base *bind.TransactOpts, gasLimit, gasPrice uint64) *bind.TransactOpts {
if base == nil {
return nil
}
if gasLimit == 0 && gasPrice == 0 {
return base
}

opts := *base
if gasLimit > 0 {
opts.GasLimit = gasLimit
}
if gasPrice > 0 {
opts.GasPrice = new(big.Int).SetUint64(gasPrice)
opts.GasFeeCap = nil
opts.GasTipCap = nil
}

return &opts
}
Loading
Loading