-
Notifications
You must be signed in to change notification settings - Fork 3
feat(evm): add gas boost retry option for contract deploys [CLD-2779] #1047
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.