diff --git a/.changeset/curly-birds-guess.md b/.changeset/curly-birds-guess.md new file mode 100644 index 00000000000..c66bd541787 --- /dev/null +++ b/.changeset/curly-birds-guess.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Fixed deadlock in RPCClient causing CL Node to stop performing RPC requests for the affected chain #bugfix diff --git a/.changeset/curly-onions-tell.md b/.changeset/curly-onions-tell.md new file mode 100644 index 00000000000..249f616c012 --- /dev/null +++ b/.changeset/curly-onions-tell.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#changed Make Mantle use default OP stack l1 gas oracle in core diff --git a/.changeset/great-timers-agree.md b/.changeset/great-timers-agree.md new file mode 100644 index 00000000000..bfa27761fb5 --- /dev/null +++ b/.changeset/great-timers-agree.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Updated gas limit estimation feature to set From address #internal diff --git a/.changeset/green-eagles-deliver.md b/.changeset/green-eagles-deliver.md new file mode 100644 index 00000000000..179b93b1080 --- /dev/null +++ b/.changeset/green-eagles-deliver.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Handle zkEVM node level OOC error as TerminallyStuck #internal diff --git a/.changeset/khaki-tigers-agree.md b/.changeset/khaki-tigers-agree.md new file mode 100644 index 00000000000..ba9e56cc1da --- /dev/null +++ b/.changeset/khaki-tigers-agree.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#added #nops Add Zircuit Configs diff --git a/.changeset/loud-windows-call.md b/.changeset/loud-windows-call.md new file mode 100644 index 00000000000..6dc8d6fac7f --- /dev/null +++ b/.changeset/loud-windows-call.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +Added gas limit estimation feature to EVM gas estimators. Introduced a new config `EVM.GasEstimator.EstimateLimit` to toggle this feature. #added diff --git a/.changeset/pretty-worms-smell.md b/.changeset/pretty-worms-smell.md new file mode 100644 index 00000000000..9633de09502 --- /dev/null +++ b/.changeset/pretty-worms-smell.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Added a custom client error message for Mantle to capture NonceTooLow error. #added diff --git a/.changeset/seven-kiwis-run.md b/.changeset/seven-kiwis-run.md new file mode 100644 index 00000000000..3b56117c469 --- /dev/null +++ b/.changeset/seven-kiwis-run.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Added custom client error messages for Mantle to capture InsufficientEth and Fatal errors. #added diff --git a/.changeset/shiny-hornets-pretend.md b/.changeset/shiny-hornets-pretend.md new file mode 100644 index 00000000000..ff9946f4e73 --- /dev/null +++ b/.changeset/shiny-hornets-pretend.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +Introduce new gas estimator #internal diff --git a/.changeset/tender-lemons-obey.md b/.changeset/tender-lemons-obey.md new file mode 100644 index 00000000000..2d6cb774b0c --- /dev/null +++ b/.changeset/tender-lemons-obey.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Enable FeeHistory estimator for Polygon zkEVM #nops diff --git a/.changeset/two-mugs-complain.md b/.changeset/two-mugs-complain.md new file mode 100644 index 00000000000..77cdcbfe9ec --- /dev/null +++ b/.changeset/two-mugs-complain.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +Edited the Optimism Stack L1 Oracle to add support for Mantle #added diff --git a/.mockery.yaml b/.mockery.yaml index 7fb335620cb..471f9318565 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -65,6 +65,9 @@ packages: feeEstimatorClient: config: mockname: FeeEstimatorClient + feeHistoryEstimatorClient: + config: + mockname: FeeHistoryEstimatorClient EvmEstimator: github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups: interfaces: @@ -450,7 +453,7 @@ packages: filename: optimism_portal2_interface.go outpkg: mock_optimism_portal_2 interfaces: - OptimismPortal2Interface: + OptimismPortal2Interface: github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/optimism_dispute_game_factory: config: dir: core/gethwrappers/liquiditymanager/mocks/mock_optimism_dispute_game_factory/ @@ -481,12 +484,20 @@ packages: PriceRegistryReader: config: filename: price_registry_reader_mock.go + FeeEstimatorConfigReader: + config: + filename: fee_estimator_config_mock.go TokenPoolReader: config: filename: token_pool_reader_mock.go USDCReader: config: filename: usdc_reader_mock.go + github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig: + interfaces: + GasPriceInterceptor: + config: + filename: gas_price_interceptor_mock.go github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/batchreader: config: filename: token_pool_batched_reader_mock.go diff --git a/common/fee/models.go b/common/fee/models.go index 0568a2f1433..0cc479d3560 100644 --- a/common/fee/models.go +++ b/common/fee/models.go @@ -14,6 +14,7 @@ var ( ErrBumpFeeExceedsLimit = errors.New("fee bump exceeds limit") ErrBump = errors.New("fee bump failed") ErrConnectivity = errors.New("transaction propagation issue: transactions are not being mined") + ErrFeeLimitTooLow = errors.New("provided fee limit too low") ) func IsBumpErr(err error) bool { @@ -63,7 +64,7 @@ func CalculateBumpedFee( // Returns highest bumped fee price of originalFeePrice bumped by fixed units or percentage. func MaxBumpedFee(originalFeePrice *big.Int, feeBumpPercent uint16, feeBumpUnits *big.Int) *big.Int { return bigmath.Max( - addPercentage(originalFeePrice, feeBumpPercent), + AddPercentage(originalFeePrice, feeBumpPercent), new(big.Int).Add(originalFeePrice, feeBumpUnits), ) } diff --git a/common/fee/utils.go b/common/fee/utils.go index 26323e11e26..3d4b001e839 100644 --- a/common/fee/utils.go +++ b/common/fee/utils.go @@ -18,7 +18,7 @@ func ApplyMultiplier(feeLimit uint64, multiplier float32) (uint64, error) { } // Returns the input value increased by the given percentage. -func addPercentage(value *big.Int, percentage uint16) *big.Int { +func AddPercentage(value *big.Int, percentage uint16) *big.Int { bumped := new(big.Int) bumped.Mul(value, big.NewInt(int64(100+percentage))) bumped.Div(bumped, big.NewInt(100)) diff --git a/common/txmgr/broadcaster.go b/common/txmgr/broadcaster.go index be3d3ca2f61..1606f58ce0d 100644 --- a/common/txmgr/broadcaster.go +++ b/common/txmgr/broadcaster.go @@ -20,6 +20,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink/v2/common/client" + commonfee "github.com/smartcontractkit/chainlink/v2/common/fee" feetypes "github.com/smartcontractkit/chainlink/v2/common/fee/types" txmgrtypes "github.com/smartcontractkit/chainlink/v2/common/txmgr/types" "github.com/smartcontractkit/chainlink/v2/common/types" @@ -434,7 +435,11 @@ func (eb *Broadcaster[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, SEQ, FEE]) hand } attempt, _, _, retryable, err := eb.NewTxAttempt(ctx, *etx, eb.lggr) - if err != nil { + // Mark transaction as fatal if provided gas limit is set too low + if errors.Is(err, commonfee.ErrFeeLimitTooLow) { + etx.Error = null.StringFrom(commonfee.ErrFeeLimitTooLow.Error()) + return eb.saveFatallyErroredTransaction(eb.lggr, etx), false + } else if err != nil { return fmt.Errorf("processUnstartedTxs failed on NewAttempt: %w", err), retryable } diff --git a/contracts/gas-snapshots/ccip.gas-snapshot b/contracts/gas-snapshots/ccip.gas-snapshot index 7e79a922d35..8c84c953054 100644 --- a/contracts/gas-snapshots/ccip.gas-snapshot +++ b/contracts/gas-snapshots/ccip.gas-snapshot @@ -103,7 +103,7 @@ CommitStore_report:test_OnlyPriceUpdateStaleReport_Revert() (gas: 59049) CommitStore_report:test_OnlyTokenPriceUpdates_Success() (gas: 53251) CommitStore_report:test_Paused_Revert() (gas: 21259) CommitStore_report:test_ReportAndPriceUpdate_Success() (gas: 84242) -CommitStore_report:test_ReportOnlyRootSuccess_gas() (gas: 56313) +CommitStore_report:test_ReportOnlyRootSuccess_gas() (gas: 56249) CommitStore_report:test_RootAlreadyCommitted_Revert() (gas: 63969) CommitStore_report:test_StaleReportWithRoot_Success() (gas: 119420) CommitStore_report:test_Unhealthy_Revert() (gas: 44751) @@ -123,13 +123,13 @@ CommitStore_verify:test_Paused_Revert() (gas: 18496) CommitStore_verify:test_TooManyLeaves_Revert() (gas: 36785) DefensiveExampleTest:test_HappyPath_Success() (gas: 200018) DefensiveExampleTest:test_Recovery() (gas: 424256) -E2E:test_E2E_3MessagesSuccess_gas() (gas: 1104303) +E2E:test_E2E_3MessagesSuccess_gas() (gas: 1133785) EVM2EVMOffRamp__releaseOrMintToken:test__releaseOrMintToken_NotACompatiblePool_Revert() (gas: 37797) EVM2EVMOffRamp__releaseOrMintToken:test__releaseOrMintToken_Success() (gas: 103733) -EVM2EVMOffRamp__releaseOrMintToken:test__releaseOrMintToken_TokenHandlingError_transfer_Revert() (gas: 85258) +EVM2EVMOffRamp__releaseOrMintToken:test__releaseOrMintToken_TokenHandlingError_transfer_Revert() (gas: 82758) EVM2EVMOffRamp__releaseOrMintToken:test_releaseOrMintToken_InvalidDataLength_Revert() (gas: 36786) EVM2EVMOffRamp__releaseOrMintToken:test_releaseOrMintToken_ReleaseOrMintBalanceMismatch_Revert() (gas: 94302) -EVM2EVMOffRamp__releaseOrMintToken:test_releaseOrMintToken_TokenHandlingError_BalanceOf_Revert() (gas: 39768) +EVM2EVMOffRamp__releaseOrMintToken:test_releaseOrMintToken_TokenHandlingError_BalanceOf_Revert() (gas: 37268) EVM2EVMOffRamp__releaseOrMintToken:test_releaseOrMintToken_skip_ReleaseOrMintBalanceMismatch_if_pool_Revert() (gas: 86559) EVM2EVMOffRamp__releaseOrMintTokens:test_OverValueWithARLOff_Success() (gas: 385308) EVM2EVMOffRamp__releaseOrMintTokens:test_PriceNotFoundForToken_Reverts() (gas: 141902) @@ -162,13 +162,13 @@ EVM2EVMOffRamp_execute:test_RootNotCommitted_Revert() (gas: 42539) EVM2EVMOffRamp_execute:test_SingleMessageNoTokensUnordered_Success() (gas: 157347) EVM2EVMOffRamp_execute:test_SingleMessageNoTokens_Success() (gas: 172692) EVM2EVMOffRamp_execute:test_SingleMessageToNonCCIPReceiver_Success() (gas: 247069) -EVM2EVMOffRamp_execute:test_SingleMessagesNoTokensSuccess_gas() (gas: 114153) +EVM2EVMOffRamp_execute:test_SingleMessagesNoTokensSuccess_gas() (gas: 113842) EVM2EVMOffRamp_execute:test_SkippedIncorrectNonceStillExecutes_Success() (gas: 407507) EVM2EVMOffRamp_execute:test_SkippedIncorrectNonce_Success() (gas: 54096) EVM2EVMOffRamp_execute:test_StrictUntouchedToSuccess_Success() (gas: 131047) EVM2EVMOffRamp_execute:test_TokenDataMismatch_Revert() (gas: 52090) EVM2EVMOffRamp_execute:test_TwoMessagesWithTokensAndGE_Success() (gas: 563497) -EVM2EVMOffRamp_execute:test_TwoMessagesWithTokensSuccess_gas() (gas: 495620) +EVM2EVMOffRamp_execute:test_TwoMessagesWithTokensSuccess_gas() (gas: 494294) EVM2EVMOffRamp_execute:test_UnexpectedTokenData_Revert() (gas: 35383) EVM2EVMOffRamp_execute:test_Unhealthy_Revert() (gas: 544753) EVM2EVMOffRamp_execute:test_UnsupportedNumberOfTokens_Revert() (gas: 64326) @@ -178,7 +178,7 @@ EVM2EVMOffRamp_execute:test_execute_RouterYULCall_Success() (gas: 427337) EVM2EVMOffRamp_executeSingleMessage:test_MessageSender_Revert() (gas: 18502) EVM2EVMOffRamp_executeSingleMessage:test_NonContractWithTokens_Success() (gas: 278153) EVM2EVMOffRamp_executeSingleMessage:test_NonContract_Success() (gas: 18659) -EVM2EVMOffRamp_executeSingleMessage:test_TokenHandlingError_Revert() (gas: 221449) +EVM2EVMOffRamp_executeSingleMessage:test_TokenHandlingError_Revert() (gas: 223949) EVM2EVMOffRamp_executeSingleMessage:test_ZeroGasDONExecution_Revert() (gas: 47881) EVM2EVMOffRamp_executeSingleMessage:test_executeSingleMessage_NoTokens_Success() (gas: 47352) EVM2EVMOffRamp_executeSingleMessage:test_executeSingleMessage_WithTokens_Success() (gas: 313973) @@ -230,21 +230,21 @@ EVM2EVMOnRamp_forwardFromRouter:test_MaxFeeBalanceReached_Revert() (gas: 36457) EVM2EVMOnRamp_forwardFromRouter:test_MessageGasLimitTooHigh_Revert() (gas: 29015) EVM2EVMOnRamp_forwardFromRouter:test_MessageTooLarge_Revert() (gas: 107571) EVM2EVMOnRamp_forwardFromRouter:test_OriginalSender_Revert() (gas: 22679) -EVM2EVMOnRamp_forwardFromRouter:test_OverValueWithARLOff_Success() (gas: 224625) +EVM2EVMOnRamp_forwardFromRouter:test_OverValueWithARLOff_Success() (gas: 227125) EVM2EVMOnRamp_forwardFromRouter:test_Paused_Revert() (gas: 53072) EVM2EVMOnRamp_forwardFromRouter:test_Permissions_Revert() (gas: 25481) EVM2EVMOnRamp_forwardFromRouter:test_PriceNotFoundForToken_Revert() (gas: 59347) EVM2EVMOnRamp_forwardFromRouter:test_ShouldIncrementNonceOnlyOnOrdered_Success() (gas: 179148) EVM2EVMOnRamp_forwardFromRouter:test_ShouldIncrementSeqNumAndNonce_Success() (gas: 177430) EVM2EVMOnRamp_forwardFromRouter:test_ShouldStoreNonLinkFees() (gas: 137322) -EVM2EVMOnRamp_forwardFromRouter:test_SourceTokenDataTooLarge_Revert() (gas: 3822827) +EVM2EVMOnRamp_forwardFromRouter:test_SourceTokenDataTooLarge_Revert() (gas: 3825327) EVM2EVMOnRamp_forwardFromRouter:test_TooManyTokens_Revert() (gas: 30187) EVM2EVMOnRamp_forwardFromRouter:test_Unhealthy_Revert() (gas: 43300) EVM2EVMOnRamp_forwardFromRouter:test_UnsupportedToken_Revert() (gas: 109283) EVM2EVMOnRamp_forwardFromRouter:test_ZeroAddressReceiver_Revert() (gas: 312579) EVM2EVMOnRamp_forwardFromRouter:test_forwardFromRouter_ShouldStoreLinkFees_Success() (gas: 112322) EVM2EVMOnRamp_forwardFromRouter:test_forwardFromRouter_UnsupportedToken_Revert() (gas: 72206) -EVM2EVMOnRamp_forwardFromRouter:test_forwardFromRouter_correctSourceTokenData_Success() (gas: 710531) +EVM2EVMOnRamp_forwardFromRouter:test_forwardFromRouter_correctSourceTokenData_Success() (gas: 713031) EVM2EVMOnRamp_forwardFromRouter_upgrade:test_V2NonceNewSenderStartsAtZero_Success() (gas: 147664) EVM2EVMOnRamp_forwardFromRouter_upgrade:test_V2NonceStartsAtV1Nonce_Success() (gas: 190529) EVM2EVMOnRamp_forwardFromRouter_upgrade:test_V2SenderNoncesReadsPreviousRamp_Success() (gas: 121320) @@ -275,7 +275,7 @@ EVM2EVMOnRamp_getTokenTransferCost:test__getTokenTransferCost_selfServeUsesDefau EVM2EVMOnRamp_linkAvailableForPayment:test_InsufficientLinkBalance_Success() (gas: 32615) EVM2EVMOnRamp_linkAvailableForPayment:test_LinkAvailableForPayment_Success() (gas: 134833) EVM2EVMOnRamp_payNops:test_AdminPayNops_Success() (gas: 143159) -EVM2EVMOnRamp_payNops:test_InsufficientBalance_Revert() (gas: 26543) +EVM2EVMOnRamp_payNops:test_InsufficientBalance_Revert() (gas: 29043) EVM2EVMOnRamp_payNops:test_NoFeesToPay_Revert() (gas: 127367) EVM2EVMOnRamp_payNops:test_NoNopsToPay_Revert() (gas: 133251) EVM2EVMOnRamp_payNops:test_NopPayNops_Success() (gas: 146446) @@ -307,7 +307,7 @@ EVM2EVMOnRamp_withdrawNonLinkFees:test_SettlingBalance_Success() (gas: 272035) EVM2EVMOnRamp_withdrawNonLinkFees:test_WithdrawNonLinkFees_Success() (gas: 53446) EVM2EVMOnRamp_withdrawNonLinkFees:test_WithdrawToZeroAddress_Revert() (gas: 12830) EtherSenderReceiverTest_ccipReceive:test_ccipReceive_fallbackToWethTransfer() (gas: 96729) -EtherSenderReceiverTest_ccipReceive:test_ccipReceive_happyPath() (gas: 47688) +EtherSenderReceiverTest_ccipReceive:test_ccipReceive_happyPath() (gas: 49688) EtherSenderReceiverTest_ccipReceive:test_ccipReceive_wrongToken() (gas: 17384) EtherSenderReceiverTest_ccipReceive:test_ccipReceive_wrongTokenAmount() (gas: 15677) EtherSenderReceiverTest_ccipSend:test_ccipSend_reverts_insufficientFee_feeToken() (gas: 99741) @@ -360,9 +360,9 @@ MerkleMultiProofTest:test_MerkleRootSingleLeaf_Success() (gas: 3649) MerkleMultiProofTest:test_SpecSync_gas() (gas: 34123) MockRouterTest:test_ccipSendWithInsufficientNativeTokens_Revert() (gas: 33965) MockRouterTest:test_ccipSendWithInvalidMsgValue_Revert() (gas: 60758) -MockRouterTest:test_ccipSendWithLinkFeeTokenAndValidMsgValue_Success() (gas: 126294) +MockRouterTest:test_ccipSendWithLinkFeeTokenAndValidMsgValue_Success() (gas: 126354) MockRouterTest:test_ccipSendWithLinkFeeTokenbutInsufficientAllowance_Revert() (gas: 63302) -MockRouterTest:test_ccipSendWithSufficientNativeFeeTokens_Success() (gas: 43853) +MockRouterTest:test_ccipSendWithSufficientNativeFeeTokens_Success() (gas: 43913) MultiAggregateRateLimiter_applyRateLimiterConfigUpdates:test_MultipleConfigsBothLanes_Success() (gas: 132031) MultiAggregateRateLimiter_applyRateLimiterConfigUpdates:test_MultipleConfigs_Success() (gas: 312057) MultiAggregateRateLimiter_applyRateLimiterConfigUpdates:test_OnlyCallableByOwner_Revert() (gas: 17717) @@ -388,7 +388,7 @@ MultiAggregateRateLimiter_onInboundMessage:test_ValidateMessageWithRateLimitExce MultiAggregateRateLimiter_onInboundMessage:test_ValidateMessageWithRateLimitReset_Success() (gas: 78780) MultiAggregateRateLimiter_onInboundMessage:test_ValidateMessageWithTokensOnDifferentChains_Success() (gas: 312061) MultiAggregateRateLimiter_onInboundMessage:test_ValidateMessageWithTokens_Success() (gas: 54784) -MultiAggregateRateLimiter_onOutboundMessage:test_RateLimitValueDifferentLanes_Success() (gas: 9223372036854754743) +MultiAggregateRateLimiter_onOutboundMessage:test_RateLimitValueDifferentLanes_Success() (gas: 53241) MultiAggregateRateLimiter_onOutboundMessage:test_ValidateMessageWithNoTokens_Success() (gas: 19104) MultiAggregateRateLimiter_onOutboundMessage:test_onOutboundMessage_ValidateMessageFromUnauthorizedCaller_Revert() (gas: 15778) MultiAggregateRateLimiter_onOutboundMessage:test_onOutboundMessage_ValidateMessageWithDifferentTokensOnDifferentChains_Success() (gas: 213732) @@ -430,16 +430,16 @@ MultiOCR3Base_transmit:test_InsufficientSignatures_Revert() (gas: 76930) MultiOCR3Base_transmit:test_NonUniqueSignature_Revert() (gas: 66127) MultiOCR3Base_transmit:test_SignatureOutOfRegistration_Revert() (gas: 33419) MultiOCR3Base_transmit:test_TooManySignatures_Revert() (gas: 79521) -MultiOCR3Base_transmit:test_TransmitSigners_gas_Success() (gas: 34131) +MultiOCR3Base_transmit:test_TransmitSigners_gas_Success() (gas: 34020) MultiOCR3Base_transmit:test_TransmitWithExtraCalldataArgs_Revert() (gas: 47114) MultiOCR3Base_transmit:test_TransmitWithLessCalldataArgs_Revert() (gas: 25682) -MultiOCR3Base_transmit:test_TransmitWithoutSignatureVerification_gas_Success() (gas: 18726) +MultiOCR3Base_transmit:test_TransmitWithoutSignatureVerification_gas_Success() (gas: 18714) MultiOCR3Base_transmit:test_UnAuthorizedTransmitter_Revert() (gas: 24191) MultiOCR3Base_transmit:test_UnauthorizedSigner_Revert() (gas: 61409) MultiOCR3Base_transmit:test_UnconfiguredPlugin_Revert() (gas: 39890) MultiOCR3Base_transmit:test_ZeroSignatures_Revert() (gas: 32973) MultiOnRampTokenPoolReentrancy:test_OnRampTokenPoolReentrancy_Success() (gas: 393335) -MultiRampsE2E:test_E2E_3MessagesMMultiOffRampSuccess_gas() (gas: 1448084) +MultiRampsE2E:test_E2E_3MessagesMMultiOffRampSuccess_gas() (gas: 1461081) NonceManager_NonceIncrementation:test_getIncrementedOutboundNonce_Success() (gas: 37907) NonceManager_NonceIncrementation:test_incrementInboundNonce_Skip() (gas: 23694) NonceManager_NonceIncrementation:test_incrementInboundNonce_Success() (gas: 38763) @@ -469,7 +469,7 @@ OCR2BaseNoChecks_setOCR2Config:test_TooManyTransmitter_Revert() (gas: 36938) OCR2BaseNoChecks_setOCR2Config:test_TransmitterCannotBeZeroAddress_Revert() (gas: 24158) OCR2BaseNoChecks_transmit:test_ConfigDigestMismatch_Revert() (gas: 17448) OCR2BaseNoChecks_transmit:test_ForkedChain_Revert() (gas: 26726) -OCR2BaseNoChecks_transmit:test_TransmitSuccess_gas() (gas: 27478) +OCR2BaseNoChecks_transmit:test_TransmitSuccess_gas() (gas: 27466) OCR2BaseNoChecks_transmit:test_UnAuthorizedTransmitter_Revert() (gas: 21296) OCR2Base_setOCR2Config:test_FMustBePositive_Revert() (gas: 12189) OCR2Base_setOCR2Config:test_FTooHigh_Revert() (gas: 12345) @@ -483,16 +483,16 @@ OCR2Base_transmit:test_ConfigDigestMismatch_Revert() (gas: 19623) OCR2Base_transmit:test_ForkedChain_Revert() (gas: 37683) OCR2Base_transmit:test_NonUniqueSignature_Revert() (gas: 55309) OCR2Base_transmit:test_SignatureOutOfRegistration_Revert() (gas: 20962) -OCR2Base_transmit:test_Transmit2SignersSuccess_gas() (gas: 51686) +OCR2Base_transmit:test_Transmit2SignersSuccess_gas() (gas: 51674) OCR2Base_transmit:test_UnAuthorizedTransmitter_Revert() (gas: 23484) OCR2Base_transmit:test_UnauthorizedSigner_Revert() (gas: 39665) OCR2Base_transmit:test_WrongNumberOfSignatures_Revert() (gas: 20557) OffRamp__releaseOrMintSingleToken:test__releaseOrMintSingleToken_NotACompatiblePool_Revert() (gas: 38408) OffRamp__releaseOrMintSingleToken:test__releaseOrMintSingleToken_Success() (gas: 106250) -OffRamp__releaseOrMintSingleToken:test__releaseOrMintSingleToken_TokenHandlingError_transfer_Revert() (gas: 87409) +OffRamp__releaseOrMintSingleToken:test__releaseOrMintSingleToken_TokenHandlingError_transfer_Revert() (gas: 84909) OffRamp__releaseOrMintSingleToken:test_releaseOrMintToken_InvalidDataLength_Revert() (gas: 38954) OffRamp__releaseOrMintSingleToken:test_releaseOrMintToken_ReleaseOrMintBalanceMismatch_Revert() (gas: 96511) -OffRamp__releaseOrMintSingleToken:test_releaseOrMintToken_TokenHandlingError_BalanceOf_Revert() (gas: 41956) +OffRamp__releaseOrMintSingleToken:test_releaseOrMintToken_TokenHandlingError_BalanceOf_Revert() (gas: 39456) OffRamp__releaseOrMintSingleToken:test_releaseOrMintToken_skip_ReleaseOrMintBalanceMismatch_if_pool_Revert() (gas: 88684) OffRamp_applySourceChainConfigUpdates:test_AddMultipleChains_Success() (gas: 468115) OffRamp_applySourceChainConfigUpdates:test_AddNewChain_Success() (gas: 99227) @@ -550,7 +550,7 @@ OffRamp_execute:test_ZeroReports_Revert() (gas: 17159) OffRamp_executeSingleMessage:test_MessageSender_Revert() (gas: 18190) OffRamp_executeSingleMessage:test_NonContractWithTokens_Success() (gas: 246556) OffRamp_executeSingleMessage:test_NonContract_Success() (gas: 20472) -OffRamp_executeSingleMessage:test_TokenHandlingError_Revert() (gas: 205195) +OffRamp_executeSingleMessage:test_TokenHandlingError_Revert() (gas: 207695) OffRamp_executeSingleMessage:test_ZeroGasDONExecution_Revert() (gas: 48734) OffRamp_executeSingleMessage:test_executeSingleMessage_NoTokens_Success() (gas: 48257) OffRamp_executeSingleMessage:test_executeSingleMessage_WithFailingValidationNoRouterCall_Revert() (gas: 229631) @@ -572,12 +572,12 @@ OffRamp_executeSingleReport:test_SingleMessageNoTokensOtherChain_Success() (gas: OffRamp_executeSingleReport:test_SingleMessageNoTokensUnordered_Success() (gas: 187191) OffRamp_executeSingleReport:test_SingleMessageNoTokens_Success() (gas: 206771) OffRamp_executeSingleReport:test_SingleMessageToNonCCIPReceiver_Success() (gas: 263519) -OffRamp_executeSingleReport:test_SingleMessagesNoTokensSuccess_gas() (gas: 138408) +OffRamp_executeSingleReport:test_SingleMessagesNoTokensSuccess_gas() (gas: 138111) OffRamp_executeSingleReport:test_SkippedIncorrectNonceStillExecutes_Success() (gas: 409328) OffRamp_executeSingleReport:test_SkippedIncorrectNonce_Success() (gas: 65876) OffRamp_executeSingleReport:test_TokenDataMismatch_Revert() (gas: 80909) OffRamp_executeSingleReport:test_TwoMessagesWithTokensAndGE_Success() (gas: 566299) -OffRamp_executeSingleReport:test_TwoMessagesWithTokensSuccess_gas() (gas: 517689) +OffRamp_executeSingleReport:test_TwoMessagesWithTokensSuccess_gas() (gas: 516604) OffRamp_executeSingleReport:test_UnexpectedTokenData_Revert() (gas: 35742) OffRamp_executeSingleReport:test_UnhealthySingleChainCurse_Revert() (gas: 517721) OffRamp_executeSingleReport:test_Unhealthy_Revert() (gas: 515089) @@ -646,10 +646,10 @@ OnRamp_forwardFromRouter:test_ShouldIncrementNonceOnlyOnOrdered_Success() (gas: OnRamp_forwardFromRouter:test_ShouldIncrementSeqNumAndNonce_Success() (gas: 205670) OnRamp_forwardFromRouter:test_ShouldStoreLinkFees() (gas: 121815) OnRamp_forwardFromRouter:test_ShouldStoreNonLinkFees() (gas: 143193) -OnRamp_forwardFromRouter:test_SourceTokenDataTooLarge_Revert() (gas: 3872608) +OnRamp_forwardFromRouter:test_SourceTokenDataTooLarge_Revert() (gas: 3875108) OnRamp_forwardFromRouter:test_UnsupportedToken_Revert() (gas: 108546) OnRamp_forwardFromRouter:test_forwardFromRouter_UnsupportedToken_Revert() (gas: 73975) -OnRamp_forwardFromRouter:test_forwardFromRouter_WithValidation_Success() (gas: 262685) +OnRamp_forwardFromRouter:test_forwardFromRouter_WithValidation_Success() (gas: 265185) OnRamp_getFee:test_EmptyMessage_Success() (gas: 104467) OnRamp_getFee:test_EnforceOutOfOrder_Revert() (gas: 74075) OnRamp_getFee:test_SingleTokenMessage_Success() (gas: 119799) @@ -662,7 +662,7 @@ OnRamp_setDynamicConfig:test_SetConfigInvalidConfig_Revert() (gas: 11112) OnRamp_setDynamicConfig:test_SetConfigOnlyOwner_Revert() (gas: 15939) OnRamp_setDynamicConfig:test_SetDynamicConfig_Success() (gas: 51996) OnRamp_withdrawFeeTokens:test_WithdrawFeeTokens_Success() (gas: 97214) -PingPong_ccipReceive:test_CcipReceive_Success() (gas: 150175) +PingPong_ccipReceive:test_CcipReceive_Success() (gas: 152675) PingPong_plumbing:test_OutOfOrderExecution_Success() (gas: 20277) PingPong_plumbing:test_Pausing_Success() (gas: 17777) PingPong_startPingPong:test_StartPingPong_With_OOO_Success() (gas: 163199) @@ -775,7 +775,7 @@ RMN_ownerUnbless:test_Unbless_Success() (gas: 74699) RMN_ownerUnvoteToCurse:test_CanBlessAndCurseAfterGlobalCurseIsLifted() (gas: 470965) RMN_ownerUnvoteToCurse:test_IsIdempotent() (gas: 397532) RMN_ownerUnvoteToCurse:test_NonOwner_Revert() (gas: 18591) -RMN_ownerUnvoteToCurse:test_OwnerUnvoteToCurseSuccess_gas() (gas: 357403) +RMN_ownerUnvoteToCurse:test_OwnerUnvoteToCurseSuccess_gas() (gas: 357400) RMN_ownerUnvoteToCurse:test_UnknownVoter_Revert() (gas: 32980) RMN_ownerUnvoteToCurse_Benchmark:test_OwnerUnvoteToCurse_1Voter_LiftsCurse_gas() (gas: 261985) RMN_permaBlessing:test_PermaBlessing() (gas: 202686) @@ -783,7 +783,7 @@ RMN_setConfig:test_BlessVoterIsZeroAddress_Revert() (gas: 15494) RMN_setConfig:test_EitherThresholdIsZero_Revert() (gas: 21095) RMN_setConfig:test_NonOwner_Revert() (gas: 14713) RMN_setConfig:test_RepeatedAddress_Revert() (gas: 18213) -RMN_setConfig:test_SetConfigSuccess_gas() (gas: 104204) +RMN_setConfig:test_SetConfigSuccess_gas() (gas: 104022) RMN_setConfig:test_TotalWeightsSmallerThanEachThreshold_Revert() (gas: 30173) RMN_setConfig:test_VoteToBlessByEjectedVoter_Revert() (gas: 130303) RMN_setConfig:test_VotersLengthIsZero_Revert() (gas: 12128) @@ -868,18 +868,18 @@ Router_getFee:test_GetFeeSupportedChain_Success() (gas: 46464) Router_getFee:test_UnsupportedDestinationChain_Revert() (gas: 17138) Router_getSupportedTokens:test_GetSupportedTokens_Revert() (gas: 10460) Router_recoverTokens:test_RecoverTokensInvalidRecipient_Revert() (gas: 11316) -Router_recoverTokens:test_RecoverTokensNoFunds_Revert() (gas: 17761) +Router_recoverTokens:test_RecoverTokensNoFunds_Revert() (gas: 20261) Router_recoverTokens:test_RecoverTokensNonOwner_Revert() (gas: 11159) Router_recoverTokens:test_RecoverTokensValueReceiver_Revert() (gas: 422138) -Router_recoverTokens:test_RecoverTokens_Success() (gas: 50437) +Router_recoverTokens:test_RecoverTokens_Success() (gas: 52437) Router_routeMessage:test_AutoExec_Success() (gas: 42684) Router_routeMessage:test_ExecutionEvent_Success() (gas: 157980) Router_routeMessage:test_ManualExec_Success() (gas: 35381) Router_routeMessage:test_OnlyOffRamp_Revert() (gas: 25116) Router_routeMessage:test_WhenNotHealthy_Revert() (gas: 44724) Router_setWrappedNative:test_OnlyOwner_Revert() (gas: 10985) -SelfFundedPingPong_ccipReceive:test_FundingIfNotANop_Revert() (gas: 53531) -SelfFundedPingPong_ccipReceive:test_Funding_Success() (gas: 417002) +SelfFundedPingPong_ccipReceive:test_FundingIfNotANop_Revert() (gas: 55531) +SelfFundedPingPong_ccipReceive:test_Funding_Success() (gas: 419502) SelfFundedPingPong_setCountIncrBeforeFunding:test_setCountIncrBeforeFunding() (gas: 20151) TokenAdminRegistry_acceptAdminRole:test_acceptAdminRole_OnlyPendingAdministrator_Revert() (gas: 51085) TokenAdminRegistry_acceptAdminRole:test_acceptAdminRole_Success() (gas: 43947) @@ -907,8 +907,8 @@ TokenPoolAndProxy:test_lockOrBurn_burnMint_Success() (gas: 6070353) TokenPoolAndProxy:test_lockOrBurn_burnWithFromMint_Success() (gas: 6101826) TokenPoolAndProxy:test_lockOrBurn_lockRelease_Success() (gas: 6319594) TokenPoolAndProxy:test_setPreviousPool_Success() (gas: 3387124) -TokenPoolAndProxyMigration:test_tokenPoolMigration_Success_1_2() (gas: 6913796) -TokenPoolAndProxyMigration:test_tokenPoolMigration_Success_1_4() (gas: 7097821) +TokenPoolAndProxyMigration:test_tokenPoolMigration_Success_1_2() (gas: 6916296) +TokenPoolAndProxyMigration:test_tokenPoolMigration_Success_1_4() (gas: 7100321) TokenPoolWithAllowList_applyAllowListUpdates:test_AllowListNotEnabled_Revert() (gas: 2209837) TokenPoolWithAllowList_applyAllowListUpdates:test_OnlyOwner_Revert() (gas: 12089) TokenPoolWithAllowList_applyAllowListUpdates:test_SetAllowListSkipsZero_Success() (gas: 23324) diff --git a/contracts/src/v0.8/ccip/test/mocks/MockRouter.sol b/contracts/src/v0.8/ccip/test/mocks/MockRouter.sol index 050cd6d4579..9181fb37c29 100644 --- a/contracts/src/v0.8/ccip/test/mocks/MockRouter.sol +++ b/contracts/src/v0.8/ccip/test/mocks/MockRouter.sol @@ -45,8 +45,19 @@ contract MockCCIPRouter is IRouter, IRouterClient { uint256 gasLimit, address receiver ) internal returns (bool success, bytes memory retData, uint256 gasUsed) { - // Only send through the router if the receiver is a contract and implements the IAny2EVMMessageReceiver interface. - if (receiver.code.length == 0 || !receiver.supportsInterface(type(IAny2EVMMessageReceiver).interfaceId)) { + // There are three cases in which we skip calling the receiver: + // 1. If the message data is empty AND the gas limit is 0. + // This indicates a message that only transfers tokens. It is valid to only send tokens to a contract + // that supports the IAny2EVMMessageReceiver interface, but without this first check we would call the + // receiver without any gas, which would revert the transaction. + // 2. If the receiver is not a contract. + // 3. If the receiver is a contract but it does not support the IAny2EVMMessageReceiver interface. + // + // The ordering of these checks is important, as the first check is the cheapest to execute. + if ( + (message.data.length == 0 && gasLimit == 0) || receiver.code.length == 0 + || !receiver.supportsInterface(type(IAny2EVMMessageReceiver).interfaceId) + ) { return (true, "", 0); } diff --git a/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go b/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go index 4e0a7162aaf..1726405e22e 100644 --- a/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go +++ b/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go @@ -590,6 +590,10 @@ func (g *TestGasEstimatorConfig) BlockHistory() evmconfig.BlockHistory { return &TestBlockHistoryConfig{} } +func (g *TestGasEstimatorConfig) FeeHistory() evmconfig.FeeHistory { + return &TestFeeHistoryConfig{} +} + func (g *TestGasEstimatorConfig) EIP1559DynamicFees() bool { return false } func (g *TestGasEstimatorConfig) LimitDefault() uint64 { return 1e6 } func (g *TestGasEstimatorConfig) BumpPercent() uint16 { return 2 } @@ -612,6 +616,7 @@ func (g *TestGasEstimatorConfig) LimitJobType() evmconfig.LimitJobType { func (g *TestGasEstimatorConfig) PriceMaxKey(addr common.Address) *assets.Wei { return assets.GWei(1) } +func (g *TestGasEstimatorConfig) EstimateLimit() bool { return false } func (e *TestEvmConfig) GasEstimator() evmconfig.GasEstimator { return &TestGasEstimatorConfig{bumpThreshold: e.BumpThreshold} @@ -637,6 +642,10 @@ func (b *TestBlockHistoryConfig) BlockHistorySize() uint16 { return 42 func (b *TestBlockHistoryConfig) EIP1559FeeCapBufferBlocks() uint16 { return 42 } func (b *TestBlockHistoryConfig) TransactionPercentile() uint16 { return 42 } +type TestFeeHistoryConfig struct { + evmconfig.FeeHistory +} + type transactionsConfig struct { evmconfig.Transactions e *TestEvmConfig diff --git a/core/chains/evm/client/chain_client.go b/core/chains/evm/client/chain_client.go index c27d294ebfd..310528424d6 100644 --- a/core/chains/evm/client/chain_client.go +++ b/core/chains/evm/client/chain_client.go @@ -84,6 +84,7 @@ type Client interface { SuggestGasPrice(ctx context.Context) (*big.Int, error) SuggestGasTipCap(ctx context.Context) (*big.Int, error) LatestBlockHeight(ctx context.Context) (*big.Int, error) + FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) HeaderByNumber(ctx context.Context, n *big.Int) (*types.Header, error) HeaderByHash(ctx context.Context, h common.Hash) (*types.Header, error) @@ -353,6 +354,14 @@ func (c *chainClient) LatestFinalizedBlock(ctx context.Context) (*evmtypes.Head, return c.multiNode.LatestFinalizedBlock(ctx) } +func (c *chainClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) { + rpc, err := c.multiNode.SelectNodeRPC() + if err != nil { + return feeHistory, err + } + return rpc.FeeHistory(ctx, blockCount, rewardPercentiles) +} + func (c *chainClient) CheckTxValidity(ctx context.Context, from common.Address, to common.Address, data []byte) *SendError { msg := ethereum.CallMsg{ From: from, diff --git a/core/chains/evm/client/errors.go b/core/chains/evm/client/errors.go index e7fff8d0dbc..563ba3da322 100644 --- a/core/chains/evm/client/errors.go +++ b/core/chains/evm/client/errors.go @@ -247,16 +247,21 @@ var zkSync = ClientErrors{ } var zkEvm = ClientErrors{ - TerminallyStuck: regexp.MustCompile(`(?:: |^)not enough .* counters to continue the execution$`), + TerminallyStuck: regexp.MustCompile(`(?:: |^)(?:not enough .* counters to continue the execution|out of counters at node level (?:.*))$`), } var aStar = ClientErrors{ TerminallyUnderpriced: regexp.MustCompile(`(?:: |^)(gas price less than block base fee)$`), } +var berachain = ClientErrors{ + Fatal: regexp.MustCompile(`(: |^)'*invalid chain ID`), +} + var mantle = ClientErrors{ InsufficientEth: regexp.MustCompile(`(: |^)'*insufficient funds for gas \* price \+ value`), Fatal: regexp.MustCompile(`(: |^)'*invalid sender`), + NonceTooLow: regexp.MustCompile(`(: |^)'*nonce too low`), } var gnosis = ClientErrors{ @@ -270,7 +275,7 @@ var internal = ClientErrors{ TerminallyStuck: regexp.MustCompile(TerminallyStuckMsg), } -var clients = []ClientErrors{parity, geth, arbitrum, metis, substrate, avalanche, nethermind, harmony, besu, erigon, klaytn, celo, zkSync, zkEvm, mantle, aStar, gnosis, internal} +var clients = []ClientErrors{parity, geth, arbitrum, metis, substrate, avalanche, nethermind, harmony, besu, erigon, klaytn, celo, zkSync, zkEvm, mantle, aStar, berachain, gnosis, internal} // ClientErrorRegexes returns a map of compiled regexes for each error type func ClientErrorRegexes(errsRegex config.ClientErrors) *ClientErrors { diff --git a/core/chains/evm/client/errors_test.go b/core/chains/evm/client/errors_test.go index bddc7dd3dbc..9f16a7a367d 100644 --- a/core/chains/evm/client/errors_test.go +++ b/core/chains/evm/client/errors_test.go @@ -44,6 +44,7 @@ func Test_Eth_Errors(t *testing.T) { {"call failed: OldNonce, Current nonce: 22, nonce of rejected tx: 17", true, "Nethermind"}, {"nonce too low. allowed nonce range: 427 - 447, actual: 426", true, "zkSync"}, {"client error nonce too low", true, "tomlConfig"}, + {"failed to forward tx to sequencer, please try again. Error message: 'nonce too low'", true, "Mantle"}, } for _, test := range tests { @@ -215,6 +216,7 @@ func Test_Eth_Errors(t *testing.T) { {"insufficient funds for gas + value. balance: 42719769622667482000, fee: 48098250000000, value: 42719769622667482000", true, "celo"}, {"client error insufficient eth", true, "tomlConfig"}, {"transaction would cause overdraft", true, "Geth"}, + {"failed to forward tx to sequencer, please try again. Error message: 'insufficient funds for gas * price + value'", true, "Mantle"}, } for _, test := range tests { err = evmclient.NewSendErrorS(test.message) @@ -316,6 +318,8 @@ func Test_Eth_Errors(t *testing.T) { {"failed to add tx to the pool: not enough step counters to continue the execution", true, "Xlayer"}, {"failed to add tx to the pool: not enough keccak counters to continue the execution", true, "zkEVM"}, {"failed to add tx to the pool: not enough keccak counters to continue the execution", true, "Xlayer"}, + {"RPC error response: failed to add tx to the pool: out of counters at node level (Steps)", true, "zkEVM"}, + {"RPC error response: failed to add tx to the pool: out of counters at node level (GasUsed, KeccakHashes, PoseidonHashes, PoseidonPaddings, MemAligns, Arithmetics, Binaries, Steps, Sha256Hashes)", true, "Xlayer"}, } for _, test := range tests { @@ -400,7 +404,11 @@ func Test_Eth_Errors_Fatal(t *testing.T) { {"Failed to serialize transaction: max priority fee per gas higher than 2^64-1", true, "zkSync"}, {"Failed to serialize transaction: oversized data. max: 1000000; actual: 1000000", true, "zkSync"}, + {"failed to forward tx to sequencer, please try again. Error message: 'invalid sender'", true, "Mantle"}, + {"client error fatal", true, "tomlConfig"}, + + {"invalid chain ID", true, "Berachain"}, } for _, test := range tests { diff --git a/core/chains/evm/client/mocks/client.go b/core/chains/evm/client/mocks/client.go index 7b5220033bc..da034d95774 100644 --- a/core/chains/evm/client/mocks/client.go +++ b/core/chains/evm/client/mocks/client.go @@ -780,6 +780,66 @@ func (_c *Client_EstimateGas_Call) RunAndReturn(run func(context.Context, ethere return _c } +// FeeHistory provides a mock function with given fields: ctx, blockCount, rewardPercentiles +func (_m *Client) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (*ethereum.FeeHistory, error) { + ret := _m.Called(ctx, blockCount, rewardPercentiles) + + if len(ret) == 0 { + panic("no return value specified for FeeHistory") + } + + var r0 *ethereum.FeeHistory + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)); ok { + return rf(ctx, blockCount, rewardPercentiles) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) *ethereum.FeeHistory); ok { + r0 = rf(ctx, blockCount, rewardPercentiles) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ethereum.FeeHistory) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, []float64) error); ok { + r1 = rf(ctx, blockCount, rewardPercentiles) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Client_FeeHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeeHistory' +type Client_FeeHistory_Call struct { + *mock.Call +} + +// FeeHistory is a helper method to define mock.On call +// - ctx context.Context +// - blockCount uint64 +// - rewardPercentiles []float64 +func (_e *Client_Expecter) FeeHistory(ctx interface{}, blockCount interface{}, rewardPercentiles interface{}) *Client_FeeHistory_Call { + return &Client_FeeHistory_Call{Call: _e.mock.On("FeeHistory", ctx, blockCount, rewardPercentiles)} +} + +func (_c *Client_FeeHistory_Call) Run(run func(ctx context.Context, blockCount uint64, rewardPercentiles []float64)) *Client_FeeHistory_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].([]float64)) + }) + return _c +} + +func (_c *Client_FeeHistory_Call) Return(feeHistory *ethereum.FeeHistory, err error) *Client_FeeHistory_Call { + _c.Call.Return(feeHistory, err) + return _c +} + +func (_c *Client_FeeHistory_Call) RunAndReturn(run func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)) *Client_FeeHistory_Call { + _c.Call.Return(run) + return _c +} + // FilterLogs provides a mock function with given fields: ctx, q func (_m *Client) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { ret := _m.Called(ctx, q) diff --git a/core/chains/evm/client/mocks/rpc_client.go b/core/chains/evm/client/mocks/rpc_client.go index 06f79efd551..5567b3f8978 100644 --- a/core/chains/evm/client/mocks/rpc_client.go +++ b/core/chains/evm/client/mocks/rpc_client.go @@ -889,6 +889,66 @@ func (_c *RPCClient_EstimateGas_Call) RunAndReturn(run func(context.Context, int return _c } +// FeeHistory provides a mock function with given fields: ctx, blockCount, rewardPercentiles +func (_m *RPCClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (*ethereum.FeeHistory, error) { + ret := _m.Called(ctx, blockCount, rewardPercentiles) + + if len(ret) == 0 { + panic("no return value specified for FeeHistory") + } + + var r0 *ethereum.FeeHistory + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)); ok { + return rf(ctx, blockCount, rewardPercentiles) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) *ethereum.FeeHistory); ok { + r0 = rf(ctx, blockCount, rewardPercentiles) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ethereum.FeeHistory) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, []float64) error); ok { + r1 = rf(ctx, blockCount, rewardPercentiles) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RPCClient_FeeHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeeHistory' +type RPCClient_FeeHistory_Call struct { + *mock.Call +} + +// FeeHistory is a helper method to define mock.On call +// - ctx context.Context +// - blockCount uint64 +// - rewardPercentiles []float64 +func (_e *RPCClient_Expecter) FeeHistory(ctx interface{}, blockCount interface{}, rewardPercentiles interface{}) *RPCClient_FeeHistory_Call { + return &RPCClient_FeeHistory_Call{Call: _e.mock.On("FeeHistory", ctx, blockCount, rewardPercentiles)} +} + +func (_c *RPCClient_FeeHistory_Call) Run(run func(ctx context.Context, blockCount uint64, rewardPercentiles []float64)) *RPCClient_FeeHistory_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].([]float64)) + }) + return _c +} + +func (_c *RPCClient_FeeHistory_Call) Return(feeHistory *ethereum.FeeHistory, err error) *RPCClient_FeeHistory_Call { + _c.Call.Return(feeHistory, err) + return _c +} + +func (_c *RPCClient_FeeHistory_Call) RunAndReturn(run func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)) *RPCClient_FeeHistory_Call { + _c.Call.Return(run) + return _c +} + // FilterEvents provides a mock function with given fields: ctx, query func (_m *RPCClient) FilterEvents(ctx context.Context, query ethereum.FilterQuery) ([]coretypes.Log, error) { ret := _m.Called(ctx, query) diff --git a/core/chains/evm/client/null_client.go b/core/chains/evm/client/null_client.go index 3129bcff9b0..5b1a4d7e1bb 100644 --- a/core/chains/evm/client/null_client.go +++ b/core/chains/evm/client/null_client.go @@ -235,3 +235,7 @@ func (nc *NullClient) LatestFinalizedBlock(_ context.Context) (*evmtypes.Head, e func (nc *NullClient) CheckTxValidity(_ context.Context, _ common.Address, _ common.Address, _ []byte) *SendError { return nil } + +func (nc *NullClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) { + return nil, nil +} diff --git a/core/chains/evm/client/rpc_client.go b/core/chains/evm/client/rpc_client.go index 07aa86fc450..295e24f7c91 100644 --- a/core/chains/evm/client/rpc_client.go +++ b/core/chains/evm/client/rpc_client.go @@ -103,6 +103,7 @@ type RPCClient interface { SuggestGasTipCap(ctx context.Context) (t *big.Int, err error) TransactionReceiptGeth(ctx context.Context, txHash common.Hash) (r *types.Receipt, err error) GetInterceptedChainInfo() (latest, highestUserObservations commonclient.ChainInfo) + FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) } const rpcSubscriptionMethodNewHeads = "newHeads" @@ -141,6 +142,7 @@ type rpcClient struct { // stateMu since it can happen on state transitions as well as rpcClient Close. chStopInFlight chan struct{} + chainInfoLock sync.RWMutex // intercepted values seen by callers of the rpcClient excluding health check calls. Need to ensure MultiNode provides repeatable read guarantee highestUserObservations commonclient.ChainInfo // most recent chain info observed during current lifecycle (reseted on DisconnectAll) @@ -336,7 +338,9 @@ func (r *rpcClient) DisconnectAll() { } r.cancelInflightRequests() r.unsubscribeAll() + r.chainInfoLock.Lock() r.latestChainInfo = commonclient.ChainInfo{} + r.chainInfoLock.Unlock() } // unsubscribeAll unsubscribes all subscriptions @@ -596,6 +600,7 @@ func (r *rpcClient) TransactionReceiptGeth(ctx context.Context, txHash common.Ha return } + func (r *rpcClient) TransactionByHash(ctx context.Context, txHash common.Hash) (tx *types.Transaction, err error) { ctx, cancel, ws, http := r.makeLiveQueryCtxAndSafeGetClients(ctx, r.rpcTimeout) defer cancel() @@ -1116,6 +1121,29 @@ func (r *rpcClient) BalanceAt(ctx context.Context, account common.Address, block return } +func (r *rpcClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) { + ctx, cancel, ws, http := r.makeLiveQueryCtxAndSafeGetClients(ctx, r.rpcTimeout) + defer cancel() + lggr := r.newRqLggr().With("blockCount", blockCount, "rewardPercentiles", rewardPercentiles) + + lggr.Debug("RPC call: evmclient.Client#FeeHistory") + start := time.Now() + if http != nil { + feeHistory, err = http.geth.FeeHistory(ctx, blockCount, nil, rewardPercentiles) + err = r.wrapHTTP(err) + } else { + feeHistory, err = ws.geth.FeeHistory(ctx, blockCount, nil, rewardPercentiles) + err = r.wrapWS(err) + } + duration := time.Since(start) + + r.logResult(lggr, err, duration, r.getRPCDomain(), "FeeHistory", + "feeHistory", feeHistory, + ) + + return +} + // CallArgs represents the data used to call the balance method of a contract. // "To" is the address of the ERC contract. "Data" is the message sent // to the contract. "From" is the sender address. @@ -1379,8 +1407,8 @@ func (r *rpcClient) onNewHead(ctx context.Context, requestCh <-chan struct{}, he return } - r.stateMu.Lock() - defer r.stateMu.Unlock() + r.chainInfoLock.Lock() + defer r.chainInfoLock.Unlock() if !commonclient.CtxIsHeathCheckRequest(ctx) { r.highestUserObservations.BlockNumber = max(r.highestUserObservations.BlockNumber, head.Number) r.highestUserObservations.TotalDifficulty = commonclient.MaxTotalDifficulty(r.highestUserObservations.TotalDifficulty, head.TotalDifficulty) @@ -1398,8 +1426,8 @@ func (r *rpcClient) onNewFinalizedHead(ctx context.Context, requestCh <-chan str if head == nil { return } - r.stateMu.Lock() - defer r.stateMu.Unlock() + r.chainInfoLock.Lock() + defer r.chainInfoLock.Unlock() if !commonclient.CtxIsHeathCheckRequest(ctx) { r.highestUserObservations.FinalizedBlockNumber = max(r.highestUserObservations.FinalizedBlockNumber, head.Number) } @@ -1412,8 +1440,8 @@ func (r *rpcClient) onNewFinalizedHead(ctx context.Context, requestCh <-chan str } func (r *rpcClient) GetInterceptedChainInfo() (latest, highestUserObservations commonclient.ChainInfo) { - r.stateMu.RLock() - defer r.stateMu.RUnlock() + r.chainInfoLock.RLock() + defer r.chainInfoLock.RUnlock() return r.latestChainInfo, r.highestUserObservations } diff --git a/core/chains/evm/client/rpc_client_test.go b/core/chains/evm/client/rpc_client_test.go index 12821880996..07e097727a3 100644 --- a/core/chains/evm/client/rpc_client_test.go +++ b/core/chains/evm/client/rpc_client_test.go @@ -7,6 +7,7 @@ import ( "fmt" "math/big" "net/url" + "sync" "testing" "time" @@ -130,6 +131,32 @@ func TestRPCClient_SubscribeNewHead(t *testing.T) { assert.Equal(t, int64(0), highestUserObservations.FinalizedBlockNumber) assert.Equal(t, (*big.Int)(nil), highestUserObservations.TotalDifficulty) }) + t.Run("Concurrent Unsubscribe and onNewHead calls do not lead to a deadlock", func(t *testing.T) { + const numberOfAttempts = 1000 // need a large number to increase the odds of reproducing the issue + server := testutils.NewWSServer(t, chainId, serverCallBack) + wsURL := server.WSURL() + + rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + defer rpc.Close() + require.NoError(t, rpc.Dial(ctx)) + var wg sync.WaitGroup + for i := 0; i < numberOfAttempts; i++ { + ch := make(chan *evmtypes.Head) + sub, err := rpc.SubscribeNewHead(tests.Context(t), ch) + require.NoError(t, err) + wg.Add(2) + go func() { + server.MustWriteBinaryMessageSync(t, makeNewHeadWSMessage(&evmtypes.Head{Number: 256, TotalDifficulty: big.NewInt(1000)})) + wg.Done() + }() + go func() { + rpc.UnsubscribeAllExceptAliveLoop() + sub.Unsubscribe() + wg.Done() + }() + wg.Wait() + } + }) t.Run("Block's chain ID matched configured", func(t *testing.T) { server := testutils.NewWSServer(t, chainId, serverCallBack) wsURL := server.WSURL() diff --git a/core/chains/evm/client/simulated_backend_client.go b/core/chains/evm/client/simulated_backend_client.go index 7dfd39f444c..3ec1bff577c 100644 --- a/core/chains/evm/client/simulated_backend_client.go +++ b/core/chains/evm/client/simulated_backend_client.go @@ -156,6 +156,10 @@ func (c *SimulatedBackendClient) LINKBalance(ctx context.Context, address common panic("not implemented") } +func (c *SimulatedBackendClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) { + panic("not implemented") +} + // TransactionReceipt returns the transaction receipt for the given transaction hash. func (c *SimulatedBackendClient) TransactionReceipt(ctx context.Context, receipt common.Hash) (*types.Receipt, error) { return c.b.TransactionReceipt(ctx, receipt) diff --git a/core/chains/evm/config/chain_scoped_gas_estimator.go b/core/chains/evm/config/chain_scoped_gas_estimator.go index 689d5e38b81..54c7c360639 100644 --- a/core/chains/evm/config/chain_scoped_gas_estimator.go +++ b/core/chains/evm/config/chain_scoped_gas_estimator.go @@ -1,6 +1,8 @@ package config import ( + "time" + gethcommon "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" @@ -36,6 +38,10 @@ func (g *gasEstimatorConfig) BlockHistory() BlockHistory { return &blockHistoryConfig{c: g.c.BlockHistory, blockDelay: g.blockDelay, bumpThreshold: g.c.BumpThreshold} } +func (g *gasEstimatorConfig) FeeHistory() FeeHistory { + return &feeHistoryConfig{c: g.c.FeeHistory} +} + func (g *gasEstimatorConfig) EIP1559DynamicFees() bool { return *g.c.EIP1559DynamicFees } @@ -108,6 +114,10 @@ func (g *gasEstimatorConfig) LimitJobType() LimitJobType { return &limitJobTypeConfig{c: g.c.LimitJobType} } +func (g *gasEstimatorConfig) EstimateLimit() bool { + return *g.c.EstimateLimit +} + type limitJobTypeConfig struct { c toml.GasLimitJobType } @@ -172,3 +182,11 @@ func (b *blockHistoryConfig) TransactionPercentile() uint16 { func (b *blockHistoryConfig) BlockDelay() uint16 { return *b.blockDelay } + +type feeHistoryConfig struct { + c toml.FeeHistoryEstimator +} + +func (u *feeHistoryConfig) CacheTimeout() time.Duration { + return u.c.CacheTimeout.Duration() +} diff --git a/core/chains/evm/config/chaintype/chaintype.go b/core/chains/evm/config/chaintype/chaintype.go index 07ea6206241..35dd214b1f5 100644 --- a/core/chains/evm/config/chaintype/chaintype.go +++ b/core/chains/evm/config/chaintype/chaintype.go @@ -14,6 +14,7 @@ const ( ChainGnosis ChainType = "gnosis" ChainHedera ChainType = "hedera" ChainKroma ChainType = "kroma" + ChainMantle ChainType = "mantle" ChainMetis ChainType = "metis" ChainOptimismBedrock ChainType = "optimismBedrock" ChainScroll ChainType = "scroll" @@ -37,7 +38,7 @@ func (c ChainType) IsL2() bool { func (c ChainType) IsValid() bool { switch c { - case "", ChainArbitrum, ChainAstar, ChainCelo, ChainGnosis, ChainHedera, ChainKroma, ChainMetis, ChainOptimismBedrock, ChainScroll, ChainWeMix, ChainXLayer, ChainZkEvm, ChainZkSync: + case "", ChainArbitrum, ChainAstar, ChainCelo, ChainGnosis, ChainHedera, ChainKroma, ChainMantle, ChainMetis, ChainOptimismBedrock, ChainScroll, ChainWeMix, ChainXLayer, ChainZkEvm, ChainZkSync: return true } return false @@ -57,6 +58,8 @@ func ChainTypeFromSlug(slug string) ChainType { return ChainHedera case "kroma": return ChainKroma + case "mantle": + return ChainMantle case "metis": return ChainMetis case "optimismBedrock": @@ -129,6 +132,7 @@ var ErrInvalidChainType = fmt.Errorf("must be one of %s or omitted", strings.Joi string(ChainGnosis), string(ChainHedera), string(ChainKroma), + string(ChainMantle), string(ChainMetis), string(ChainOptimismBedrock), string(ChainScroll), diff --git a/core/chains/evm/config/config.go b/core/chains/evm/config/config.go index 3ccdfeea8b8..3d00fe86a43 100644 --- a/core/chains/evm/config/config.go +++ b/core/chains/evm/config/config.go @@ -117,6 +117,7 @@ type AutoPurgeConfig interface { type GasEstimator interface { BlockHistory() BlockHistory + FeeHistory() FeeHistory LimitJobType() LimitJobType EIP1559DynamicFees() bool @@ -136,6 +137,7 @@ type GasEstimator interface { PriceMin() *assets.Wei Mode() string PriceMaxKey(gethcommon.Address) *assets.Wei + EstimateLimit() bool } type LimitJobType interface { @@ -157,6 +159,10 @@ type BlockHistory interface { TransactionPercentile() uint16 } +type FeeHistory interface { + CacheTimeout() time.Duration +} + type Workflow interface { FromAddress() *types.EIP55Address ForwarderAddress() *types.EIP55Address diff --git a/core/chains/evm/config/config_test.go b/core/chains/evm/config/config_test.go index 617a1605d85..e0dec00e688 100644 --- a/core/chains/evm/config/config_test.go +++ b/core/chains/evm/config/config_test.go @@ -220,6 +220,13 @@ func TestChainScopedConfig_BlockHistory(t *testing.T) { assert.Equal(t, uint16(1), bh.BlockDelay()) assert.Equal(t, uint16(4), bh.EIP1559FeeCapBufferBlocks()) } +func TestChainScopedConfig_FeeHistory(t *testing.T) { + t.Parallel() + cfg := testutils.NewTestChainScopedConfig(t, nil) + + u := cfg.EVM().GasEstimator().FeeHistory() + assert.Equal(t, 10*time.Second, u.CacheTimeout()) +} func TestChainScopedConfig_GasEstimator(t *testing.T) { t.Parallel() @@ -243,6 +250,7 @@ func TestChainScopedConfig_GasEstimator(t *testing.T) { assert.Equal(t, assets.GWei(100), ge.FeeCapDefault()) assert.Equal(t, assets.NewWeiI(1), ge.TipCapDefault()) assert.Equal(t, assets.NewWeiI(1), ge.TipCapMin()) + assert.Equal(t, false, ge.EstimateLimit()) } func TestChainScopedConfig_BSCDefaults(t *testing.T) { diff --git a/core/chains/evm/config/mocks/gas_estimator.go b/core/chains/evm/config/mocks/gas_estimator.go index b8e813e8068..70b9c18d0bf 100644 --- a/core/chains/evm/config/mocks/gas_estimator.go +++ b/core/chains/evm/config/mocks/gas_estimator.go @@ -298,6 +298,51 @@ func (_c *GasEstimator_EIP1559DynamicFees_Call) RunAndReturn(run func() bool) *G return _c } +// EstimateLimit provides a mock function with given fields: +func (_m *GasEstimator) EstimateLimit() bool { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for EstimateLimit") + } + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// GasEstimator_EstimateLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EstimateLimit' +type GasEstimator_EstimateLimit_Call struct { + *mock.Call +} + +// EstimateLimit is a helper method to define mock.On call +func (_e *GasEstimator_Expecter) EstimateLimit() *GasEstimator_EstimateLimit_Call { + return &GasEstimator_EstimateLimit_Call{Call: _e.mock.On("EstimateLimit")} +} + +func (_c *GasEstimator_EstimateLimit_Call) Run(run func()) *GasEstimator_EstimateLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *GasEstimator_EstimateLimit_Call) Return(_a0 bool) *GasEstimator_EstimateLimit_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *GasEstimator_EstimateLimit_Call) RunAndReturn(run func() bool) *GasEstimator_EstimateLimit_Call { + _c.Call.Return(run) + return _c +} + // FeeCapDefault provides a mock function with given fields: func (_m *GasEstimator) FeeCapDefault() *assets.Wei { ret := _m.Called() @@ -345,6 +390,53 @@ func (_c *GasEstimator_FeeCapDefault_Call) RunAndReturn(run func() *assets.Wei) return _c } +// FeeHistory provides a mock function with given fields: +func (_m *GasEstimator) FeeHistory() config.FeeHistory { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for FeeHistory") + } + + var r0 config.FeeHistory + if rf, ok := ret.Get(0).(func() config.FeeHistory); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(config.FeeHistory) + } + } + + return r0 +} + +// GasEstimator_FeeHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeeHistory' +type GasEstimator_FeeHistory_Call struct { + *mock.Call +} + +// FeeHistory is a helper method to define mock.On call +func (_e *GasEstimator_Expecter) FeeHistory() *GasEstimator_FeeHistory_Call { + return &GasEstimator_FeeHistory_Call{Call: _e.mock.On("FeeHistory")} +} + +func (_c *GasEstimator_FeeHistory_Call) Run(run func()) *GasEstimator_FeeHistory_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *GasEstimator_FeeHistory_Call) Return(_a0 config.FeeHistory) *GasEstimator_FeeHistory_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *GasEstimator_FeeHistory_Call) RunAndReturn(run func() config.FeeHistory) *GasEstimator_FeeHistory_Call { + _c.Call.Return(run) + return _c +} + // LimitDefault provides a mock function with given fields: func (_m *GasEstimator) LimitDefault() uint64 { ret := _m.Called() diff --git a/core/chains/evm/config/toml/config.go b/core/chains/evm/config/toml/config.go index ac7841ac497..90854d90cf9 100644 --- a/core/chains/evm/config/toml/config.go +++ b/core/chains/evm/config/toml/config.go @@ -554,6 +554,7 @@ type GasEstimator struct { LimitMultiplier *decimal.Decimal LimitTransfer *uint64 LimitJobType GasLimitJobType `toml:",omitempty"` + EstimateLimit *bool BumpMin *assets.Wei BumpPercent *uint16 @@ -567,6 +568,7 @@ type GasEstimator struct { TipCapMin *assets.Wei BlockHistory BlockHistoryEstimator `toml:",omitempty"` + FeeHistory FeeHistoryEstimator `toml:",omitempty"` } func (e *GasEstimator) ValidateConfig() (err error) { @@ -641,6 +643,9 @@ func (e *GasEstimator) setFrom(f *GasEstimator) { if v := f.LimitTransfer; v != nil { e.LimitTransfer = v } + if v := f.EstimateLimit; v != nil { + e.EstimateLimit = v + } if v := f.PriceDefault; v != nil { e.PriceDefault = v } @@ -658,6 +663,7 @@ func (e *GasEstimator) setFrom(f *GasEstimator) { } e.LimitJobType.setFrom(&f.LimitJobType) e.BlockHistory.setFrom(&f.BlockHistory) + e.FeeHistory.setFrom(&f.FeeHistory) } type GasLimitJobType struct { @@ -720,6 +726,16 @@ func (e *BlockHistoryEstimator) setFrom(f *BlockHistoryEstimator) { } } +type FeeHistoryEstimator struct { + CacheTimeout *commonconfig.Duration +} + +func (u *FeeHistoryEstimator) setFrom(f *FeeHistoryEstimator) { + if v := f.CacheTimeout; v != nil { + u.CacheTimeout = v + } +} + type KeySpecificConfig []KeySpecific func (ks KeySpecificConfig) ValidateConfig() (err error) { diff --git a/core/chains/evm/config/toml/defaults/Astar_Mainnet.toml b/core/chains/evm/config/toml/defaults/Astar_Mainnet.toml index 2d356d95649..d1697d137a3 100644 --- a/core/chains/evm/config/toml/defaults/Astar_Mainnet.toml +++ b/core/chains/evm/config/toml/defaults/Astar_Mainnet.toml @@ -1,6 +1,6 @@ ChainID = '592' -# FinalityTag will be enabled once the custom tag is implemented -# FinalityTagEnabled = true +ChainType = 'astar' +FinalityTagEnabled = true FinalityDepth = 100 LogPollInterval = '6s' diff --git a/core/chains/evm/config/toml/defaults/BOB_Testnet.toml b/core/chains/evm/config/toml/defaults/BOB_Testnet.toml new file mode 100644 index 00000000000..f749e5e7295 --- /dev/null +++ b/core/chains/evm/config/toml/defaults/BOB_Testnet.toml @@ -0,0 +1,12 @@ +ChainID = '808813' +FinalityDepth = 900 # compat test finality_depth: ~850 +LogPollInterval = '5s' # compat test block_time: ~0.5s + +NoNewFinalizedHeadsThreshold = '10m' # with 5s threw PLENTY of [RPC's finalized state is out of sync; no new finalized heads received for 5s (last finalized head received was 4939141)] + +FinalityTagEnabled = true #compat test get_logs>supports_finalized_tag: returned true + +[GasEstimator] +EIP1559DynamicFees = true +Mode = 'FeeHistory' + diff --git a/core/chains/evm/config/toml/defaults/Berachain_Testnet.toml b/core/chains/evm/config/toml/defaults/Berachain_Testnet.toml new file mode 100644 index 00000000000..ed71c509150 --- /dev/null +++ b/core/chains/evm/config/toml/defaults/Berachain_Testnet.toml @@ -0,0 +1,12 @@ +ChainID = '80084' +FinalityDepth = 10 # compat test finality_depth: 1 +LogPollInterval = '5s' # compat test block_time: ~1.7s + +NoNewFinalizedHeadsThreshold = '10m' # with 5s threw PLENTY of [RPC's finalized state is out of sync; no new finalized heads received for 5s (last finalized head received was 4939141)] + +FinalityTagEnabled = true #compat test get_logs>supports_finalized_tag: returned true + +[GasEstimator] +EIP1559DynamicFees = true +Mode = 'FeeHistory' + diff --git a/core/chains/evm/config/toml/defaults/Bsquared_Testnet.toml b/core/chains/evm/config/toml/defaults/Bsquared_Testnet.toml new file mode 100644 index 00000000000..2df06dafd07 --- /dev/null +++ b/core/chains/evm/config/toml/defaults/Bsquared_Testnet.toml @@ -0,0 +1,12 @@ +ChainID = '1123' +FinalityDepth = 2000 # compat test finality_depth: ~1900 +LogPollInterval = '5s' # compat test block_time: ~2s + +NoNewFinalizedHeadsThreshold = '10m' # with 10m threw PLENTY of [RPC's finalized state is out of sync; no new finalized heads received for 10m0s (last finalized head received was 3100170)] + +FinalityTagEnabled = true #compat test get_logs>supports_finalized_tag: returned true + +[GasEstimator] +EIP1559DynamicFees = true +Mode = 'FeeHistory' + diff --git a/core/chains/evm/config/toml/defaults/Metis_Mainnet.toml b/core/chains/evm/config/toml/defaults/Metis_Mainnet.toml index f057400d014..388b9f00b09 100644 --- a/core/chains/evm/config/toml/defaults/Metis_Mainnet.toml +++ b/core/chains/evm/config/toml/defaults/Metis_Mainnet.toml @@ -1,6 +1,6 @@ # Metis is an L2 chain based on Optimism. ChainID = '1088' -ChainType = 'metis' +ChainType = 'optimismBedrock' # Sequencer offers absolute finality FinalityDepth = 10 FinalityTagEnabled = true diff --git a/core/chains/evm/config/toml/defaults/Metis_Sepolia.toml b/core/chains/evm/config/toml/defaults/Metis_Sepolia.toml index 286b888e1a8..4ff4056c75d 100644 --- a/core/chains/evm/config/toml/defaults/Metis_Sepolia.toml +++ b/core/chains/evm/config/toml/defaults/Metis_Sepolia.toml @@ -1,5 +1,5 @@ ChainID = '59902' -ChainType = 'metis' +ChainType = 'optimismBedrock' FinalityDepth = 10 FinalityTagEnabled = true MinIncomingConfirmations = 1 diff --git a/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Cardona.toml b/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Cardona.toml index cd91465dae6..46ce80e29fc 100644 --- a/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Cardona.toml +++ b/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Cardona.toml @@ -13,12 +13,14 @@ ContractConfirmations = 1 ResendAfterThreshold = '3m' [GasEstimator] -PriceMin = '1 mwei' +Mode = 'FeeHistory' +# The FeeHistory estimator does not enforce PriceMin, setting it to 0 to not place any limits on the price +PriceMin = '0' BumpPercent = 40 -BumpMin = '20 mwei' -[GasEstimator.BlockHistory] -BlockHistorySize = 12 +[GasEstimator.FeeHistory] +# Refresh the suggested price every 4 seconds, to stay slightly below their polling rate of 5s +CacheTimeout = '4s' [HeadTracker] HistoryDepth = 2000 diff --git a/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Goerli.toml b/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Goerli.toml deleted file mode 100644 index 6a9b47190fd..00000000000 --- a/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Goerli.toml +++ /dev/null @@ -1,24 +0,0 @@ -ChainID = '1442' -ChainType = 'zkevm' -FinalityDepth = 500 -NoNewHeadsThreshold = '12m' -MinIncomingConfirmations = 1 -LogPollInterval = '30s' -RPCDefaultBatchSize = 100 - -[OCR] -ContractConfirmations = 1 - -[Transactions] -ResendAfterThreshold = '3m' - -[GasEstimator] -PriceMin = '50 mwei' -BumpPercent = 40 -BumpMin = '20 mwei' - -[GasEstimator.BlockHistory] -BlockHistorySize = 12 - -[HeadTracker] -HistoryDepth = 2000 diff --git a/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Mainnet.toml b/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Mainnet.toml index 79e0cb0fce5..2fef7874d17 100644 --- a/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Mainnet.toml +++ b/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Mainnet.toml @@ -14,12 +14,14 @@ ContractConfirmations = 1 ResendAfterThreshold = '3m' [GasEstimator] -PriceMin = '100 mwei' +Mode = 'FeeHistory' +# The FeeHistory estimator does not enforce PriceMin, setting it to 0 to not place any limits on the price +PriceMin = '0' BumpPercent = 40 -BumpMin = '100 mwei' -[GasEstimator.BlockHistory] -BlockHistorySize = 12 +[GasEstimator.FeeHistory] +# Refresh the suggested price every 4 seconds, to stay slightly below their polling rate of 5s +CacheTimeout = '4s' [HeadTracker] # Polygon suffers from a tremendous number of re-orgs, we need to set this to something very large to be conservative enough diff --git a/core/chains/evm/config/toml/defaults/Unichain_Testnet.toml b/core/chains/evm/config/toml/defaults/Unichain_Testnet.toml new file mode 100644 index 00000000000..d36e9219189 --- /dev/null +++ b/core/chains/evm/config/toml/defaults/Unichain_Testnet.toml @@ -0,0 +1,11 @@ +ChainID = '1301' +FinalityDepth = 2000 # compat test finality_depth: ~1900 +LogPollInterval = '5s' # compat test block_time: ~1s + +NoNewFinalizedHeadsThreshold = '10m' # not sure what to put here, from Worldchain set to 5s threw a huge amount of errors + +FinalityTagEnabled = true #compat test get_logs>supports_finalized_tag: returned true + +[GasEstimator] +EIP1559DynamicFees = true +Mode = 'FeeHistory' \ No newline at end of file diff --git a/core/chains/evm/config/toml/defaults/Worldchain_Testnet.toml b/core/chains/evm/config/toml/defaults/Worldchain_Testnet.toml new file mode 100644 index 00000000000..fb5a6d0bd93 --- /dev/null +++ b/core/chains/evm/config/toml/defaults/Worldchain_Testnet.toml @@ -0,0 +1,11 @@ +ChainID = '4801' +FinalityDepth = 2500 # compat test finality_depth: ~2400, but it also threw 4000 in other round, which is it? +LogPollInterval = '5s' # compat test block_time: ~0.5s + +NoNewFinalizedHeadsThreshold = '10m' # with 5s threw PLENTY of [RPC's finalized state is out of sync; no new finalized heads received for 5s (last finalized head received was 4939141)] + +FinalityTagEnabled = true #compat test get_logs>supports_finalized_tag: returned true + +[GasEstimator] +EIP1559DynamicFees = true +Mode = 'FeeHistory' diff --git a/core/chains/evm/config/toml/defaults/Zircuit_Mainnet.toml b/core/chains/evm/config/toml/defaults/Zircuit_Mainnet.toml new file mode 100644 index 00000000000..ec336d3efed --- /dev/null +++ b/core/chains/evm/config/toml/defaults/Zircuit_Mainnet.toml @@ -0,0 +1,36 @@ +ChainID = '48900' +ChainType = 'optimismBedrock' +FinalityTagEnabled = true +FinalityDepth = 1000 +LinkContractAddress = '0x5D6d033B4FbD2190D99D930719fAbAcB64d2439a' +LogPollInterval = "2s" +NoNewHeadsThreshold = "40s" +NoNewFinalizedHeadsThreshold = "15m" + +[GasEstimator] +EIP1559DynamicFees = true +PriceMin = '1 wei' +BumpMin = '100 wei' + +[GasEstimator.BlockHistory] +BlockHistorySize = 24 + +[Transactions] +ResendAfterThreshold = '30s' + +[HeadTracker] +HistoryDepth = 2000 + +[NodePool] +SyncThreshold = 10 + +[OCR] +ContractConfirmations = 1 + +[OCR2.Automation] +GasLimit = 6500000 + +[Transactions.AutoPurge] +Enabled = true +Threshold = 90 # 90 blocks at 2s block time ~3 minutes +MinAttempts = 3 \ No newline at end of file diff --git a/core/chains/evm/config/toml/defaults/Zircuit_Sepolia.toml b/core/chains/evm/config/toml/defaults/Zircuit_Sepolia.toml new file mode 100644 index 00000000000..d6934d533dc --- /dev/null +++ b/core/chains/evm/config/toml/defaults/Zircuit_Sepolia.toml @@ -0,0 +1,36 @@ +ChainID = '48899' +ChainType = 'optimismBedrock' +FinalityTagEnabled = true +FinalityDepth = 1000 +LinkContractAddress = '0xDEE94506570cA186BC1e3516fCf4fd719C312cCD' +LogPollInterval = "2s" +NoNewHeadsThreshold = "40s" +NoNewFinalizedHeadsThreshold = "15m" + +[GasEstimator] +EIP1559DynamicFees = true +PriceMin = '1 wei' +BumpMin = '100 wei' + +[GasEstimator.BlockHistory] +BlockHistorySize = 60 + +[Transactions] +ResendAfterThreshold = '30s' + +[HeadTracker] +HistoryDepth = 2000 + +[NodePool] +SyncThreshold = 10 + +[OCR] +ContractConfirmations = 1 + +[OCR2.Automation] +GasLimit = 6500000 + +[Transactions.AutoPurge] +Enabled = true +Threshold = 90 # 90 blocks at 2s block time ~3 minutes +MinAttempts = 3 \ No newline at end of file diff --git a/core/chains/evm/config/toml/defaults/fallback.toml b/core/chains/evm/config/toml/defaults/fallback.toml index e3136323f6f..fb8eed39499 100644 --- a/core/chains/evm/config/toml/defaults/fallback.toml +++ b/core/chains/evm/config/toml/defaults/fallback.toml @@ -48,6 +48,7 @@ EIP1559DynamicFees = false FeeCapDefault = '100 gwei' TipCapDefault = '1' TipCapMin = '1' +EstimateLimit = false [GasEstimator.BlockHistory] BatchSize = 25 @@ -56,6 +57,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 diff --git a/core/chains/evm/gas/fee_history_estimator.go b/core/chains/evm/gas/fee_history_estimator.go new file mode 100644 index 00000000000..ba3192be100 --- /dev/null +++ b/core/chains/evm/gas/fee_history_estimator.go @@ -0,0 +1,440 @@ +package gas + +import ( + "context" + "fmt" + "math/big" + "strconv" + "sync" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + bigmath "github.com/smartcontractkit/chainlink-common/pkg/utils/big_math" + + commonfee "github.com/smartcontractkit/chainlink/v2/common/fee" + feetypes "github.com/smartcontractkit/chainlink/v2/common/fee/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" +) + +// metrics are thread safe +var ( + promFeeHistoryEstimatorGasPrice = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "gas_price_updater", + Help: "Sets latest gas price (in Wei)", + }, + []string{"evmChainID"}, + ) + promFeeHistoryEstimatorBaseFee = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "base_fee_updater", + Help: "Sets latest BaseFee (in Wei)", + }, + []string{"evmChainID"}, + ) + promFeeHistoryEstimatorMaxPriorityFeePerGas = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "max_priority_fee_per_gas_updater", + Help: "Sets latest MaxPriorityFeePerGas (in Wei)", + }, + []string{"evmChainID"}, + ) + promFeeHistoryEstimatorMaxFeePerGas = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "max_fee_per_gas_updater", + Help: "Sets latest MaxFeePerGas (in Wei)", + }, + []string{"evmChainID"}, + ) +) + +const ( + MinimumBumpPercentage = 10 // based on geth's spec + ConnectivityPercentile = 85 + BaseFeeBufferPercentage = 40 +) + +type FeeHistoryEstimatorConfig struct { + BumpPercent uint16 + CacheTimeout time.Duration + + EIP1559 bool + BlockHistorySize uint64 + RewardPercentile float64 +} + +type feeHistoryEstimatorClient interface { + SuggestGasPrice(ctx context.Context) (*big.Int, error) + FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) +} + +type FeeHistoryEstimator struct { + services.StateMachine + + client feeHistoryEstimatorClient + logger logger.Logger + config FeeHistoryEstimatorConfig + chainID *big.Int + + gasPriceMu sync.RWMutex + gasPrice *assets.Wei + + dynamicPriceMu sync.RWMutex + dynamicPrice DynamicFee + + priorityFeeThresholdMu sync.RWMutex + priorityFeeThreshold *assets.Wei + + l1Oracle rollups.L1Oracle + + wg *sync.WaitGroup + stopCh services.StopChan + refreshCh chan struct{} +} + +func NewFeeHistoryEstimator(lggr logger.Logger, client feeHistoryEstimatorClient, cfg FeeHistoryEstimatorConfig, chainID *big.Int, l1Oracle rollups.L1Oracle) *FeeHistoryEstimator { + return &FeeHistoryEstimator{ + client: client, + logger: logger.Named(lggr, "FeeHistoryEstimator"), + config: cfg, + chainID: chainID, + l1Oracle: l1Oracle, + wg: new(sync.WaitGroup), + stopCh: make(chan struct{}), + refreshCh: make(chan struct{}), + } +} + +func (f *FeeHistoryEstimator) Start(context.Context) error { + return f.StartOnce("FeeHistoryEstimator", func() error { + if f.config.BumpPercent < MinimumBumpPercentage { + return fmt.Errorf("BumpPercent: %s is less than minimum allowed percentage: %s", + strconv.FormatUint(uint64(f.config.BumpPercent), 10), strconv.Itoa(MinimumBumpPercentage)) + } + if f.config.EIP1559 && f.config.RewardPercentile > ConnectivityPercentile { + return fmt.Errorf("RewardPercentile: %s is greater than maximum allowed percentile: %s", + strconv.FormatUint(uint64(f.config.RewardPercentile), 10), strconv.Itoa(ConnectivityPercentile)) + } + f.wg.Add(1) + go f.run() + + return nil + }) +} + +func (f *FeeHistoryEstimator) Close() error { + return f.StopOnce("FeeHistoryEstimator", func() error { + close(f.stopCh) + f.wg.Wait() + return nil + }) +} + +func (f *FeeHistoryEstimator) run() { + defer f.wg.Done() + + t := services.TickerConfig{ + JitterPct: services.DefaultJitter, + }.NewTicker(f.config.CacheTimeout) + + for { + select { + case <-f.stopCh: + return + case <-f.refreshCh: + t.Reset() + case <-t.C: + if f.config.EIP1559 { + if err := f.RefreshDynamicPrice(); err != nil { + f.logger.Error(err) + } + } else { + if _, err := f.RefreshGasPrice(); err != nil { + f.logger.Error(err) + } + } + } + } +} + +// GetLegacyGas will fetch the cached gas price value. +func (f *FeeHistoryEstimator) GetLegacyGas(ctx context.Context, _ []byte, gasLimit uint64, maxPrice *assets.Wei, opts ...feetypes.Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint64, err error) { + chainSpecificGasLimit = gasLimit + if gasPrice, err = f.getGasPrice(); err != nil { + return + } + + if gasPrice.Cmp(maxPrice) > 0 { + f.logger.Warnf("estimated gas price: %s is greater than the maximum gas price configured: %s, returning the maximum price instead.", gasPrice, maxPrice) + return maxPrice, chainSpecificGasLimit, nil + } + return +} + +// RefreshGasPrice will use eth_gasPrice to fetch and cache the latest gas price from the RPC. +func (f *FeeHistoryEstimator) RefreshGasPrice() (*assets.Wei, error) { + ctx, cancel := f.stopCh.CtxCancel(evmclient.ContextWithDefaultTimeout()) + defer cancel() + + gasPrice, err := f.client.SuggestGasPrice(ctx) + if err != nil { + return nil, err + } + + promFeeHistoryEstimatorGasPrice.WithLabelValues(f.chainID.String()).Set(float64(gasPrice.Int64())) + + gasPriceWei := assets.NewWei(gasPrice) + + f.logger.Debugf("fetched new gas price: %v", gasPriceWei) + + f.gasPriceMu.Lock() + defer f.gasPriceMu.Unlock() + f.gasPrice = gasPriceWei + return f.gasPrice, nil +} + +func (f *FeeHistoryEstimator) getGasPrice() (*assets.Wei, error) { + f.gasPriceMu.RLock() + defer f.gasPriceMu.RUnlock() + if f.gasPrice == nil { + return f.gasPrice, fmt.Errorf("gas price not set") + } + return f.gasPrice, nil +} + +// GetDynamicFee will fetch the cached dynamic prices. +func (f *FeeHistoryEstimator) GetDynamicFee(ctx context.Context, maxPrice *assets.Wei) (fee DynamicFee, err error) { + if fee, err = f.getDynamicPrice(); err != nil { + return + } + + if fee.FeeCap.Cmp(maxPrice) > 0 { + f.logger.Warnf("estimated maxFeePerGas: %v is greater than the maximum price configured: %v, returning the maximum price instead.", + fee.FeeCap, maxPrice) + fee.FeeCap = maxPrice + if fee.TipCap.Cmp(maxPrice) > 0 { + f.logger.Warnf("estimated maxPriorityFeePerGas: %v is greater than the maximum price configured: %v, returning the maximum price instead.", + fee.TipCap, maxPrice) + fee.TipCap = maxPrice + } + } + + return +} + +// RefreshDynamicPrice uses eth_feeHistory to fetch the baseFee of the next block and the Nth maxPriorityFeePerGas percentiles +// of the past X blocks. It also fetches the highest 85th maxPriorityFeePerGas percentile of the past X blocks, which represents +// the highest percentile we're willing to pay. A buffer is added on top of the latest baseFee to catch fluctuations in the next +// blocks. On Ethereum the increase is baseFee * 1.125 per block, however in some chains that may vary. +func (f *FeeHistoryEstimator) RefreshDynamicPrice() error { + ctx, cancel := f.stopCh.CtxCancel(evmclient.ContextWithDefaultTimeout()) + defer cancel() + + // RewardPercentile will be used for maxPriorityFeePerGas estimations and connectivityPercentile to set the highest threshold for bumping. + feeHistory, err := f.client.FeeHistory(ctx, max(f.config.BlockHistorySize, 1), []float64{f.config.RewardPercentile, ConnectivityPercentile}) + if err != nil { + return err + } + + // eth_feeHistory doesn't return the latest baseFee of the range but rather the latest + 1, because it can be derived from the existing + // values. Source: https://github.com/ethereum/go-ethereum/blob/b0f66e34ca2a4ea7ae23475224451c8c9a569826/eth/gasprice/feehistory.go#L235 + // nextBlock is the latest returned + 1 to be aligned with the base fee value. + nextBaseFee := assets.NewWei(feeHistory.BaseFee[len(feeHistory.BaseFee)-1]) + nextBlock := big.NewInt(0).Add(feeHistory.OldestBlock, big.NewInt(int64(f.config.BlockHistorySize))) + + // If BlockHistorySize is 0 it means priority fees will be ignored from the calculations, so we set them to 0. + // If it's not we exclude 0 priced priority fees from the RPC response, even though some networks allow them. For empty blocks, eth_feeHistory + // returns priority fees with 0 values so it's safer to discard them in order to pick values from a more representative sample. + maxPriorityFeePerGas := assets.NewWeiI(0) + priorityFeeThresholdWei := assets.NewWeiI(0) + if f.config.BlockHistorySize > 0 { + var nonZeroRewardsLen int64 = 0 + priorityFee := big.NewInt(0) + priorityFeeThreshold := big.NewInt(0) + for _, reward := range feeHistory.Reward { + // reward needs to have values for two percentiles + if len(reward) < 2 { + return fmt.Errorf("reward size incorrect: %d", len(reward)) + } + // We'll calculate the average of non-zero priority fees + if reward[0].Cmp(big.NewInt(0)) > 0 { + priorityFee = priorityFee.Add(priorityFee, reward[0]) + nonZeroRewardsLen += 1 + } + // We take the max value for the bumping threshold + if reward[1].Cmp(big.NewInt(0)) > 0 { + priorityFeeThreshold = bigmath.Max(priorityFeeThreshold, reward[1]) + } + } + + if nonZeroRewardsLen == 0 || priorityFeeThreshold.Cmp(big.NewInt(0)) == 0 { + return nil + } + priorityFeeThresholdWei = assets.NewWei(priorityFeeThreshold) + maxPriorityFeePerGas = assets.NewWei(priorityFee.Div(priorityFee, big.NewInt(nonZeroRewardsLen))) + } + // baseFeeBufferPercentage is added on top as a safety to catch fluctuations in the next blocks. + maxFeePerGas := nextBaseFee.AddPercentage(BaseFeeBufferPercentage).Add(maxPriorityFeePerGas) + + promFeeHistoryEstimatorBaseFee.WithLabelValues(f.chainID.String()).Set(float64(nextBaseFee.Int64())) + promFeeHistoryEstimatorMaxPriorityFeePerGas.WithLabelValues(f.chainID.String()).Set(float64(maxPriorityFeePerGas.Int64())) + promFeeHistoryEstimatorMaxFeePerGas.WithLabelValues(f.chainID.String()).Set(float64(maxFeePerGas.Int64())) + + f.logger.Debugf("Fetched new dynamic prices, nextBlock#: %v - oldestBlock#: %v - nextBaseFee: %v - maxFeePerGas: %v - maxPriorityFeePerGas: %v - maxPriorityFeeThreshold: %v", + nextBlock, feeHistory.OldestBlock, nextBaseFee, maxFeePerGas, maxPriorityFeePerGas, priorityFeeThresholdWei) + + f.priorityFeeThresholdMu.Lock() + f.priorityFeeThreshold = priorityFeeThresholdWei + f.priorityFeeThresholdMu.Unlock() + + f.dynamicPriceMu.Lock() + defer f.dynamicPriceMu.Unlock() + f.dynamicPrice.FeeCap = maxFeePerGas + f.dynamicPrice.TipCap = maxPriorityFeePerGas + return nil +} + +func (f *FeeHistoryEstimator) getDynamicPrice() (fee DynamicFee, err error) { + f.dynamicPriceMu.RLock() + defer f.dynamicPriceMu.RUnlock() + if f.dynamicPrice.FeeCap == nil || f.dynamicPrice.TipCap == nil { + return fee, fmt.Errorf("dynamic price not set") + } + return f.dynamicPrice, nil +} + +// BumpLegacyGas provides a bumped gas price value by bumping the previous one by BumpPercent. +// If the original value is higher than the max price it returns an error as there is no room for bumping. +// It aggregates the market, bumped, and max gas price to provide a correct value. +func (f *FeeHistoryEstimator) BumpLegacyGas(ctx context.Context, originalGasPrice *assets.Wei, gasLimit uint64, maxPrice *assets.Wei, _ []EvmPriorAttempt) (*assets.Wei, uint64, error) { + // Sanitize original fee input + if originalGasPrice == nil || originalGasPrice.Cmp(maxPrice) >= 0 { + return nil, 0, fmt.Errorf("%w: error while retrieving original gas price: originalGasPrice: %s. Maximum price configured: %s", + commonfee.ErrBump, originalGasPrice, maxPrice) + } + + currentGasPrice, err := f.RefreshGasPrice() + if err != nil { + return nil, 0, err + } + f.IfStarted(func() { f.refreshCh <- struct{}{} }) + + bumpedGasPrice := originalGasPrice.AddPercentage(f.config.BumpPercent) + bumpedGasPrice, err = LimitBumpedFee(originalGasPrice, currentGasPrice, bumpedGasPrice, maxPrice) + if err != nil { + return nil, 0, fmt.Errorf("failed to limit gas price: %w", err) + } + + f.logger.Debugw("bumped gas price", "originalGasPrice", originalGasPrice, "marketGasPrice", currentGasPrice, "bumpedGasPrice", bumpedGasPrice) + + return bumpedGasPrice, gasLimit, nil +} + +// BumpDynamicFee provides a bumped dynamic fee by bumping the previous one by BumpPercent. +// If the original values are higher than the max price it returns an error as there is no room for bumping. If maxPriorityFeePerGas is bumped +// above the priority fee threshold then there is a good chance there is a connectivity issue and we shouldn't bump. +// Both maxFeePerGas as well as maxPriorityFeePerGas need to be bumped otherwise the RPC won't accept the transaction and throw an error. +// See: https://github.com/ethereum/go-ethereum/issues/24284 +// It aggregates the market, bumped, and max price to provide a correct value, for both maxFeePerGas as well as maxPriorityFerPergas. +func (f *FeeHistoryEstimator) BumpDynamicFee(ctx context.Context, originalFee DynamicFee, maxPrice *assets.Wei, _ []EvmPriorAttempt) (bumped DynamicFee, err error) { + // For chains that don't have a mempool there is no concept of gas bumping so we force-call RefreshDynamicPrice to update the underlying base fee value + if f.config.BlockHistorySize == 0 { + if !f.IfStarted(func() { + if refreshErr := f.RefreshDynamicPrice(); refreshErr != nil { + err = refreshErr + return + } + f.refreshCh <- struct{}{} + bumped, err = f.GetDynamicFee(ctx, maxPrice) + }) { + return bumped, fmt.Errorf("estimator not started") + } + return bumped, err + } + + // Sanitize original fee input + // According to geth's spec we need to bump both maxFeePerGas and maxPriorityFeePerGas for the new attempt to be accepted by the RPC + if originalFee.FeeCap == nil || + originalFee.TipCap == nil || + ((originalFee.TipCap.Cmp(originalFee.FeeCap)) > 0) || + (originalFee.FeeCap.Cmp(maxPrice) >= 0) { + return bumped, fmt.Errorf("%w: error while retrieving original dynamic fees: (originalFeePerGas: %s - originalPriorityFeePerGas: %s). Maximum price configured: %s", + commonfee.ErrBump, originalFee.FeeCap, originalFee.TipCap, maxPrice) + } + + currentDynamicPrice, err := f.getDynamicPrice() + if err != nil { + return + } + + bumpedMaxPriorityFeePerGas := originalFee.TipCap.AddPercentage(f.config.BumpPercent) + bumpedMaxFeePerGas := originalFee.FeeCap.AddPercentage(f.config.BumpPercent) + + bumpedMaxPriorityFeePerGas, err = LimitBumpedFee(originalFee.TipCap, currentDynamicPrice.TipCap, bumpedMaxPriorityFeePerGas, maxPrice) + if err != nil { + return bumped, fmt.Errorf("failed to limit maxPriorityFeePerGas: %w", err) + } + + priorityFeeThreshold, e := f.getPriorityFeeThreshold() + if e != nil { + return bumped, e + } + + if bumpedMaxPriorityFeePerGas.Cmp(priorityFeeThreshold) > 0 { + return bumped, fmt.Errorf("bumpedMaxPriorityFeePerGas: %s is above market's %sth percentile: %s, bumping is halted", + bumpedMaxPriorityFeePerGas, strconv.Itoa(ConnectivityPercentile), priorityFeeThreshold) + } + + bumpedMaxFeePerGas, err = LimitBumpedFee(originalFee.FeeCap, currentDynamicPrice.FeeCap, bumpedMaxFeePerGas, maxPrice) + if err != nil { + return bumped, fmt.Errorf("failed to limit maxFeePerGas: %w", err) + } + + bumpedFee := DynamicFee{FeeCap: bumpedMaxFeePerGas, TipCap: bumpedMaxPriorityFeePerGas} + f.logger.Debugw("bumped dynamic fee", "originalFee", originalFee, "marketFee", currentDynamicPrice, "bumpedFee", bumpedFee) + + return bumpedFee, nil +} + +// LimitBumpedFee selects the maximum value between the bumped attempt and the current fee, if there is one. If the result is higher than the max price it gets capped. +// Geth's implementation has a hard 10% minimum limit for the bumped values, otherwise it rejects the transaction with an error. +// See: https://github.com/ethereum/go-ethereum/blob/bff330335b94af3643ac2fb809793f77de3069d4/core/tx_list.go#L298 +// +// Note: for chains that support EIP-1559 but we still choose to send Legacy transactions to them, the limit is still enforcable due to the fact that Legacy transactions +// are treated the same way as Dynamic transactions under the hood. For chains that don't support EIP-1559 at all, the limit isn't enforcable but a 10% minimum bump percentage +// makes sense anyway. +func LimitBumpedFee(originalFee *assets.Wei, currentFee *assets.Wei, bumpedFee *assets.Wei, maxPrice *assets.Wei) (*assets.Wei, error) { + if currentFee != nil { + bumpedFee = assets.WeiMax(currentFee, bumpedFee) + } + bumpedFee = assets.WeiMin(bumpedFee, maxPrice) + + // The first check is added for the following edge case: + // If originalFee is below 10 wei, then adding the minimum bump percentage won't have any effect on the final value because of rounding down. + // Similarly for bumpedFee, it can have the exact same value as the originalFee, even if we bumped, given an originalFee of less than 10 wei + // and a small enough BumpPercent. + if bumpedFee.Cmp(originalFee) == 0 || + bumpedFee.Cmp(originalFee.AddPercentage(MinimumBumpPercentage)) < 0 { + return nil, fmt.Errorf("%w: %s is bumped less than minimum allowed percentage(%s) from originalFee: %s - maxPrice: %s", + commonfee.ErrBump, bumpedFee, strconv.Itoa(MinimumBumpPercentage), originalFee, maxPrice) + } + return bumpedFee, nil +} + +func (f *FeeHistoryEstimator) getPriorityFeeThreshold() (*assets.Wei, error) { + f.priorityFeeThresholdMu.RLock() + defer f.priorityFeeThresholdMu.RUnlock() + if f.priorityFeeThreshold == nil { + return f.priorityFeeThreshold, fmt.Errorf("priorityFeeThreshold not set") + } + return f.priorityFeeThreshold, nil +} + +func (f *FeeHistoryEstimator) Name() string { return f.logger.Name() } +func (f *FeeHistoryEstimator) L1Oracle() rollups.L1Oracle { return f.l1Oracle } +func (f *FeeHistoryEstimator) HealthReport() map[string]error { return map[string]error{f.Name(): nil} } +func (f *FeeHistoryEstimator) OnNewLongestChain(context.Context, *evmtypes.Head) {} diff --git a/core/chains/evm/gas/fee_history_estimator_test.go b/core/chains/evm/gas/fee_history_estimator_test.go new file mode 100644 index 00000000000..6e42e0e2094 --- /dev/null +++ b/core/chains/evm/gas/fee_history_estimator_test.go @@ -0,0 +1,513 @@ +package gas_test + +import ( + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/mocks" +) + +func TestFeeHistoryEstimatorLifecycle(t *testing.T) { + t.Parallel() + var gasLimit uint64 = 21000 + maxPrice := assets.NewWeiI(100) + chainID := big.NewInt(0) + + t.Run("fails if you fetch gas price before the estimator starts", func(t *testing.T) { + cfg := gas.FeeHistoryEstimatorConfig{ + BumpPercent: 20, + RewardPercentile: 60, + EIP1559: false, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), nil, cfg, chainID, nil) + _, _, err := u.GetLegacyGas(tests.Context(t), nil, gasLimit, maxPrice) + assert.ErrorContains(t, err, "gas price not set") + }) + + t.Run("fails to start if BumpPercent is lower than the minimum cap", func(t *testing.T) { + cfg := gas.FeeHistoryEstimatorConfig{BumpPercent: 9} + + u := gas.NewFeeHistoryEstimator(logger.Test(t), nil, cfg, chainID, nil) + assert.ErrorContains(t, u.Start(tests.Context(t)), "BumpPercent") + }) + + t.Run("fails to start if RewardPercentile is higher than ConnectivityPercentile in EIP-1559", func(t *testing.T) { + cfg := gas.FeeHistoryEstimatorConfig{ + BumpPercent: 20, + RewardPercentile: 99, + EIP1559: true, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), nil, cfg, chainID, nil) + assert.ErrorContains(t, u.Start(tests.Context(t)), "RewardPercentile") + }) + + t.Run("starts if configs are correct", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(10), nil).Maybe() + + cfg := gas.FeeHistoryEstimatorConfig{ + BumpPercent: 20, + RewardPercentile: 10, + CacheTimeout: 10 * time.Second, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), nil, cfg, chainID, nil) + err := u.Start(tests.Context(t)) + assert.NoError(t, err) + err = u.Close() + assert.NoError(t, err) + }) +} + +func TestFeeHistoryEstimatorGetLegacyGas(t *testing.T) { + t.Parallel() + + var gasLimit uint64 = 21000 + maxPrice := assets.NewWeiI(100) + chainID := big.NewInt(0) + + t.Run("fetches a new gas price when first called", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(10), nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{} + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + _, err := u.RefreshGasPrice() + assert.NoError(t, err) + gasPrice, _, err := u.GetLegacyGas(tests.Context(t), nil, gasLimit, maxPrice) + assert.NoError(t, err) + assert.Equal(t, assets.NewWeiI(10), gasPrice) + }) + + t.Run("will return max price if estimation exceeds it", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(10), nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{} + + maxPrice := assets.NewWeiI(1) + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + _, err := u.RefreshGasPrice() + assert.NoError(t, err) + gas1, _, err := u.GetLegacyGas(tests.Context(t), nil, gasLimit, maxPrice) + assert.NoError(t, err) + assert.Equal(t, maxPrice, gas1) + }) + + t.Run("fails if gas price has not been set yet", func(t *testing.T) { + cfg := gas.FeeHistoryEstimatorConfig{} + + maxPrice := assets.NewWeiI(1) + u := gas.NewFeeHistoryEstimator(logger.Test(t), nil, cfg, chainID, nil) + _, _, err := u.GetLegacyGas(tests.Context(t), nil, gasLimit, maxPrice) + assert.Error(t, err) + assert.ErrorContains(t, err, "gas price not set") + }) +} + +func TestFeeHistoryEstimatorBumpLegacyGas(t *testing.T) { + t.Parallel() + + var gasLimit uint64 = 21000 + maxPrice := assets.NewWeiI(100) + chainID := big.NewInt(0) + + t.Run("bumps a previous attempt by BumpPercent", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalGasPrice := assets.NewWeiI(10) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(10), nil) + + cfg := gas.FeeHistoryEstimatorConfig{BumpPercent: 50, CacheTimeout: 5 * time.Second} + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + servicetest.RunHealthy(t, u) + gasPrice, _, err := u.BumpLegacyGas(tests.Context(t), originalGasPrice, gasLimit, maxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, assets.NewWeiI(15), gasPrice) + }) + + t.Run("fails if the original attempt is nil, or equal or higher than the max price", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + + cfg := gas.FeeHistoryEstimatorConfig{} + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + + var originalPrice *assets.Wei + _, _, err := u.BumpLegacyGas(tests.Context(t), originalPrice, gasLimit, maxPrice, nil) + assert.Error(t, err) + + originalPrice = assets.NewWeiI(100) + _, _, err = u.BumpLegacyGas(tests.Context(t), originalPrice, gasLimit, maxPrice, nil) + assert.Error(t, err) + }) + + t.Run("returns market gas price if bumped original fee is lower", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(80), nil).Once() + originalGasPrice := assets.NewWeiI(10) + + cfg := gas.FeeHistoryEstimatorConfig{} + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + gas, _, err := u.BumpLegacyGas(tests.Context(t), originalGasPrice, gasLimit, maxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, assets.NewWeiI(80), gas) + }) + + t.Run("returns max gas price if bumped original fee is higher", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(1), nil).Once() + originalGasPrice := assets.NewWeiI(10) + + cfg := gas.FeeHistoryEstimatorConfig{BumpPercent: 50} + + maxPrice := assets.NewWeiI(14) + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + gas, _, err := u.BumpLegacyGas(tests.Context(t), originalGasPrice, gasLimit, maxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, maxPrice, gas) + }) + + t.Run("returns max gas price if the aggregation of max and original bumped fee is higher", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(1), nil).Once() + originalGasPrice := assets.NewWeiI(10) + + cfg := gas.FeeHistoryEstimatorConfig{BumpPercent: 50} + + maxPrice := assets.NewWeiI(14) + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + gas, _, err := u.BumpLegacyGas(tests.Context(t), originalGasPrice, gasLimit, maxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, maxPrice, gas) + }) + + t.Run("fails if the bumped gas price is lower than the minimum bump percentage", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(100), nil).Once() + originalGasPrice := assets.NewWeiI(100) + + cfg := gas.FeeHistoryEstimatorConfig{BumpPercent: 20} + + // Price will be capped by the max price + maxPrice := assets.NewWeiI(101) + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + _, _, err := u.BumpLegacyGas(tests.Context(t), originalGasPrice, gasLimit, maxPrice, nil) + assert.Error(t, err) + }) +} + +func TestFeeHistoryEstimatorGetDynamicFee(t *testing.T) { + t.Parallel() + + maxPrice := assets.NewWeiI(100) + chainID := big.NewInt(0) + + t.Run("fetches a new dynamic fee when first called", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + baseFee := big.NewInt(5) + maxPriorityFeePerGas1 := big.NewInt(33) + maxPriorityFeePerGas2 := big.NewInt(20) + + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas1, big.NewInt(5)}, {maxPriorityFeePerGas2, big.NewInt(5)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee, baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + blockHistoryLength := 2 + cfg := gas.FeeHistoryEstimatorConfig{BlockHistorySize: uint64(blockHistoryLength)} + avrgPriorityFee := big.NewInt(0) + avrgPriorityFee.Add(maxPriorityFeePerGas1, maxPriorityFeePerGas2).Div(avrgPriorityFee, big.NewInt(int64(blockHistoryLength))) + maxFee := (*assets.Wei)(baseFee).AddPercentage(gas.BaseFeeBufferPercentage).Add((*assets.Wei)(avrgPriorityFee)) + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + dynamicFee, err := u.GetDynamicFee(tests.Context(t), maxPrice) + assert.NoError(t, err) + assert.Equal(t, maxFee, dynamicFee.FeeCap) + assert.Equal(t, (*assets.Wei)(avrgPriorityFee), dynamicFee.TipCap) + }) + + t.Run("fails if dynamic prices have not been set yet", func(t *testing.T) { + cfg := gas.FeeHistoryEstimatorConfig{} + + maxPrice := assets.NewWeiI(1) + u := gas.NewFeeHistoryEstimator(logger.Test(t), nil, cfg, chainID, nil) + _, err := u.GetDynamicFee(tests.Context(t), maxPrice) + assert.Error(t, err) + assert.ErrorContains(t, err, "dynamic price not set") + }) + + t.Run("will return max price if tip cap or fee cap exceed it", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + baseFee := big.NewInt(1) + maxPriorityFeePerGas := big.NewInt(3) + maxPrice := assets.NewWeiI(2) + + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas, big.NewInt(5)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{BlockHistorySize: 1} + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + dynamicFee, err := u.GetDynamicFee(tests.Context(t), maxPrice) + assert.NoError(t, err) + assert.Equal(t, maxPrice, dynamicFee.FeeCap) + assert.Equal(t, maxPrice, dynamicFee.TipCap) + }) +} + +func TestFeeHistoryEstimatorBumpDynamicFee(t *testing.T) { + t.Parallel() + + globalMaxPrice := assets.NewWeiI(100) + chainID := big.NewInt(0) + + t.Run("bumps a previous attempt by BumpPercent", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(10), + } + + // These values will be ignored because they are lower prices than the originalFee + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{big.NewInt(5), big.NewInt(50)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{big.NewInt(5)}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{ + BlockHistorySize: 2, + BumpPercent: 50, + } + + expectedFeeCap := originalFee.FeeCap.AddPercentage(cfg.BumpPercent) + expectedTipCap := originalFee.TipCap.AddPercentage(cfg.BumpPercent) + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + dynamicFee, err := u.BumpDynamicFee(tests.Context(t), originalFee, globalMaxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, expectedFeeCap, dynamicFee.FeeCap) + assert.Equal(t, expectedTipCap, dynamicFee.TipCap) + }) + + t.Run("fails if the original attempt is invalid", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + maxPrice := assets.NewWeiI(20) + cfg := gas.FeeHistoryEstimatorConfig{BlockHistorySize: 1} + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + // nil original fee + var originalFee gas.DynamicFee + _, err := u.BumpDynamicFee(tests.Context(t), originalFee, maxPrice, nil) + assert.Error(t, err) + + // tip cap is higher than fee cap + originalFee = gas.DynamicFee{ + FeeCap: assets.NewWeiI(10), + TipCap: assets.NewWeiI(11), + } + _, err = u.BumpDynamicFee(tests.Context(t), originalFee, maxPrice, nil) + assert.Error(t, err) + + // fee cap is equal or higher to max price + originalFee = gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(10), + } + _, err = u.BumpDynamicFee(tests.Context(t), originalFee, maxPrice, nil) + assert.Error(t, err) + }) + + t.Run("returns market prices if bumped original fee is lower", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(10), + } + + // Market fees + baseFee := big.NewInt(5) + maxPriorityFeePerGas := big.NewInt(33) + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas, big.NewInt(100)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + maxFee := (*assets.Wei)(baseFee).AddPercentage(gas.BaseFeeBufferPercentage).Add((*assets.Wei)(maxPriorityFeePerGas)) + + cfg := gas.FeeHistoryEstimatorConfig{ + BlockHistorySize: 1, + BumpPercent: 50, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + bumpedFee, err := u.BumpDynamicFee(tests.Context(t), originalFee, globalMaxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, (*assets.Wei)(maxPriorityFeePerGas), bumpedFee.TipCap) + assert.Equal(t, maxFee, bumpedFee.FeeCap) + }) + + t.Run("fails if connectivity percentile value is reached", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(10), + } + + // Market fees + baseFee := big.NewInt(5) + maxPriorityFeePerGas := big.NewInt(33) + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas, big.NewInt(30)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{ + BlockHistorySize: 1, + BumpPercent: 50, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + _, err = u.BumpDynamicFee(tests.Context(t), originalFee, globalMaxPrice, nil) + assert.Error(t, err) + }) + + t.Run("returns max price if the aggregation of max and original bumped fee is higher", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(18), + } + + maxPrice := assets.NewWeiI(25) + // Market fees + baseFee := big.NewInt(1) + maxPriorityFeePerGas := big.NewInt(1) + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas, big.NewInt(30)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{ + BlockHistorySize: 1, + BumpPercent: 50, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + bumpedFee, err := u.BumpDynamicFee(tests.Context(t), originalFee, maxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, maxPrice, bumpedFee.TipCap) + assert.Equal(t, maxPrice, bumpedFee.FeeCap) + }) + + t.Run("fails if the bumped gas price is lower than the minimum bump percentage", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(18), + } + + maxPrice := assets.NewWeiI(21) + // Market fees + baseFee := big.NewInt(1) + maxPriorityFeePerGas := big.NewInt(1) + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas, big.NewInt(30)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{ + BlockHistorySize: 1, + BumpPercent: 50, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + _, err = u.BumpDynamicFee(tests.Context(t), originalFee, maxPrice, nil) + assert.Error(t, err) + }) + + t.Run("ignores maxPriorityFeePerGas if there is no mempool and forces refetch", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(40), + TipCap: assets.NewWeiI(0), + } + + // Market fees + baseFee := big.NewInt(10) + maxPriorityFeePerGas := big.NewInt(0) + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas, big.NewInt(0)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil) + + cfg := gas.FeeHistoryEstimatorConfig{ + BlockHistorySize: 0, + BumpPercent: 20, + CacheTimeout: 10 * time.Second, + EIP1559: true, + } + + maxFeePerGas := assets.NewWei(baseFee).AddPercentage(gas.BaseFeeBufferPercentage) + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + servicetest.RunHealthy(t, u) + bumpedFee, err := u.BumpDynamicFee(tests.Context(t), originalFee, globalMaxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, assets.NewWeiI(0), (*assets.Wei)(maxPriorityFeePerGas)) + assert.Equal(t, maxFeePerGas, bumpedFee.FeeCap) + }) +} diff --git a/core/chains/evm/gas/helpers_test.go b/core/chains/evm/gas/helpers_test.go index 2c12ed426a6..0c9a74a18dc 100644 --- a/core/chains/evm/gas/helpers_test.go +++ b/core/chains/evm/gas/helpers_test.go @@ -157,6 +157,7 @@ type MockGasEstimatorConfig struct { FeeCapDefaultF *assets.Wei LimitMaxF uint64 ModeF string + EstimateLimitF bool } func NewMockGasConfig() *MockGasEstimatorConfig { @@ -214,3 +215,7 @@ func (m *MockGasEstimatorConfig) LimitMax() uint64 { func (m *MockGasEstimatorConfig) Mode() string { return m.ModeF } + +func (m *MockGasEstimatorConfig) EstimateLimit() bool { + return m.EstimateLimitF +} diff --git a/core/chains/evm/gas/mocks/evm_fee_estimator.go b/core/chains/evm/gas/mocks/evm_fee_estimator.go index a9adc261ce3..a7deca2c639 100644 --- a/core/chains/evm/gas/mocks/evm_fee_estimator.go +++ b/core/chains/evm/gas/mocks/evm_fee_estimator.go @@ -7,6 +7,8 @@ import ( assets "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + common "github.com/ethereum/go-ethereum/common" + context "context" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" @@ -145,14 +147,14 @@ func (_c *EvmFeeEstimator_Close_Call) RunAndReturn(run func() error) *EvmFeeEsti return _c } -// GetFee provides a mock function with given fields: ctx, calldata, feeLimit, maxFeePrice, opts -func (_m *EvmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...types.Opt) (gas.EvmFee, uint64, error) { +// GetFee provides a mock function with given fields: ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts +func (_m *EvmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress *common.Address, toAddress *common.Address, opts ...types.Opt) (gas.EvmFee, uint64, error) { _va := make([]interface{}, len(opts)) for _i := range opts { _va[_i] = opts[_i] } var _ca []interface{} - _ca = append(_ca, ctx, calldata, feeLimit, maxFeePrice) + _ca = append(_ca, ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress) _ca = append(_ca, _va...) ret := _m.Called(_ca...) @@ -163,23 +165,23 @@ func (_m *EvmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit var r0 gas.EvmFee var r1 uint64 var r2 error - if rf, ok := ret.Get(0).(func(context.Context, []byte, uint64, *assets.Wei, ...types.Opt) (gas.EvmFee, uint64, error)); ok { - return rf(ctx, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(0).(func(context.Context, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) (gas.EvmFee, uint64, error)); ok { + return rf(ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } - if rf, ok := ret.Get(0).(func(context.Context, []byte, uint64, *assets.Wei, ...types.Opt) gas.EvmFee); ok { - r0 = rf(ctx, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(0).(func(context.Context, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) gas.EvmFee); ok { + r0 = rf(ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } else { r0 = ret.Get(0).(gas.EvmFee) } - if rf, ok := ret.Get(1).(func(context.Context, []byte, uint64, *assets.Wei, ...types.Opt) uint64); ok { - r1 = rf(ctx, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(1).(func(context.Context, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) uint64); ok { + r1 = rf(ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } else { r1 = ret.Get(1).(uint64) } - if rf, ok := ret.Get(2).(func(context.Context, []byte, uint64, *assets.Wei, ...types.Opt) error); ok { - r2 = rf(ctx, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(2).(func(context.Context, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) error); ok { + r2 = rf(ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } else { r2 = ret.Error(2) } @@ -197,43 +199,45 @@ type EvmFeeEstimator_GetFee_Call struct { // - calldata []byte // - feeLimit uint64 // - maxFeePrice *assets.Wei +// - fromAddress *common.Address +// - toAddress *common.Address // - opts ...types.Opt -func (_e *EvmFeeEstimator_Expecter) GetFee(ctx interface{}, calldata interface{}, feeLimit interface{}, maxFeePrice interface{}, opts ...interface{}) *EvmFeeEstimator_GetFee_Call { +func (_e *EvmFeeEstimator_Expecter) GetFee(ctx interface{}, calldata interface{}, feeLimit interface{}, maxFeePrice interface{}, fromAddress interface{}, toAddress interface{}, opts ...interface{}) *EvmFeeEstimator_GetFee_Call { return &EvmFeeEstimator_GetFee_Call{Call: _e.mock.On("GetFee", - append([]interface{}{ctx, calldata, feeLimit, maxFeePrice}, opts...)...)} + append([]interface{}{ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress}, opts...)...)} } -func (_c *EvmFeeEstimator_GetFee_Call) Run(run func(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...types.Opt)) *EvmFeeEstimator_GetFee_Call { +func (_c *EvmFeeEstimator_GetFee_Call) Run(run func(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress *common.Address, toAddress *common.Address, opts ...types.Opt)) *EvmFeeEstimator_GetFee_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]types.Opt, len(args)-4) - for i, a := range args[4:] { + variadicArgs := make([]types.Opt, len(args)-6) + for i, a := range args[6:] { if a != nil { variadicArgs[i] = a.(types.Opt) } } - run(args[0].(context.Context), args[1].([]byte), args[2].(uint64), args[3].(*assets.Wei), variadicArgs...) + run(args[0].(context.Context), args[1].([]byte), args[2].(uint64), args[3].(*assets.Wei), args[4].(*common.Address), args[5].(*common.Address), variadicArgs...) }) return _c } -func (_c *EvmFeeEstimator_GetFee_Call) Return(fee gas.EvmFee, chainSpecificFeeLimit uint64, err error) *EvmFeeEstimator_GetFee_Call { - _c.Call.Return(fee, chainSpecificFeeLimit, err) +func (_c *EvmFeeEstimator_GetFee_Call) Return(fee gas.EvmFee, estimatedFeeLimit uint64, err error) *EvmFeeEstimator_GetFee_Call { + _c.Call.Return(fee, estimatedFeeLimit, err) return _c } -func (_c *EvmFeeEstimator_GetFee_Call) RunAndReturn(run func(context.Context, []byte, uint64, *assets.Wei, ...types.Opt) (gas.EvmFee, uint64, error)) *EvmFeeEstimator_GetFee_Call { +func (_c *EvmFeeEstimator_GetFee_Call) RunAndReturn(run func(context.Context, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) (gas.EvmFee, uint64, error)) *EvmFeeEstimator_GetFee_Call { _c.Call.Return(run) return _c } -// GetMaxCost provides a mock function with given fields: ctx, amount, calldata, feeLimit, maxFeePrice, opts -func (_m *EvmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...types.Opt) (*big.Int, error) { +// GetMaxCost provides a mock function with given fields: ctx, amount, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts +func (_m *EvmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress *common.Address, toAddress *common.Address, opts ...types.Opt) (*big.Int, error) { _va := make([]interface{}, len(opts)) for _i := range opts { _va[_i] = opts[_i] } var _ca []interface{} - _ca = append(_ca, ctx, amount, calldata, feeLimit, maxFeePrice) + _ca = append(_ca, ctx, amount, calldata, feeLimit, maxFeePrice, fromAddress, toAddress) _ca = append(_ca, _va...) ret := _m.Called(_ca...) @@ -243,19 +247,19 @@ func (_m *EvmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, ca var r0 *big.Int var r1 error - if rf, ok := ret.Get(0).(func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, ...types.Opt) (*big.Int, error)); ok { - return rf(ctx, amount, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(0).(func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) (*big.Int, error)); ok { + return rf(ctx, amount, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } - if rf, ok := ret.Get(0).(func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, ...types.Opt) *big.Int); ok { - r0 = rf(ctx, amount, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(0).(func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) *big.Int); ok { + r0 = rf(ctx, amount, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*big.Int) } } - if rf, ok := ret.Get(1).(func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, ...types.Opt) error); ok { - r1 = rf(ctx, amount, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(1).(func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) error); ok { + r1 = rf(ctx, amount, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } else { r1 = ret.Error(1) } @@ -274,21 +278,23 @@ type EvmFeeEstimator_GetMaxCost_Call struct { // - calldata []byte // - feeLimit uint64 // - maxFeePrice *assets.Wei +// - fromAddress *common.Address +// - toAddress *common.Address // - opts ...types.Opt -func (_e *EvmFeeEstimator_Expecter) GetMaxCost(ctx interface{}, amount interface{}, calldata interface{}, feeLimit interface{}, maxFeePrice interface{}, opts ...interface{}) *EvmFeeEstimator_GetMaxCost_Call { +func (_e *EvmFeeEstimator_Expecter) GetMaxCost(ctx interface{}, amount interface{}, calldata interface{}, feeLimit interface{}, maxFeePrice interface{}, fromAddress interface{}, toAddress interface{}, opts ...interface{}) *EvmFeeEstimator_GetMaxCost_Call { return &EvmFeeEstimator_GetMaxCost_Call{Call: _e.mock.On("GetMaxCost", - append([]interface{}{ctx, amount, calldata, feeLimit, maxFeePrice}, opts...)...)} + append([]interface{}{ctx, amount, calldata, feeLimit, maxFeePrice, fromAddress, toAddress}, opts...)...)} } -func (_c *EvmFeeEstimator_GetMaxCost_Call) Run(run func(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...types.Opt)) *EvmFeeEstimator_GetMaxCost_Call { +func (_c *EvmFeeEstimator_GetMaxCost_Call) Run(run func(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress *common.Address, toAddress *common.Address, opts ...types.Opt)) *EvmFeeEstimator_GetMaxCost_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]types.Opt, len(args)-5) - for i, a := range args[5:] { + variadicArgs := make([]types.Opt, len(args)-7) + for i, a := range args[7:] { if a != nil { variadicArgs[i] = a.(types.Opt) } } - run(args[0].(context.Context), args[1].(assets.Eth), args[2].([]byte), args[3].(uint64), args[4].(*assets.Wei), variadicArgs...) + run(args[0].(context.Context), args[1].(assets.Eth), args[2].([]byte), args[3].(uint64), args[4].(*assets.Wei), args[5].(*common.Address), args[6].(*common.Address), variadicArgs...) }) return _c } @@ -298,7 +304,7 @@ func (_c *EvmFeeEstimator_GetMaxCost_Call) Return(_a0 *big.Int, _a1 error) *EvmF return _c } -func (_c *EvmFeeEstimator_GetMaxCost_Call) RunAndReturn(run func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, ...types.Opt) (*big.Int, error)) *EvmFeeEstimator_GetMaxCost_Call { +func (_c *EvmFeeEstimator_GetMaxCost_Call) RunAndReturn(run func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) (*big.Int, error)) *EvmFeeEstimator_GetMaxCost_Call { _c.Call.Return(run) return _c } diff --git a/core/chains/evm/gas/mocks/fee_estimator_client.go b/core/chains/evm/gas/mocks/fee_estimator_client.go index 8e10107597f..f5ca52ec920 100644 --- a/core/chains/evm/gas/mocks/fee_estimator_client.go +++ b/core/chains/evm/gas/mocks/fee_estimator_client.go @@ -241,6 +241,123 @@ func (_c *FeeEstimatorClient_ConfiguredChainID_Call) RunAndReturn(run func() *bi return _c } +// EstimateGas provides a mock function with given fields: ctx, call +func (_m *FeeEstimatorClient) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { + ret := _m.Called(ctx, call) + + if len(ret) == 0 { + panic("no return value specified for EstimateGas") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) (uint64, error)); ok { + return rf(ctx, call) + } + if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) uint64); ok { + r0 = rf(ctx, call) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, ethereum.CallMsg) error); ok { + r1 = rf(ctx, call) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FeeEstimatorClient_EstimateGas_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EstimateGas' +type FeeEstimatorClient_EstimateGas_Call struct { + *mock.Call +} + +// EstimateGas is a helper method to define mock.On call +// - ctx context.Context +// - call ethereum.CallMsg +func (_e *FeeEstimatorClient_Expecter) EstimateGas(ctx interface{}, call interface{}) *FeeEstimatorClient_EstimateGas_Call { + return &FeeEstimatorClient_EstimateGas_Call{Call: _e.mock.On("EstimateGas", ctx, call)} +} + +func (_c *FeeEstimatorClient_EstimateGas_Call) Run(run func(ctx context.Context, call ethereum.CallMsg)) *FeeEstimatorClient_EstimateGas_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(ethereum.CallMsg)) + }) + return _c +} + +func (_c *FeeEstimatorClient_EstimateGas_Call) Return(_a0 uint64, _a1 error) *FeeEstimatorClient_EstimateGas_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FeeEstimatorClient_EstimateGas_Call) RunAndReturn(run func(context.Context, ethereum.CallMsg) (uint64, error)) *FeeEstimatorClient_EstimateGas_Call { + _c.Call.Return(run) + return _c +} + +// FeeHistory provides a mock function with given fields: ctx, blockCount, rewardPercentiles +func (_m *FeeEstimatorClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (*ethereum.FeeHistory, error) { + ret := _m.Called(ctx, blockCount, rewardPercentiles) + + if len(ret) == 0 { + panic("no return value specified for FeeHistory") + } + + var r0 *ethereum.FeeHistory + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)); ok { + return rf(ctx, blockCount, rewardPercentiles) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) *ethereum.FeeHistory); ok { + r0 = rf(ctx, blockCount, rewardPercentiles) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ethereum.FeeHistory) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, []float64) error); ok { + r1 = rf(ctx, blockCount, rewardPercentiles) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FeeEstimatorClient_FeeHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeeHistory' +type FeeEstimatorClient_FeeHistory_Call struct { + *mock.Call +} + +// FeeHistory is a helper method to define mock.On call +// - ctx context.Context +// - blockCount uint64 +// - rewardPercentiles []float64 +func (_e *FeeEstimatorClient_Expecter) FeeHistory(ctx interface{}, blockCount interface{}, rewardPercentiles interface{}) *FeeEstimatorClient_FeeHistory_Call { + return &FeeEstimatorClient_FeeHistory_Call{Call: _e.mock.On("FeeHistory", ctx, blockCount, rewardPercentiles)} +} + +func (_c *FeeEstimatorClient_FeeHistory_Call) Run(run func(ctx context.Context, blockCount uint64, rewardPercentiles []float64)) *FeeEstimatorClient_FeeHistory_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].([]float64)) + }) + return _c +} + +func (_c *FeeEstimatorClient_FeeHistory_Call) Return(feeHistory *ethereum.FeeHistory, err error) *FeeEstimatorClient_FeeHistory_Call { + _c.Call.Return(feeHistory, err) + return _c +} + +func (_c *FeeEstimatorClient_FeeHistory_Call) RunAndReturn(run func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)) *FeeEstimatorClient_FeeHistory_Call { + _c.Call.Return(run) + return _c +} + // HeadByNumber provides a mock function with given fields: ctx, n func (_m *FeeEstimatorClient) HeadByNumber(ctx context.Context, n *big.Int) (*types.Head, error) { ret := _m.Called(ctx, n) @@ -300,6 +417,64 @@ func (_c *FeeEstimatorClient_HeadByNumber_Call) RunAndReturn(run func(context.Co return _c } +// SuggestGasPrice provides a mock function with given fields: ctx +func (_m *FeeEstimatorClient) SuggestGasPrice(ctx context.Context) (*big.Int, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for SuggestGasPrice") + } + + var r0 *big.Int + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*big.Int, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *big.Int); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FeeEstimatorClient_SuggestGasPrice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SuggestGasPrice' +type FeeEstimatorClient_SuggestGasPrice_Call struct { + *mock.Call +} + +// SuggestGasPrice is a helper method to define mock.On call +// - ctx context.Context +func (_e *FeeEstimatorClient_Expecter) SuggestGasPrice(ctx interface{}) *FeeEstimatorClient_SuggestGasPrice_Call { + return &FeeEstimatorClient_SuggestGasPrice_Call{Call: _e.mock.On("SuggestGasPrice", ctx)} +} + +func (_c *FeeEstimatorClient_SuggestGasPrice_Call) Run(run func(ctx context.Context)) *FeeEstimatorClient_SuggestGasPrice_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *FeeEstimatorClient_SuggestGasPrice_Call) Return(_a0 *big.Int, _a1 error) *FeeEstimatorClient_SuggestGasPrice_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FeeEstimatorClient_SuggestGasPrice_Call) RunAndReturn(run func(context.Context) (*big.Int, error)) *FeeEstimatorClient_SuggestGasPrice_Call { + _c.Call.Return(run) + return _c +} + // NewFeeEstimatorClient creates a new instance of FeeEstimatorClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewFeeEstimatorClient(t interface { diff --git a/core/chains/evm/gas/mocks/fee_history_estimator_client.go b/core/chains/evm/gas/mocks/fee_history_estimator_client.go new file mode 100644 index 00000000000..74862145016 --- /dev/null +++ b/core/chains/evm/gas/mocks/fee_history_estimator_client.go @@ -0,0 +1,157 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + big "math/big" + + ethereum "github.com/ethereum/go-ethereum" + + mock "github.com/stretchr/testify/mock" +) + +// FeeHistoryEstimatorClient is an autogenerated mock type for the feeHistoryEstimatorClient type +type FeeHistoryEstimatorClient struct { + mock.Mock +} + +type FeeHistoryEstimatorClient_Expecter struct { + mock *mock.Mock +} + +func (_m *FeeHistoryEstimatorClient) EXPECT() *FeeHistoryEstimatorClient_Expecter { + return &FeeHistoryEstimatorClient_Expecter{mock: &_m.Mock} +} + +// FeeHistory provides a mock function with given fields: ctx, blockCount, rewardPercentiles +func (_m *FeeHistoryEstimatorClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (*ethereum.FeeHistory, error) { + ret := _m.Called(ctx, blockCount, rewardPercentiles) + + if len(ret) == 0 { + panic("no return value specified for FeeHistory") + } + + var r0 *ethereum.FeeHistory + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)); ok { + return rf(ctx, blockCount, rewardPercentiles) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) *ethereum.FeeHistory); ok { + r0 = rf(ctx, blockCount, rewardPercentiles) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ethereum.FeeHistory) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, []float64) error); ok { + r1 = rf(ctx, blockCount, rewardPercentiles) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FeeHistoryEstimatorClient_FeeHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeeHistory' +type FeeHistoryEstimatorClient_FeeHistory_Call struct { + *mock.Call +} + +// FeeHistory is a helper method to define mock.On call +// - ctx context.Context +// - blockCount uint64 +// - rewardPercentiles []float64 +func (_e *FeeHistoryEstimatorClient_Expecter) FeeHistory(ctx interface{}, blockCount interface{}, rewardPercentiles interface{}) *FeeHistoryEstimatorClient_FeeHistory_Call { + return &FeeHistoryEstimatorClient_FeeHistory_Call{Call: _e.mock.On("FeeHistory", ctx, blockCount, rewardPercentiles)} +} + +func (_c *FeeHistoryEstimatorClient_FeeHistory_Call) Run(run func(ctx context.Context, blockCount uint64, rewardPercentiles []float64)) *FeeHistoryEstimatorClient_FeeHistory_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].([]float64)) + }) + return _c +} + +func (_c *FeeHistoryEstimatorClient_FeeHistory_Call) Return(feeHistory *ethereum.FeeHistory, err error) *FeeHistoryEstimatorClient_FeeHistory_Call { + _c.Call.Return(feeHistory, err) + return _c +} + +func (_c *FeeHistoryEstimatorClient_FeeHistory_Call) RunAndReturn(run func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)) *FeeHistoryEstimatorClient_FeeHistory_Call { + _c.Call.Return(run) + return _c +} + +// SuggestGasPrice provides a mock function with given fields: ctx +func (_m *FeeHistoryEstimatorClient) SuggestGasPrice(ctx context.Context) (*big.Int, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for SuggestGasPrice") + } + + var r0 *big.Int + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*big.Int, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *big.Int); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FeeHistoryEstimatorClient_SuggestGasPrice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SuggestGasPrice' +type FeeHistoryEstimatorClient_SuggestGasPrice_Call struct { + *mock.Call +} + +// SuggestGasPrice is a helper method to define mock.On call +// - ctx context.Context +func (_e *FeeHistoryEstimatorClient_Expecter) SuggestGasPrice(ctx interface{}) *FeeHistoryEstimatorClient_SuggestGasPrice_Call { + return &FeeHistoryEstimatorClient_SuggestGasPrice_Call{Call: _e.mock.On("SuggestGasPrice", ctx)} +} + +func (_c *FeeHistoryEstimatorClient_SuggestGasPrice_Call) Run(run func(ctx context.Context)) *FeeHistoryEstimatorClient_SuggestGasPrice_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *FeeHistoryEstimatorClient_SuggestGasPrice_Call) Return(_a0 *big.Int, _a1 error) *FeeHistoryEstimatorClient_SuggestGasPrice_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FeeHistoryEstimatorClient_SuggestGasPrice_Call) RunAndReturn(run func(context.Context) (*big.Int, error)) *FeeHistoryEstimatorClient_SuggestGasPrice_Call { + _c.Call.Return(run) + return _c +} + +// NewFeeHistoryEstimatorClient creates a new instance of FeeHistoryEstimatorClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewFeeHistoryEstimatorClient(t interface { + mock.TestingT + Cleanup(func()) +}) *FeeHistoryEstimatorClient { + mock := &FeeHistoryEstimatorClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/chains/evm/gas/models.go b/core/chains/evm/gas/models.go index c9607091fb0..92ae4394929 100644 --- a/core/chains/evm/gas/models.go +++ b/core/chains/evm/gas/models.go @@ -26,6 +26,9 @@ import ( evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ) +// EstimateGasBuffer is a multiplier applied to estimated gas when the EstimateLimit feature is enabled +const EstimateGasBuffer = float32(1.15) + // EvmFeeEstimator provides a unified interface that wraps EvmEstimator and can determine if legacy or dynamic fee estimation should be used type EvmFeeEstimator interface { services.Service @@ -33,11 +36,11 @@ type EvmFeeEstimator interface { // L1Oracle returns the L1 gas price oracle only if the chain has one, e.g. OP stack L2s and Arbitrum. L1Oracle() rollups.L1Oracle - GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint64, err error) + GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress, toAddress *common.Address, opts ...feetypes.Opt) (fee EvmFee, estimatedFeeLimit uint64, err error) BumpFee(ctx context.Context, originalFee EvmFee, feeLimit uint64, maxFeePrice *assets.Wei, attempts []EvmPriorAttempt) (bumpedFee EvmFee, chainSpecificFeeLimit uint64, err error) // GetMaxCost returns the total value = max price x fee units + transferred value - GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (*big.Int, error) + GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress, toAddress *common.Address, opts ...feetypes.Opt) (*big.Int, error) } type feeEstimatorClient interface { @@ -46,6 +49,9 @@ type feeEstimatorClient interface { CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error ConfiguredChainID() *big.Int HeadByNumber(ctx context.Context, n *big.Int) (*evmtypes.Head, error) + EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) + SuggestGasPrice(ctx context.Context) (*big.Int, error) + FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) } // NewEstimator returns the estimator for a given config @@ -70,6 +76,7 @@ func NewEstimator(lggr logger.Logger, ethClient feeEstimatorClient, cfg Config, "tipCapMin", geCfg.TipCapMin(), "priceMax", geCfg.PriceMax(), "priceMin", geCfg.PriceMin(), + "estimateLimit", geCfg.EstimateLimit(), ) df := geCfg.EIP1559DynamicFees() @@ -105,13 +112,25 @@ func NewEstimator(lggr logger.Logger, ethClient feeEstimatorClient, cfg Config, newEstimator = func(l logger.Logger) EvmEstimator { return NewSuggestedPriceEstimator(lggr, ethClient, geCfg, l1Oracle) } + case "FeeHistory": + newEstimator = func(l logger.Logger) EvmEstimator { + ccfg := FeeHistoryEstimatorConfig{ + BumpPercent: geCfg.BumpPercent(), + CacheTimeout: geCfg.FeeHistory().CacheTimeout(), + EIP1559: geCfg.EIP1559DynamicFees(), + BlockHistorySize: uint64(geCfg.BlockHistory().BlockHistorySize()), + RewardPercentile: float64(geCfg.BlockHistory().TransactionPercentile()), + } + return NewFeeHistoryEstimator(lggr, ethClient, ccfg, ethClient.ConfiguredChainID(), l1Oracle) + } + default: lggr.Warnf("GasEstimator: unrecognised mode '%s', falling back to FixedPriceEstimator", s) newEstimator = func(l logger.Logger) EvmEstimator { return NewFixedPriceEstimator(geCfg, ethClient, bh, lggr, l1Oracle) } } - return NewEvmFeeEstimator(lggr, newEstimator, df, geCfg), nil + return NewEvmFeeEstimator(lggr, newEstimator, df, geCfg, ethClient), nil } // DynamicFee encompasses both FeeCap and TipCap for EIP1559 transactions @@ -182,17 +201,19 @@ type evmFeeEstimator struct { EvmEstimator EIP1559Enabled bool geCfg GasEstimatorConfig + ethClient feeEstimatorClient } var _ EvmFeeEstimator = (*evmFeeEstimator)(nil) -func NewEvmFeeEstimator(lggr logger.Logger, newEstimator func(logger.Logger) EvmEstimator, eip1559Enabled bool, geCfg GasEstimatorConfig) EvmFeeEstimator { +func NewEvmFeeEstimator(lggr logger.Logger, newEstimator func(logger.Logger) EvmEstimator, eip1559Enabled bool, geCfg GasEstimatorConfig, ethClient feeEstimatorClient) EvmFeeEstimator { lggr = logger.Named(lggr, "WrappedEvmEstimator") return &evmFeeEstimator{ lggr: lggr, EvmEstimator: newEstimator(lggr), EIP1559Enabled: eip1559Enabled, geCfg: geCfg, + ethClient: ethClient, } } @@ -262,7 +283,10 @@ func (e *evmFeeEstimator) L1Oracle() rollups.L1Oracle { return e.EvmEstimator.L1Oracle() } -func (e *evmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint64, err error) { +// GetFee returns an initial estimated gas price and gas limit for a transaction +// The gas limit provided by the caller can be adjusted by gas estimation or for 2D fees +func (e *evmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress, toAddress *common.Address, opts ...feetypes.Opt) (fee EvmFee, estimatedFeeLimit uint64, err error) { + var chainSpecificFeeLimit uint64 // get dynamic fee if e.EIP1559Enabled { var dynamicFee DynamicFee @@ -270,24 +294,23 @@ func (e *evmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit if err != nil { return } - chainSpecificFeeLimit, err = commonfee.ApplyMultiplier(feeLimit, e.geCfg.LimitMultiplier()) fee.DynamicFeeCap = dynamicFee.FeeCap fee.DynamicTipCap = dynamicFee.TipCap - return - } - - // get legacy fee - fee.Legacy, chainSpecificFeeLimit, err = e.EvmEstimator.GetLegacyGas(ctx, calldata, feeLimit, maxFeePrice, opts...) - if err != nil { - return + chainSpecificFeeLimit = feeLimit + } else { + // get legacy fee + fee.Legacy, chainSpecificFeeLimit, err = e.EvmEstimator.GetLegacyGas(ctx, calldata, feeLimit, maxFeePrice, opts...) + if err != nil { + return + } } - chainSpecificFeeLimit, err = commonfee.ApplyMultiplier(chainSpecificFeeLimit, e.geCfg.LimitMultiplier()) + estimatedFeeLimit, err = e.estimateFeeLimit(ctx, chainSpecificFeeLimit, calldata, fromAddress, toAddress) return } -func (e *evmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (*big.Int, error) { - fees, gasLimit, err := e.GetFee(ctx, calldata, feeLimit, maxFeePrice, opts...) +func (e *evmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress, toAddress *common.Address, opts ...feetypes.Opt) (*big.Int, error) { + fees, gasLimit, err := e.GetFee(ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) if err != nil { return nil, err } @@ -338,6 +361,56 @@ func (e *evmFeeEstimator) BumpFee(ctx context.Context, originalFee EvmFee, feeLi return } +func (e *evmFeeEstimator) estimateFeeLimit(ctx context.Context, feeLimit uint64, calldata []byte, fromAddress, toAddress *common.Address) (estimatedFeeLimit uint64, err error) { + // Use the feeLimit * LimitMultiplier as the provided gas limit since this multiplier is applied on top of the caller specified gas limit + providedGasLimit, err := commonfee.ApplyMultiplier(feeLimit, e.geCfg.LimitMultiplier()) + if err != nil { + return estimatedFeeLimit, err + } + // Use provided fee limit by default if EstimateLimit is disabled + if !e.geCfg.EstimateLimit() { + return providedGasLimit, nil + } + // Create call msg for gas limit estimation + // Skip setting Gas to avoid capping the results of the estimation + callMsg := ethereum.CallMsg{ + To: toAddress, + Data: calldata, + } + if fromAddress != nil { + callMsg.From = *fromAddress + } + estimatedGas, estimateErr := e.ethClient.EstimateGas(ctx, callMsg) + if estimateErr != nil { + if providedGasLimit > 0 { + // Do not return error if estimate gas failed, we can still use the provided limit instead since it is an upper limit + e.lggr.Errorw("failed to estimate gas limit. falling back to the provided gas limit with multiplier", "callMsg", callMsg, "providedGasLimitWithMultiplier", providedGasLimit, "error", estimateErr) + return providedGasLimit, nil + } + return estimatedFeeLimit, fmt.Errorf("gas estimation failed and provided gas limit is 0: %w", estimateErr) + } + e.lggr.Debugw("estimated gas", "estimatedGas", estimatedGas, "providedGasLimitWithMultiplier", providedGasLimit) + // Return error if estimated gas without the buffer exceeds the provided gas limit, if provided + // Transaction would be destined to run out of gas and fail + if providedGasLimit > 0 && estimatedGas > providedGasLimit { + e.lggr.Errorw("estimated gas exceeds provided gas limit with multiplier", "estimatedGas", estimatedGas, "providedGasLimitWithMultiplier", providedGasLimit) + return estimatedFeeLimit, commonfee.ErrFeeLimitTooLow + } + // Apply EstimateGasBuffer to the estimated gas limit + estimatedFeeLimit, err = commonfee.ApplyMultiplier(estimatedGas, EstimateGasBuffer) + if err != nil { + return + } + // If provided gas limit is not 0, fallback to it if the buffer causes the estimated gas limit to exceed it + // The provided gas limit should be used as an upper bound to avoid unexpected behavior for products + if providedGasLimit > 0 && estimatedFeeLimit > providedGasLimit { + e.lggr.Debugw("estimated gas limit with buffer exceeds the provided gas limit with multiplier. falling back to the provided gas limit with multiplier", "estimatedGasLimit", estimatedFeeLimit, "providedGasLimitWithMultiplier", providedGasLimit) + estimatedFeeLimit = providedGasLimit + } + + return +} + // Config defines an interface for configuration in the gas package type Config interface { ChainType() chaintype.ChainType @@ -359,6 +432,7 @@ type GasEstimatorConfig interface { PriceMin() *assets.Wei PriceMax() *assets.Wei Mode() string + EstimateLimit() bool } type BlockHistoryConfig interface { diff --git a/core/chains/evm/gas/models_test.go b/core/chains/evm/gas/models_test.go index 92ea901596f..f2afc26c854 100644 --- a/core/chains/evm/gas/models_test.go +++ b/core/chains/evm/gas/models_test.go @@ -1,6 +1,7 @@ package gas_test import ( + "errors" "math/big" "testing" @@ -12,12 +13,14 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + commonfee "github.com/smartcontractkit/chainlink/v2/common/fee" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/chaintype" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" rollupMocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups/mocks" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/testutils" ) func TestWrappedEvmEstimator(t *testing.T) { @@ -35,9 +38,9 @@ func TestWrappedEvmEstimator(t *testing.T) { est := mocks.NewEvmEstimator(t) est.On("GetDynamicFee", mock.Anything, mock.Anything). - Return(dynamicFee, nil).Twice() + Return(dynamicFee, nil).Times(6) est.On("GetLegacyGas", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(legacyFee, gasLimit, nil).Twice() + Return(legacyFee, gasLimit, nil).Times(6) est.On("BumpDynamicFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(dynamicFee, nil).Once() est.On("BumpLegacyGas", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). @@ -49,6 +52,9 @@ func TestWrappedEvmEstimator(t *testing.T) { mockEstimatorName := "WrappedEvmEstimator" mockEvmEstimatorName := "WrappedEvmEstimator.MockEstimator" + fromAddress := testutils.NewAddress() + toAddress := testutils.NewAddress() + // L1Oracle returns the correct L1Oracle interface t.Run("L1Oracle", func(t *testing.T) { lggr := logger.Test(t) @@ -59,7 +65,7 @@ func TestWrappedEvmEstimator(t *testing.T) { getEst := func(logger.Logger) gas.EvmEstimator { return evmEstimator } // expect nil - estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, nil) + estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, nil, nil) l1Oracle := estimator.L1Oracle() assert.Nil(t, l1Oracle) @@ -68,7 +74,7 @@ func TestWrappedEvmEstimator(t *testing.T) { oracle, err := rollups.NewL1GasOracle(lggr, nil, chaintype.ChainOptimismBedrock) require.NoError(t, err) // cast oracle to L1Oracle interface - estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) evmEstimator.On("L1Oracle").Return(oracle).Once() l1Oracle = estimator.L1Oracle() @@ -80,8 +86,8 @@ func TestWrappedEvmEstimator(t *testing.T) { lggr := logger.Test(t) // expect legacy fee data dynamicFees := false - estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) - fee, max, err := estimator.GetFee(ctx, nil, 0, nil) + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, nil) + fee, max, err := estimator.GetFee(ctx, nil, 0, nil, nil, nil) require.NoError(t, err) assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), max) assert.True(t, legacyFee.Equal(fee.Legacy)) @@ -90,8 +96,8 @@ func TestWrappedEvmEstimator(t *testing.T) { // expect dynamic fee data dynamicFees = true - estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) - fee, max, err = estimator.GetFee(ctx, nil, gasLimit, nil) + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, nil) + fee, max, err = estimator.GetFee(ctx, nil, gasLimit, nil, nil, nil) require.NoError(t, err) assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), max) assert.True(t, dynamicFee.FeeCap.Equal(fee.DynamicFeeCap)) @@ -103,7 +109,7 @@ func TestWrappedEvmEstimator(t *testing.T) { t.Run("BumpFee", func(t *testing.T) { lggr := logger.Test(t) dynamicFees := false - estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, nil) // expect legacy fee data fee, max, err := estimator.BumpFee(ctx, gas.EvmFee{Legacy: assets.NewWeiI(0)}, 0, nil, nil) @@ -141,8 +147,8 @@ func TestWrappedEvmEstimator(t *testing.T) { // expect legacy fee data dynamicFees := false - estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) - total, err := estimator.GetMaxCost(ctx, val, nil, gasLimit, nil) + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, nil) + total, err := estimator.GetMaxCost(ctx, val, nil, gasLimit, nil, nil, nil) require.NoError(t, err) fee := new(big.Int).Mul(legacyFee.ToInt(), big.NewInt(int64(gasLimit))) fee, _ = new(big.Float).Mul(new(big.Float).SetInt(fee), big.NewFloat(float64(limitMultiplier))).Int(nil) @@ -150,8 +156,8 @@ func TestWrappedEvmEstimator(t *testing.T) { // expect dynamic fee data dynamicFees = true - estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) - total, err = estimator.GetMaxCost(ctx, val, nil, gasLimit, nil) + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, nil) + total, err = estimator.GetMaxCost(ctx, val, nil, gasLimit, nil, nil, nil) require.NoError(t, err) fee = new(big.Int).Mul(dynamicFee.FeeCap.ToInt(), big.NewInt(int64(gasLimit))) fee, _ = new(big.Float).Mul(new(big.Float).SetInt(fee), big.NewFloat(float64(limitMultiplier))).Int(nil) @@ -166,7 +172,7 @@ func TestWrappedEvmEstimator(t *testing.T) { estimator := gas.NewEvmFeeEstimator(lggr, func(logger.Logger) gas.EvmEstimator { return evmEstimator - }, false, geCfg) + }, false, geCfg, nil) require.Equal(t, mockEstimatorName, estimator.Name()) require.Equal(t, mockEvmEstimatorName, evmEstimator.Name()) @@ -185,7 +191,7 @@ func TestWrappedEvmEstimator(t *testing.T) { evmEstimator.On("L1Oracle", mock.Anything).Return(nil).Twice() - estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) err := estimator.Start(ctx) require.NoError(t, err) err = estimator.Close() @@ -193,7 +199,7 @@ func TestWrappedEvmEstimator(t *testing.T) { evmEstimator.On("L1Oracle", mock.Anything).Return(oracle).Twice() - estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) err = estimator.Start(ctx) require.NoError(t, err) err = estimator.Close() @@ -210,11 +216,11 @@ func TestWrappedEvmEstimator(t *testing.T) { oracle.On("Ready").Return(nil).Twice() getEst := func(logger.Logger) gas.EvmEstimator { return evmEstimator } - estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) err := estimator.Ready() require.NoError(t, err) - estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) err = estimator.Ready() require.NoError(t, err) }) @@ -235,7 +241,7 @@ func TestWrappedEvmEstimator(t *testing.T) { oracle.On("HealthReport").Return(map[string]error{oracleKey: oracleError}).Once() getEst := func(logger.Logger) gas.EvmEstimator { return evmEstimator } - estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) report := estimator.HealthReport() require.True(t, pkgerrors.Is(report[evmEstimatorKey], evmEstimatorError)) require.Nil(t, report[oracleKey]) @@ -243,10 +249,160 @@ func TestWrappedEvmEstimator(t *testing.T) { evmEstimator.On("L1Oracle").Return(oracle).Once() - estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) report = estimator.HealthReport() require.True(t, pkgerrors.Is(report[evmEstimatorKey], evmEstimatorError)) require.True(t, pkgerrors.Is(report[oracleKey], oracleError)) require.NotNil(t, report[mockEstimatorName]) }) + + t.Run("GetFee, estimate gas limit enabled, succeeds", func(t *testing.T) { + estimatedGasLimit := uint64(5) + lggr := logger.Test(t) + // expect legacy fee data + dynamicFees := false + geCfg.EstimateLimitF = true + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(estimatedGasLimit, nil).Twice() + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err := estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(estimatedGasLimit)*gas.EstimateGasBuffer), limit) + assert.True(t, legacyFee.Equal(fee.Legacy)) + assert.Nil(t, fee.DynamicTipCap) + assert.Nil(t, fee.DynamicFeeCap) + + // expect dynamic fee data + dynamicFees = true + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err = estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(estimatedGasLimit)*gas.EstimateGasBuffer), limit) + assert.True(t, dynamicFee.FeeCap.Equal(fee.DynamicFeeCap)) + assert.True(t, dynamicFee.TipCap.Equal(fee.DynamicTipCap)) + assert.Nil(t, fee.Legacy) + }) + + t.Run("GetFee, estimate gas limit enabled, estimate exceeds provided limit, returns error", func(t *testing.T) { + estimatedGasLimit := uint64(100) + lggr := logger.Test(t) + // expect legacy fee data + dynamicFees := false + geCfg.EstimateLimitF = true + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(estimatedGasLimit, nil).Twice() + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + _, _, err := estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.ErrorIs(t, err, commonfee.ErrFeeLimitTooLow) + + // expect dynamic fee data + dynamicFees = true + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + _, _, err = estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.ErrorIs(t, err, commonfee.ErrFeeLimitTooLow) + }) + + t.Run("GetFee, estimate gas limit enabled, buffer exceeds provided limit, fallsback to provided limit", func(t *testing.T) { + estimatedGasLimit := uint64(15) // same as provided limit + lggr := logger.Test(t) + dynamicFees := false // expect legacy fee data + geCfg.EstimateLimitF = true + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(estimatedGasLimit, nil).Twice() + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err := estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), limit) + assert.True(t, legacyFee.Equal(fee.Legacy)) + assert.Nil(t, fee.DynamicTipCap) + assert.Nil(t, fee.DynamicFeeCap) + + dynamicFees = true // expect dynamic fee data + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err = estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), limit) + assert.True(t, dynamicFee.FeeCap.Equal(fee.DynamicFeeCap)) + assert.True(t, dynamicFee.TipCap.Equal(fee.DynamicTipCap)) + assert.Nil(t, fee.Legacy) + }) + + t.Run("GetFee, estimate gas limit enabled, RPC fails and fallsback to provided gas limit", func(t *testing.T) { + lggr := logger.Test(t) + // expect legacy fee data + dynamicFees := false + geCfg.EstimateLimitF = true + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(0), errors.New("something broke")).Twice() + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err := estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), limit) + assert.True(t, legacyFee.Equal(fee.Legacy)) + assert.Nil(t, fee.DynamicTipCap) + assert.Nil(t, fee.DynamicFeeCap) + + // expect dynamic fee data + dynamicFees = true + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err = estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), limit) + assert.True(t, dynamicFee.FeeCap.Equal(fee.DynamicFeeCap)) + assert.True(t, dynamicFee.TipCap.Equal(fee.DynamicTipCap)) + assert.Nil(t, fee.Legacy) + }) + + t.Run("GetFee, estimate gas limit enabled, provided fee limit 0, returns uncapped estimation", func(t *testing.T) { + est.On("GetDynamicFee", mock.Anything, mock.Anything). + Return(dynamicFee, nil).Once() + est.On("GetLegacyGas", mock.Anything, mock.Anything, uint64(0), mock.Anything). + Return(legacyFee, uint64(0), nil).Once() + estimatedGasLimit := uint64(100) // same as provided limit + lggr := logger.Test(t) + // expect legacy fee data + dynamicFees := false + geCfg.EstimateLimitF = true + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(estimatedGasLimit, nil).Twice() + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err := estimator.GetFee(ctx, []byte{}, uint64(0), nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(estimatedGasLimit)*gas.EstimateGasBuffer), limit) + assert.True(t, legacyFee.Equal(fee.Legacy)) + assert.Nil(t, fee.DynamicTipCap) + assert.Nil(t, fee.DynamicFeeCap) + + // expect dynamic fee data + dynamicFees = true + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err = estimator.GetFee(ctx, []byte{}, 0, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(estimatedGasLimit)*gas.EstimateGasBuffer), limit) + assert.True(t, dynamicFee.FeeCap.Equal(fee.DynamicFeeCap)) + assert.True(t, dynamicFee.TipCap.Equal(fee.DynamicTipCap)) + assert.Nil(t, fee.Legacy) + }) + + t.Run("GetFee, estimate gas limit enabled, provided fee limit 0, returns error on failure", func(t *testing.T) { + est.On("GetDynamicFee", mock.Anything, mock.Anything). + Return(dynamicFee, nil).Once() + est.On("GetLegacyGas", mock.Anything, mock.Anything, uint64(0), mock.Anything). + Return(legacyFee, uint64(0), nil).Once() + lggr := logger.Test(t) + // expect legacy fee data + dynamicFees := false + geCfg.EstimateLimitF = true + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(0), errors.New("something broke")).Twice() + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + _, _, err := estimator.GetFee(ctx, []byte{}, 0, nil, &fromAddress, &toAddress) + require.Error(t, err) + + // expect dynamic fee data + dynamicFees = true + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + _, _, err = estimator.GetFee(ctx, []byte{}, 0, nil, &fromAddress, &toAddress) + require.Error(t, err) + }) } diff --git a/core/chains/evm/gas/rollups/arbitrum_l1_oracle.go b/core/chains/evm/gas/rollups/arbitrum_l1_oracle.go index c01244db70c..d758dc711e9 100644 --- a/core/chains/evm/gas/rollups/arbitrum_l1_oracle.go +++ b/core/chains/evm/gas/rollups/arbitrum_l1_oracle.go @@ -112,6 +112,10 @@ func (o *arbitrumL1Oracle) Name() string { return o.logger.Name() } +func (o *arbitrumL1Oracle) ChainType(_ context.Context) chaintype.ChainType { + return o.chainType +} + func (o *arbitrumL1Oracle) Start(ctx context.Context) error { return o.StartOnce(o.Name(), func() error { go o.run() diff --git a/core/chains/evm/gas/rollups/l1_oracle.go b/core/chains/evm/gas/rollups/l1_oracle.go index f707fab6841..41951755986 100644 --- a/core/chains/evm/gas/rollups/l1_oracle.go +++ b/core/chains/evm/gas/rollups/l1_oracle.go @@ -26,6 +26,7 @@ type L1Oracle interface { GasPrice(ctx context.Context) (*assets.Wei, error) GetGasCost(ctx context.Context, tx *types.Transaction, blockNum *big.Int) (*assets.Wei, error) + ChainType(ctx context.Context) chaintype.ChainType } type l1OracleClient interface { @@ -43,7 +44,7 @@ const ( PollPeriod = 6 * time.Second ) -var supportedChainTypes = []chaintype.ChainType{chaintype.ChainArbitrum, chaintype.ChainOptimismBedrock, chaintype.ChainKroma, chaintype.ChainScroll, chaintype.ChainZkSync} +var supportedChainTypes = []chaintype.ChainType{chaintype.ChainArbitrum, chaintype.ChainOptimismBedrock, chaintype.ChainKroma, chaintype.ChainScroll, chaintype.ChainZkSync, chaintype.ChainMantle} func IsRollupWithL1Support(chainType chaintype.ChainType) bool { return slices.Contains(supportedChainTypes, chainType) @@ -56,7 +57,7 @@ func NewL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainType chai var l1Oracle L1Oracle var err error switch chainType { - case chaintype.ChainOptimismBedrock, chaintype.ChainKroma, chaintype.ChainScroll: + case chaintype.ChainOptimismBedrock, chaintype.ChainKroma, chaintype.ChainScroll, chaintype.ChainMantle: l1Oracle, err = NewOpStackL1GasOracle(lggr, ethClient, chainType) case chaintype.ChainArbitrum: l1Oracle, err = NewArbitrumL1GasOracle(lggr, ethClient) diff --git a/core/chains/evm/gas/rollups/mocks/l1_oracle.go b/core/chains/evm/gas/rollups/mocks/l1_oracle.go index e82cb4ee903..25bb3a2db59 100644 --- a/core/chains/evm/gas/rollups/mocks/l1_oracle.go +++ b/core/chains/evm/gas/rollups/mocks/l1_oracle.go @@ -7,6 +7,8 @@ import ( assets "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + chaintype "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/chaintype" + context "context" mock "github.com/stretchr/testify/mock" @@ -27,6 +29,52 @@ func (_m *L1Oracle) EXPECT() *L1Oracle_Expecter { return &L1Oracle_Expecter{mock: &_m.Mock} } +// ChainType provides a mock function with given fields: ctx +func (_m *L1Oracle) ChainType(ctx context.Context) chaintype.ChainType { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ChainType") + } + + var r0 chaintype.ChainType + if rf, ok := ret.Get(0).(func(context.Context) chaintype.ChainType); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(chaintype.ChainType) + } + + return r0 +} + +// L1Oracle_ChainType_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChainType' +type L1Oracle_ChainType_Call struct { + *mock.Call +} + +// ChainType is a helper method to define mock.On call +// - ctx context.Context +func (_e *L1Oracle_Expecter) ChainType(ctx interface{}) *L1Oracle_ChainType_Call { + return &L1Oracle_ChainType_Call{Call: _e.mock.On("ChainType", ctx)} +} + +func (_c *L1Oracle_ChainType_Call) Run(run func(ctx context.Context)) *L1Oracle_ChainType_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *L1Oracle_ChainType_Call) Return(_a0 chaintype.ChainType) *L1Oracle_ChainType_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *L1Oracle_ChainType_Call) RunAndReturn(run func(context.Context) chaintype.ChainType) *L1Oracle_ChainType_Call { + _c.Call.Return(run) + return _c +} + // Close provides a mock function with given fields: func (_m *L1Oracle) Close() error { ret := _m.Called() diff --git a/core/chains/evm/gas/rollups/op_l1_oracle.go b/core/chains/evm/gas/rollups/op_l1_oracle.go index 6805cd7095b..11babc5ca5d 100644 --- a/core/chains/evm/gas/rollups/op_l1_oracle.go +++ b/core/chains/evm/gas/rollups/op_l1_oracle.go @@ -50,6 +50,7 @@ type optimismL1Oracle struct { blobBaseFeeCalldata []byte blobBaseFeeScalarCalldata []byte decimalsCalldata []byte + tokenRatioCalldata []byte isEcotoneCalldata []byte isEcotoneMethodAbi abi.ABI isFjordCalldata []byte @@ -87,7 +88,7 @@ const ( // decimals is a hex encoded call to: // `function decimals() public pure returns (uint256);` decimalsMethod = "decimals" - // OPGasOracleAddress is the address of the precompiled contract that exists on Optimism and Base. + // OPGasOracleAddress is the address of the precompiled contract that exists on Optimism, Base and Mantle. OPGasOracleAddress = "0x420000000000000000000000000000000000000F" // KromaGasOracleAddress is the address of the precompiled contract that exists on Kroma. KromaGasOracleAddress = "0x4200000000000000000000000000000000000005" @@ -98,7 +99,7 @@ const ( func NewOpStackL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainType chaintype.ChainType) (*optimismL1Oracle, error) { var precompileAddress string switch chainType { - case chaintype.ChainOptimismBedrock: + case chaintype.ChainOptimismBedrock, chaintype.ChainMantle: precompileAddress = OPGasOracleAddress case chaintype.ChainKroma: precompileAddress = KromaGasOracleAddress @@ -219,6 +220,10 @@ func (o *optimismL1Oracle) Name() string { return o.logger.Name() } +func (o *optimismL1Oracle) ChainType(_ context.Context) chaintype.ChainType { + return o.chainType +} + func (o *optimismL1Oracle) Start(ctx context.Context) error { return o.StartOnce(o.Name(), func() error { go o.run() diff --git a/core/chains/evm/gas/rollups/zkSync_l1_oracle.go b/core/chains/evm/gas/rollups/zkSync_l1_oracle.go index 31d93bc587d..c2941233545 100644 --- a/core/chains/evm/gas/rollups/zkSync_l1_oracle.go +++ b/core/chains/evm/gas/rollups/zkSync_l1_oracle.go @@ -83,6 +83,10 @@ func (o *zkSyncL1Oracle) Name() string { return o.logger.Name() } +func (o *zkSyncL1Oracle) ChainType(_ context.Context) chaintype.ChainType { + return o.chainType +} + func (o *zkSyncL1Oracle) Start(ctx context.Context) error { return o.StartOnce(o.Name(), func() error { go o.run() diff --git a/core/chains/evm/txmgr/attempts.go b/core/chains/evm/txmgr/attempts.go index 8566adcb5c5..c284ee77bd4 100644 --- a/core/chains/evm/txmgr/attempts.go +++ b/core/chains/evm/txmgr/attempts.go @@ -58,7 +58,7 @@ func (c *evmTxAttemptBuilder) NewTxAttempt(ctx context.Context, etx Tx, lggr log // used for L2 re-estimation on broadcasting (note EIP1559 must be disabled otherwise this will fail with mismatched fees + tx type) func (c *evmTxAttemptBuilder) NewTxAttemptWithType(ctx context.Context, etx Tx, lggr logger.Logger, txType int, opts ...feetypes.Opt) (attempt TxAttempt, fee gas.EvmFee, feeLimit uint64, retryable bool, err error) { keySpecificMaxGasPriceWei := c.feeConfig.PriceMaxKey(etx.FromAddress) - fee, feeLimit, err = c.EvmFeeEstimator.GetFee(ctx, etx.EncodedPayload, etx.FeeLimit, keySpecificMaxGasPriceWei, opts...) + fee, feeLimit, err = c.EvmFeeEstimator.GetFee(ctx, etx.EncodedPayload, etx.FeeLimit, keySpecificMaxGasPriceWei, &etx.FromAddress, &etx.ToAddress, opts...) if err != nil { return attempt, fee, feeLimit, true, pkgerrors.Wrap(err, "failed to get fee") // estimator errors are retryable } @@ -71,8 +71,8 @@ func (c *evmTxAttemptBuilder) NewTxAttemptWithType(ctx context.Context, etx Tx, // used in the txm broadcaster + confirmer when tx ix rejected for too low fee or is not included in a timely manner func (c *evmTxAttemptBuilder) NewBumpTxAttempt(ctx context.Context, etx Tx, previousAttempt TxAttempt, priorAttempts []TxAttempt, lggr logger.Logger) (attempt TxAttempt, bumpedFee gas.EvmFee, bumpedFeeLimit uint64, retryable bool, err error) { keySpecificMaxGasPriceWei := c.feeConfig.PriceMaxKey(etx.FromAddress) - - bumpedFee, bumpedFeeLimit, err = c.EvmFeeEstimator.BumpFee(ctx, previousAttempt.TxFee, etx.FeeLimit, keySpecificMaxGasPriceWei, newEvmPriorAttempts(priorAttempts)) + // Use the fee limit from the previous attempt to maintain limits adjusted for 2D fees or by estimation + bumpedFee, bumpedFeeLimit, err = c.EvmFeeEstimator.BumpFee(ctx, previousAttempt.TxFee, previousAttempt.ChainSpecificFeeLimit, keySpecificMaxGasPriceWei, newEvmPriorAttempts(priorAttempts)) if err != nil { return attempt, bumpedFee, bumpedFeeLimit, true, pkgerrors.Wrap(err, "failed to bump fee") // estimator errors are retryable } diff --git a/core/chains/evm/txmgr/attempts_test.go b/core/chains/evm/txmgr/attempts_test.go index 6be8cd7067b..5c43368fcc4 100644 --- a/core/chains/evm/txmgr/attempts_test.go +++ b/core/chains/evm/txmgr/attempts_test.go @@ -339,7 +339,7 @@ func TestTxm_NewCustomTxAttempt_NonRetryableErrors(t *testing.T) { func TestTxm_EvmTxAttemptBuilder_RetryableEstimatorError(t *testing.T) { est := gasmocks.NewEvmFeeEstimator(t) - est.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{}, uint64(0), pkgerrors.New("fail")) + est.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{}, uint64(0), pkgerrors.New("fail")) est.On("BumpFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{}, uint64(0), pkgerrors.New("fail")) kst := ksmocks.NewEth(t) diff --git a/core/chains/evm/txmgr/broadcaster_test.go b/core/chains/evm/txmgr/broadcaster_test.go index cbbe522ba10..514d5331592 100644 --- a/core/chains/evm/txmgr/broadcaster_test.go +++ b/core/chains/evm/txmgr/broadcaster_test.go @@ -29,8 +29,10 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" commonclient "github.com/smartcontractkit/chainlink/v2/common/client" + commmonfee "github.com/smartcontractkit/chainlink/v2/common/fee" txmgrcommon "github.com/smartcontractkit/chainlink/v2/common/txmgr" txmgrtypes "github.com/smartcontractkit/chainlink/v2/common/txmgr/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" evmconfig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config" @@ -69,7 +71,7 @@ func NewTestEthBroadcaster( estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { return gas.NewFixedPriceEstimator(config.EVM().GasEstimator(), nil, ge.BlockHistory(), lggr, nil) - }, ge.EIP1559DynamicFees(), ge) + }, ge.EIP1559DynamicFees(), ge, ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, keyStore, estimator) ethBroadcaster := txmgrcommon.NewBroadcaster(txStore, txmgr.NewEvmTxmClient(ethClient, nil), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(config.EVM().GasEstimator()), config.EVM().Transactions(), gconfig.Database().Listener(), keyStore, txBuilder, nonceTracker, lggr, checkerFactory, nonceAutoSync, "") @@ -642,7 +644,7 @@ func TestEthBroadcaster_ProcessUnstartedEthTxs_OptimisticLockingOnEthTx(t *testi chStartEstimate := make(chan struct{}) chBlock := make(chan struct{}) - estimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, ccfg.EVM().GasEstimator().PriceMaxKey(fromAddress)).Return(gas.EvmFee{Legacy: assets.GWei(32)}, uint64(500), nil).Run(func(_ mock.Arguments) { + estimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, ccfg.EVM().GasEstimator().PriceMaxKey(fromAddress), mock.Anything, mock.Anything).Return(gas.EvmFee{Legacy: assets.GWei(32)}, uint64(500), nil).Run(func(_ mock.Arguments) { close(chStartEstimate) <-chBlock }).Once() @@ -1177,7 +1179,7 @@ func TestEthBroadcaster_ProcessUnstartedEthTxs_Errors(t *testing.T) { t.Run("callback set by ctor", func(t *testing.T) { estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { return gas.NewFixedPriceEstimator(evmcfg.EVM().GasEstimator(), nil, evmcfg.EVM().GasEstimator().BlockHistory(), lggr, nil) - }, evmcfg.EVM().GasEstimator().EIP1559DynamicFees(), evmcfg.EVM().GasEstimator()) + }, evmcfg.EVM().GasEstimator().EIP1559DynamicFees(), evmcfg.EVM().GasEstimator(), ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), evmcfg.EVM().GasEstimator(), ethKeyStore, estimator) localNextNonce = getLocalNextNonce(t, nonceTracker, fromAddress) eb2 := txmgr.NewEvmBroadcaster(txStore, txmClient, txmgr.NewEvmTxmConfig(evmcfg.EVM()), txmgr.NewEvmTxmFeeConfig(evmcfg.EVM().GasEstimator()), evmcfg.EVM().Transactions(), cfg.Database().Listener(), ethKeyStore, txBuilder, lggr, &testCheckerFactory{}, false, "") @@ -1666,6 +1668,73 @@ func TestEthBroadcaster_ProcessUnstartedEthTxs_Errors(t *testing.T) { }) } +func TestEthBroadcaster_ProcessUnstartedEthTxs_GasEstimationError(t *testing.T) { + toAddress := testutils.NewAddress() + value := big.Int(assets.NewEthValue(142)) + gasLimit := uint64(242) + encodedPayload := []byte{0, 1} + + db := pgtest.NewSqlxDB(t) + cfg := configtest.NewTestGeneralConfig(t) + cfg.EVMConfigs()[0].GasEstimator.EstimateLimit = ptr(true) // Enabled gas limit estimation + limitMultiplier := float32(1.25) + cfg.EVMConfigs()[0].GasEstimator.LimitMultiplier = ptr(decimal.NewFromFloat32(limitMultiplier)) // Set LimitMultiplier for the buffer + txStore := cltest.NewTestTxStore(t, db) + + ethKeyStore := cltest.NewKeyStore(t, db).Eth() + _, fromAddress := cltest.MustInsertRandomKey(t, ethKeyStore) + + config := evmtest.NewChainScopedConfig(t, cfg) + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("PendingNonceAt", mock.Anything, fromAddress).Return(uint64(0), nil).Once() + lggr := logger.Test(t) + txmClient := txmgr.NewEvmTxmClient(ethClient, nil) + nonceTracker := txmgr.NewNonceTracker(lggr, txStore, txmClient) + ge := config.EVM().GasEstimator() + estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { + return gas.NewFixedPriceEstimator(ge, nil, ge.BlockHistory(), lggr, nil) + }, ge.EIP1559DynamicFees(), ge, ethClient) + txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, ethKeyStore, estimator) + eb := txmgrcommon.NewBroadcaster(txStore, txmgr.NewEvmTxmClient(ethClient, nil), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(config.EVM().GasEstimator()), config.EVM().Transactions(), cfg.Database().Listener(), ethKeyStore, txBuilder, nonceTracker, lggr, &testCheckerFactory{}, false, "") + + // Mark instance as test + eb.XXXTestDisableUnstartedTxAutoProcessing() + servicetest.Run(t, eb) + ctx := tests.Context(t) + t.Run("gas limit lowered after estimation", func(t *testing.T) { + estimatedGasLimit := uint64(100) + etx := mustCreateUnstartedTx(t, txStore, fromAddress, toAddress, encodedPayload, gasLimit, value, testutils.FixtureChainID) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(estimatedGasLimit, nil).Once() + ethClient.On("SendTransactionReturnCode", mock.Anything, mock.MatchedBy(func(tx *gethTypes.Transaction) bool { + return tx.Nonce() == uint64(0) + }), fromAddress).Return(commonclient.Successful, nil).Once() + + // Do the thing + retryable, err := eb.ProcessUnstartedTxs(ctx, fromAddress) + assert.NoError(t, err) + assert.False(t, retryable) + + dbEtx, err := txStore.FindTxWithAttempts(ctx, etx.ID) + require.NoError(t, err) + attempt := dbEtx.TxAttempts[0] + require.Equal(t, uint64(float32(estimatedGasLimit)*gas.EstimateGasBuffer), attempt.ChainSpecificFeeLimit) + }) + t.Run("provided gas limit too low, transaction marked as fatal error", func(t *testing.T) { + etx := mustCreateUnstartedTx(t, txStore, fromAddress, toAddress, encodedPayload, gasLimit, value, testutils.FixtureChainID) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(float32(gasLimit)*limitMultiplier)+1, nil).Once() + + // Do the thing + retryable, err := eb.ProcessUnstartedTxs(ctx, fromAddress) + assert.NoError(t, err) + assert.False(t, retryable) + + dbEtx, err := txStore.FindTxWithAttempts(ctx, etx.ID) + require.NoError(t, err) + require.Equal(t, txmgrcommon.TxFatalError, dbEtx.State) + require.Equal(t, commmonfee.ErrFeeLimitTooLow.Error(), dbEtx.Error.String) + }) +} + func TestEthBroadcaster_ProcessUnstartedEthTxs_KeystoreErrors(t *testing.T) { toAddress := gethCommon.HexToAddress("0x6C03DDA95a2AEd917EeCc6eddD4b9D16E6380411") value := big.Int(assets.NewEthValue(142)) @@ -1760,15 +1829,15 @@ func TestEthBroadcaster_SyncNonce(t *testing.T) { kst := cltest.NewKeyStore(t, db).Eth() _, fromAddress := cltest.RandomKey{Disabled: false}.MustInsertWithState(t, kst) + ethClient := testutils.NewEthClientMockWithDefaultChain(t) estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { return gas.NewFixedPriceEstimator(evmcfg.EVM().GasEstimator(), nil, evmcfg.EVM().GasEstimator().BlockHistory(), lggr, nil) - }, evmcfg.EVM().GasEstimator().EIP1559DynamicFees(), evmcfg.EVM().GasEstimator()) + }, evmcfg.EVM().GasEstimator().EIP1559DynamicFees(), evmcfg.EVM().GasEstimator(), ethClient) checkerFactory := &testCheckerFactory{} ge := evmcfg.EVM().GasEstimator() t.Run("does nothing if nonce sync is disabled", func(t *testing.T) { - ethClient := testutils.NewEthClientMockWithDefaultChain(t) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, kst, estimator) kst := ksmocks.NewEth(t) @@ -1838,7 +1907,7 @@ func TestEthBroadcaster_HederaBroadcastValidation(t *testing.T) { ge := evmcfg.EVM().GasEstimator() estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { return gas.NewFixedPriceEstimator(evmcfg.EVM().GasEstimator(), nil, ge.BlockHistory(), lggr, nil) - }, ge.EIP1559DynamicFees(), ge) + }, ge.EIP1559DynamicFees(), ge, ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, ethKeyStore, estimator) checkerFactory := &txmgr.CheckerFactory{Client: ethClient} ctx := tests.Context(t) diff --git a/core/chains/evm/txmgr/confirmer_test.go b/core/chains/evm/txmgr/confirmer_test.go index 45d7ac9e125..82b668f168a 100644 --- a/core/chains/evm/txmgr/confirmer_test.go +++ b/core/chains/evm/txmgr/confirmer_test.go @@ -128,7 +128,7 @@ func TestEthConfirmer_Lifecycle(t *testing.T) { newEst := func(logger.Logger) gas.EvmEstimator { return estimator } lggr := logger.Test(t) ge := config.EVM().GasEstimator() - feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge) + feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge, ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, ethKeyStore, feeEstimator) stuckTxDetector := txmgr.NewStuckTxDetector(lggr, testutils.FixtureChainID, "", assets.NewWei(assets.NewEth(100).ToInt()), config.EVM().Transactions().AutoPurge(), feeEstimator, txStore, ethClient) ec := txmgr.NewEvmConfirmer(txStore, txmgr.NewEvmTxmClient(ethClient, nil), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(ge), config.EVM().Transactions(), gconfig.Database(), ethKeyStore, txBuilder, lggr, stuckTxDetector) @@ -1646,7 +1646,7 @@ func TestEthConfirmer_RebroadcastWhereNecessary_WithConnectivityCheck(t *testing newEst := func(logger.Logger) gas.EvmEstimator { return estimator } estimator.On("BumpLegacyGas", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, uint64(0), pkgerrors.Wrapf(commonfee.ErrConnectivity, "transaction...")) ge := ccfg.EVM().GasEstimator() - feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge) + feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge, ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, kst, feeEstimator) addresses := []gethCommon.Address{fromAddress} kst.On("EnabledAddressesForChain", mock.Anything, &cltest.FixtureChainID).Return(addresses, nil).Maybe() @@ -1695,7 +1695,7 @@ func TestEthConfirmer_RebroadcastWhereNecessary_WithConnectivityCheck(t *testing newEst := func(logger.Logger) gas.EvmEstimator { return estimator } // Create confirmer with necessary state ge := ccfg.EVM().GasEstimator() - feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge) + feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge, ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, kst, feeEstimator) addresses := []gethCommon.Address{fromAddress} kst.On("EnabledAddressesForChain", mock.Anything, &cltest.FixtureChainID).Return(addresses, nil).Maybe() @@ -3195,7 +3195,7 @@ func TestEthConfirmer_ProcessStuckTransactions(t *testing.T) { fee := gas.EvmFee{Legacy: marketGasPrice} bumpedLegacy := assets.GWei(30) bumpedFee := gas.EvmFee{Legacy: bumpedLegacy} - feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything).Return(fee, uint64(0), nil) + feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything, mock.Anything, mock.Anything).Return(fee, uint64(0), nil) feeEstimator.On("BumpFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(bumpedFee, uint64(10_000), nil) autoPurgeThreshold := uint32(5) autoPurgeMinAttempts := uint32(3) @@ -3286,7 +3286,7 @@ func newEthConfirmer(t testing.TB, txStore txmgr.EvmTxStore, ethClient client.Cl ge := config.EVM().GasEstimator() estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { return gas.NewFixedPriceEstimator(ge, nil, ge.BlockHistory(), lggr, nil) - }, ge.EIP1559DynamicFees(), ge) + }, ge.EIP1559DynamicFees(), ge, ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, ks, estimator) stuckTxDetector := txmgr.NewStuckTxDetector(lggr, testutils.FixtureChainID, "", assets.NewWei(assets.NewEth(100).ToInt()), config.EVM().Transactions().AutoPurge(), estimator, txStore, ethClient) ec := txmgr.NewEvmConfirmer(txStore, txmgr.NewEvmTxmClient(ethClient, nil), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(ge), config.EVM().Transactions(), gconfig.Database(), ks, txBuilder, lggr, stuckTxDetector) diff --git a/core/chains/evm/txmgr/stuck_tx_detector.go b/core/chains/evm/txmgr/stuck_tx_detector.go index 5d621dc0b20..d756e95058e 100644 --- a/core/chains/evm/txmgr/stuck_tx_detector.go +++ b/core/chains/evm/txmgr/stuck_tx_detector.go @@ -25,7 +25,7 @@ import ( ) type stuckTxDetectorGasEstimator interface { - GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee gas.EvmFee, chainSpecificFeeLimit uint64, err error) + GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress, toAddress *common.Address, opts ...feetypes.Opt) (fee gas.EvmFee, chainSpecificFeeLimit uint64, err error) } type stuckTxDetectorClient interface { @@ -199,7 +199,7 @@ func (d *stuckTxDetector) detectStuckTransactionsHeuristic(ctx context.Context, defer d.purgeBlockNumLock.RUnlock() // Get gas price from internal gas estimator // Send with max gas price time 2 to prevent the results from being capped. Need the market gas price here. - marketGasPrice, _, err := d.gasEstimator.GetFee(ctx, []byte{}, 0, d.maxPrice.Mul(big.NewInt(2))) + marketGasPrice, _, err := d.gasEstimator.GetFee(ctx, []byte{}, 0, d.maxPrice.Mul(big.NewInt(2)), nil, nil) if err != nil { return txs, fmt.Errorf("failed to get market gas price for overflow detection: %w", err) } @@ -215,6 +215,20 @@ func (d *stuckTxDetector) detectStuckTransactionsHeuristic(ctx context.Context, } // Tx attempts are loaded from newest to oldest oldestBroadcastAttempt, newestBroadcastAttempt, broadcastedAttemptsCount := findBroadcastedAttempts(tx) + d.lggr.Debugf("found %d broadcasted attempts for tx id %d in stuck transaction heuristic", broadcastedAttemptsCount, tx.ID) + + // attempt shouldn't be nil as we validated in FindUnconfirmedTxWithLowestNonce, but added anyway for a "belts and braces" approach + if oldestBroadcastAttempt == nil || newestBroadcastAttempt == nil { + d.lggr.Debugw("failed to find broadcast attempt for tx in stuck transaction heuristic", "tx", tx) + continue + } + + // sanity check + if oldestBroadcastAttempt.BroadcastBeforeBlockNum == nil { + d.lggr.Debugw("BroadcastBeforeBlockNum was not set for broadcast attempt in stuck transaction heuristic", "attempt", oldestBroadcastAttempt) + continue + } + // 2. Check if Threshold amount of blocks have passed since the oldest attempt's broadcast block num if *oldestBroadcastAttempt.BroadcastBeforeBlockNum > blockNum-int64(*d.cfg.Threshold()) { continue @@ -244,17 +258,18 @@ func compareGasFees(attemptGas gas.EvmFee, marketGas gas.EvmFee) int { } // Assumes tx attempts are loaded newest to oldest -func findBroadcastedAttempts(tx Tx) (oldestAttempt TxAttempt, newestAttempt TxAttempt, broadcastedCount uint32) { +func findBroadcastedAttempts(tx Tx) (oldestAttempt *TxAttempt, newestAttempt *TxAttempt, broadcastedCount uint32) { foundNewest := false - for _, attempt := range tx.TxAttempts { + for i := range tx.TxAttempts { + attempt := tx.TxAttempts[i] if attempt.State != types.TxAttemptBroadcast { continue } if !foundNewest { - newestAttempt = attempt + newestAttempt = &attempt foundNewest = true } - oldestAttempt = attempt + oldestAttempt = &attempt broadcastedCount++ } return diff --git a/core/chains/evm/txmgr/stuck_tx_detector_test.go b/core/chains/evm/txmgr/stuck_tx_detector_test.go index def49f8e113..c653d297e8d 100644 --- a/core/chains/evm/txmgr/stuck_tx_detector_test.go +++ b/core/chains/evm/txmgr/stuck_tx_detector_test.go @@ -73,7 +73,7 @@ func TestStuckTxDetector_LoadPurgeBlockNumMap(t *testing.T) { feeEstimator := gasmocks.NewEvmFeeEstimator(t) marketGasPrice := assets.GWei(15) fee := gas.EvmFee{Legacy: marketGasPrice} - feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything).Return(fee, uint64(0), nil) + feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything, mock.Anything, mock.Anything).Return(fee, uint64(0), nil) autoPurgeThreshold := uint32(5) autoPurgeMinAttempts := uint32(3) autoPurgeCfg := testAutoPurgeConfig{ @@ -194,7 +194,7 @@ func TestStuckTxDetector_DetectStuckTransactionsHeuristic(t *testing.T) { // Return 10 gwei as market gas price marketGasPrice := tenGwei fee := gas.EvmFee{Legacy: marketGasPrice} - feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything).Return(fee, uint64(0), nil) + feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything, mock.Anything, mock.Anything).Return(fee, uint64(0), nil) ethClient := testutils.NewEthClientMockWithDefaultChain(t) autoPurgeThreshold := uint32(5) autoPurgeMinAttempts := uint32(3) @@ -278,6 +278,15 @@ func TestStuckTxDetector_DetectStuckTransactionsHeuristic(t *testing.T) { require.NoError(t, err) require.Len(t, txs, 1) }) + + t.Run("detects stuck transaction with empty BroadcastBeforeBlockNum in attempts will be skipped without panic", func(t *testing.T) { + _, fromAddress := cltest.MustInsertRandomKey(t, ethKeyStore) + enabledAddresses := []common.Address{fromAddress} + mustInsertUnconfirmedTxWithBroadcastAttemptsContainsEmptyBroadcastBeforeBlockNum(t, txStore, 0, fromAddress, autoPurgeMinAttempts, marketGasPrice.Add(oneGwei)) + txs, err := stuckTxDetector.DetectStuckTransactions(ctx, enabledAddresses, blockNum) + require.NoError(t, err) + require.Len(t, txs, 0) + }) } func TestStuckTxDetector_DetectStuckTransactionsZkEVM(t *testing.T) { @@ -435,6 +444,23 @@ func mustInsertUnconfirmedTxWithBroadcastAttempts(t *testing.T, txStore txmgr.Te return etx } +// helper function for edge case where broadcast attempt contains empty pointer +func mustInsertUnconfirmedTxWithBroadcastAttemptsContainsEmptyBroadcastBeforeBlockNum(t *testing.T, txStore txmgr.TestEvmTxStore, nonce int64, fromAddress common.Address, numAttempts uint32, latestGasPrice *assets.Wei) txmgr.Tx { + ctx := tests.Context(t) + etx := cltest.MustInsertUnconfirmedEthTx(t, txStore, nonce, fromAddress) + // Insert attempts from oldest to newest + for i := int64(numAttempts - 1); i >= 0; i-- { + attempt := cltest.NewLegacyEthTxAttempt(t, etx.ID) + attempt.State = txmgrtypes.TxAttemptBroadcast + attempt.BroadcastBeforeBlockNum = nil + attempt.TxFee = gas.EvmFee{Legacy: latestGasPrice.Sub(assets.NewWeiI(i))} + require.NoError(t, txStore.InsertTxAttempt(ctx, &attempt)) + } + etx, err := txStore.FindTxWithAttempts(ctx, etx.ID) + require.NoError(t, err) + return etx +} + func mustInsertFatalErrorTxWithError(t *testing.T, txStore txmgr.TestEvmTxStore, nonce int64, fromAddress common.Address, blockNum int64) txmgr.Tx { etx := cltest.NewEthTx(fromAddress) etx.State = txmgrcommon.TxFatalError diff --git a/core/chains/evm/txmgr/test_helpers.go b/core/chains/evm/txmgr/test_helpers.go index 3b3584a988b..b1317cb421b 100644 --- a/core/chains/evm/txmgr/test_helpers.go +++ b/core/chains/evm/txmgr/test_helpers.go @@ -73,6 +73,10 @@ func (g *TestGasEstimatorConfig) BlockHistory() evmconfig.BlockHistory { return &TestBlockHistoryConfig{} } +func (g *TestGasEstimatorConfig) FeeHistory() evmconfig.FeeHistory { + return &TestFeeHistoryConfig{} +} + func (g *TestGasEstimatorConfig) EIP1559DynamicFees() bool { return false } func (g *TestGasEstimatorConfig) LimitDefault() uint64 { return 42 } func (g *TestGasEstimatorConfig) BumpPercent() uint16 { return 42 } @@ -89,6 +93,7 @@ func (g *TestGasEstimatorConfig) LimitTransfer() uint64 { return 42 } func (g *TestGasEstimatorConfig) PriceMax() *assets.Wei { return assets.NewWeiI(42) } func (g *TestGasEstimatorConfig) PriceMin() *assets.Wei { return assets.NewWeiI(42) } func (g *TestGasEstimatorConfig) Mode() string { return "FixedPrice" } +func (g *TestGasEstimatorConfig) EstimateLimit() bool { return false } func (g *TestGasEstimatorConfig) LimitJobType() evmconfig.LimitJobType { return &TestLimitJobTypeConfig{} } @@ -120,6 +125,12 @@ func (b *TestBlockHistoryConfig) BlockHistorySize() uint16 { return 42 func (b *TestBlockHistoryConfig) EIP1559FeeCapBufferBlocks() uint16 { return 42 } func (b *TestBlockHistoryConfig) TransactionPercentile() uint16 { return 42 } +type TestFeeHistoryConfig struct { + evmconfig.FeeHistory +} + +func (b *TestFeeHistoryConfig) CacheTimeout() time.Duration { return 0 * time.Second } + type transactionsConfig struct { evmconfig.Transactions e *TestEvmConfig diff --git a/core/config/docs/chains-evm.toml b/core/config/docs/chains-evm.toml index 73ea0ebb357..bffc9ddf0cc 100644 --- a/core/config/docs/chains-evm.toml +++ b/core/config/docs/chains-evm.toml @@ -194,6 +194,8 @@ LimitMax = 8_000_000 # Default LimitMultiplier = '1.0' # Default # LimitTransfer is the gas limit used for an ordinary ETH transfer. LimitTransfer = 21_000 # Default +# EstimateLimit enables estimating gas limits for transactions. This feature respects the gas limit provided during transaction creation as an upper bound. +EstimateLimit = false # Default # BumpMin is the minimum fixed amount of wei by which gas is bumped on each transaction attempt. BumpMin = '5 gwei' # Default # BumpPercent is the percentage by which to bump gas on a transaction that has exceeded `BumpThreshold`. The larger of `BumpPercent` and `BumpMin` is taken for gas bumps. @@ -310,6 +312,15 @@ EIP1559FeeCapBufferBlocks = 13 # Example # Setting it lower will tend to set lower gas prices. TransactionPercentile = 60 # Default +[EVM.GasEstimator.FeeHistory] +# CacheTimeout is the time to wait in order to refresh the cached values stored in the FeeHistory estimator. A small jitter is applied so the timeout won't be exactly the same each time. +# +# You want this value to be close to the block time. For slower chains, like Ethereum, you can set it to 12s, the same as the block time. For faster chains you can skip a block or two +# and set it to two times the block time i.e. on Optimism you can set it to 4s. Ideally, you don't want to go lower than 1s since the RTT times of the RPC requests will be comparable to +# the timeout. The estimator is already adding a buffer to account for a potential increase in prices within one or two blocks. On the other hand, slower frequency will fail to refresh +# the prices and end up in stale values. +CacheTimeout = '10s' # Default + # The head tracker continually listens for new heads from the chain. # # In addition to these settings, it log warnings if `EVM.NoNewHeadsThreshold` is exceeded without any new blocks being emitted. diff --git a/core/internal/features/features_test.go b/core/internal/features/features_test.go index 12595973791..7be0307798f 100644 --- a/core/internal/features/features_test.go +++ b/core/internal/features/features_test.go @@ -1339,7 +1339,7 @@ func TestIntegration_BlockHistoryEstimator(t *testing.T) { chain := evmtest.MustGetDefaultChain(t, legacyChains) estimator := chain.GasEstimator() - gasPrice, gasLimit, err := estimator.GetFee(testutils.Context(t), nil, 500_000, maxGasPrice) + gasPrice, gasLimit, err := estimator.GetFee(testutils.Context(t), nil, 500_000, maxGasPrice, nil, nil) require.NoError(t, err) assert.Equal(t, uint64(500000), gasLimit) assert.Equal(t, "41.5 gwei", gasPrice.Legacy.String()) @@ -1360,7 +1360,7 @@ func TestIntegration_BlockHistoryEstimator(t *testing.T) { newHeads.TrySend(h43) gomega.NewWithT(t).Eventually(func() string { - gasPrice, _, err := estimator.GetFee(testutils.Context(t), nil, 500000, maxGasPrice) + gasPrice, _, err := estimator.GetFee(testutils.Context(t), nil, 500000, maxGasPrice, nil, nil) require.NoError(t, err) return gasPrice.Legacy.String() }, testutils.WaitTimeout(t), cltest.DBPollingInterval).Should(gomega.Equal("45 gwei")) diff --git a/core/scripts/ccip/manual-execution/go.mod b/core/scripts/ccip/manual-execution/go.mod index 8d646c03f88..356cf7d8dc8 100644 --- a/core/scripts/ccip/manual-execution/go.mod +++ b/core/scripts/ccip/manual-execution/go.mod @@ -5,7 +5,7 @@ go 1.20 require ( github.com/ethereum/go-ethereum v1.11.3 github.com/pkg/errors v0.9.1 - github.com/smartcontractkit/chain-selectors v1.0.21 + github.com/smartcontractkit/chain-selectors v1.0.27 go.uber.org/multierr v1.1.0 golang.org/x/crypto v0.1.0 ) diff --git a/core/scripts/ccip/manual-execution/go.sum b/core/scripts/ccip/manual-execution/go.sum index d79872f13fc..1e38a8a5e0b 100644 --- a/core/scripts/ccip/manual-execution/go.sum +++ b/core/scripts/ccip/manual-execution/go.sum @@ -70,8 +70,8 @@ github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/smartcontractkit/chain-selectors v1.0.21 h1:KCR9SA7PhOexaBzFieHoLv1WonwhVOPtOStpqTmLC4E= -github.com/smartcontractkit/chain-selectors v1.0.21/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= +github.com/smartcontractkit/chain-selectors v1.0.27 h1:VE/ftX9Aae4gnw67yR1raKi+30iWKL/sWq8uyiLHM8k= +github.com/smartcontractkit/chain-selectors v1.0.27/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 13ed1c4aa9d..2a8aaff9dd0 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -22,7 +22,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.17.0 github.com/shopspring/decimal v1.4.0 - github.com/smartcontractkit/chain-selectors v1.0.21 + github.com/smartcontractkit/chain-selectors v1.0.27 github.com/smartcontractkit/chainlink-automation v1.0.4 github.com/smartcontractkit/chainlink-common v0.2.1-0.20240717132349-ee5af9b79834 github.com/smartcontractkit/chainlink/v2 v2.0.0-00010101000000-000000000000 diff --git a/core/scripts/go.sum b/core/scripts/go.sum index 468c13a6709..38c63819f85 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1066,8 +1066,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartcontractkit/chain-selectors v1.0.21 h1:KCR9SA7PhOexaBzFieHoLv1WonwhVOPtOStpqTmLC4E= -github.com/smartcontractkit/chain-selectors v1.0.21/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= +github.com/smartcontractkit/chain-selectors v1.0.27 h1:VE/ftX9Aae4gnw67yR1raKi+30iWKL/sWq8uyiLHM8k= +github.com/smartcontractkit/chain-selectors v1.0.27/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8umfIfVVlwC7+n5izbLSFgjw8= github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= github.com/smartcontractkit/chainlink-ccip v0.0.0-20240816163757-48726fd8165f h1:lQZBOjeYFpCdk0mGQUhbrJipd00tu49xK4zSijC/9Co= diff --git a/core/services/chainlink/config_test.go b/core/services/chainlink/config_test.go index e9ed223b9b3..9afc0aa9428 100644 --- a/core/services/chainlink/config_test.go +++ b/core/services/chainlink/config_test.go @@ -520,6 +520,7 @@ func TestConfig_Marshal(t *testing.T) { LimitMax: ptr[uint64](17), LimitMultiplier: mustDecimal("1.234"), LimitTransfer: ptr[uint64](100), + EstimateLimit: ptr(false), TipCapDefault: assets.NewWeiI(2), TipCapMin: assets.NewWeiI(1), PriceDefault: assets.NewWeiI(math.MaxInt64), @@ -543,6 +544,9 @@ func TestConfig_Marshal(t *testing.T) { EIP1559FeeCapBufferBlocks: ptr[uint16](13), TransactionPercentile: ptr[uint16](15), }, + FeeHistory: evmcfg.FeeHistoryEstimator{ + CacheTimeout: &second, + }, }, KeySpecific: []evmcfg.KeySpecific{ @@ -1024,6 +1028,7 @@ LimitDefault = 12 LimitMax = 17 LimitMultiplier = '1.234' LimitTransfer = 100 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 10 BumpThreshold = 6 @@ -1049,6 +1054,9 @@ CheckInclusionPercentile = 19 EIP1559FeeCapBufferBlocks = 13 TransactionPercentile = 15 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '1s' + [EVM.HeadTracker] HistoryDepth = 15 MaxBufferSize = 17 @@ -1304,7 +1312,7 @@ func TestConfig_Validate(t *testing.T) { - 1: 10 errors: - ChainType: invalid value (Foo): must not be set with this chain id - Nodes: missing: must have at least one node - - ChainType: invalid value (Foo): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync or omitted + - ChainType: invalid value (Foo): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, mantle, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync or omitted - HeadTracker.HistoryDepth: invalid value (30): must be greater than or equal to FinalizedBlockOffset - GasEstimator.BumpThreshold: invalid value (0): cannot be 0 if auto-purge feature is enabled for Foo - Transactions.AutoPurge.Threshold: missing: needs to be set if auto-purge feature is enabled for Foo @@ -1317,7 +1325,7 @@ func TestConfig_Validate(t *testing.T) { - 2: 5 errors: - ChainType: invalid value (Arbitrum): only "optimismBedrock" can be used with this chain id - Nodes: missing: must have at least one node - - ChainType: invalid value (Arbitrum): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync or omitted + - ChainType: invalid value (Arbitrum): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, mantle, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync or omitted - FinalityDepth: invalid value (0): must be greater than or equal to 1 - MinIncomingConfirmations: invalid value (0): must be greater than or equal to 1 - 3.Nodes: 5 errors: diff --git a/core/services/chainlink/testdata/config-full.toml b/core/services/chainlink/testdata/config-full.toml index 9df2e705081..c10d59f339b 100644 --- a/core/services/chainlink/testdata/config-full.toml +++ b/core/services/chainlink/testdata/config-full.toml @@ -316,6 +316,7 @@ LimitDefault = 12 LimitMax = 17 LimitMultiplier = '1.234' LimitTransfer = 100 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 10 BumpThreshold = 6 @@ -341,6 +342,9 @@ CheckInclusionPercentile = 19 EIP1559FeeCapBufferBlocks = 13 TransactionPercentile = 15 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '1s' + [EVM.HeadTracker] HistoryDepth = 15 MaxBufferSize = 17 diff --git a/core/services/chainlink/testdata/config-multi-chain-effective.toml b/core/services/chainlink/testdata/config-multi-chain-effective.toml index e560099c2c3..edb8d0a2490 100644 --- a/core/services/chainlink/testdata/config-multi-chain-effective.toml +++ b/core/services/chainlink/testdata/config-multi-chain-effective.toml @@ -303,6 +303,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -318,6 +319,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -403,6 +407,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -418,6 +423,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -497,6 +505,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -512,6 +521,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 diff --git a/core/services/keeper/upkeep_executer_test.go b/core/services/keeper/upkeep_executer_test.go index c70a92c725c..55926242a2a 100644 --- a/core/services/keeper/upkeep_executer_test.go +++ b/core/services/keeper/upkeep_executer_test.go @@ -48,7 +48,7 @@ func mockEstimator(t *testing.T) gas.EvmFeeEstimator { // note: estimator will only return 1 of legacy or dynamic fees (not both) // assumed to call legacy estimator only estimator := gasmocks.NewEvmFeeEstimator(t) - estimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(gas.EvmFee{ + estimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(gas.EvmFee{ Legacy: assets.GWei(60), }, uint32(60), nil) return estimator diff --git a/core/services/ocr2/plugins/ccip/ccipcommit/ocr2_test.go b/core/services/ocr2/plugins/ccip/ccipcommit/ocr2_test.go index f1ca4d91cdf..b02b7138a16 100644 --- a/core/services/ocr2/plugins/ccip/ccipcommit/ocr2_test.go +++ b/core/services/ocr2/plugins/ccip/ccipcommit/ocr2_test.go @@ -20,15 +20,13 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/hashutil" "github.com/smartcontractkit/chainlink-common/pkg/merklemulti" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" - + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/mocks" mocks2 "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" @@ -45,7 +43,6 @@ import ( ccipdatamocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0" - ccipdbmocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdb/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/prices" ) @@ -410,7 +407,9 @@ func TestCommitReportingPlugin_Report(t *testing.T) { evmEstimator := mocks.NewEvmFeeEstimator(t) evmEstimator.On("L1Oracle").Return(nil) - gasPriceEstimator := prices.NewDAGasPriceEstimator(evmEstimator, nil, 2e9, 2e9) // 200% deviation + + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + gasPriceEstimator := prices.NewDAGasPriceEstimator(evmEstimator, nil, 2e9, 2e9, feeEstimatorConfig) // 200% deviation var destTokens []cciptypes.Address for tk := range tc.tokenDecimals { @@ -434,7 +433,7 @@ func TestCommitReportingPlugin_Report(t *testing.T) { })).Return(destDecimals, nil).Maybe() lp := mocks2.NewLogPoller(t) - commitStoreReader, err := v1_2_0.NewCommitStore(logger.TestLogger(t), utils.RandomAddress(), nil, lp) + commitStoreReader, err := v1_2_0.NewCommitStore(logger.TestLogger(t), utils.RandomAddress(), nil, lp, feeEstimatorConfig) assert.NoError(t, err) healthCheck := ccipcachemocks.NewChainHealthcheck(t) @@ -1532,6 +1531,7 @@ func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { nil, tc.daGasPriceDeviationPPB, tc.execGasPriceDeviationPPB, + ccipdatamocks.NewFeeEstimatorConfigReader(t), ) r := &CommitReportingPlugin{ diff --git a/core/services/ocr2/plugins/ccip/ccipexec/initializers.go b/core/services/ocr2/plugins/ccip/ccipexec/initializers.go index c8d079b263f..aa42ff28286 100644 --- a/core/services/ocr2/plugins/ccip/ccipexec/initializers.go +++ b/core/services/ocr2/plugins/ccip/ccipexec/initializers.go @@ -18,8 +18,6 @@ import ( cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/cache" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipcalc" "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/statuschecker" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" @@ -27,6 +25,8 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/cache" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipcalc" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/factory" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/observability" diff --git a/core/services/ocr2/plugins/ccip/ccipexec/ocr2_test.go b/core/services/ocr2/plugins/ccip/ccipexec/ocr2_test.go index 84cb73c6643..627afda975d 100644 --- a/core/services/ocr2/plugins/ccip/ccipexec/ocr2_test.go +++ b/core/services/ocr2/plugins/ccip/ccipexec/ocr2_test.go @@ -16,17 +16,18 @@ import ( mapset "github.com/deckarep/golang-set/v2" "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" - "github.com/smartcontractkit/libocr/commontypes" - "github.com/smartcontractkit/libocr/offchainreporting2/types" - ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" + "github.com/smartcontractkit/libocr/commontypes" + "github.com/smartcontractkit/libocr/offchainreporting2/types" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" lpMocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/cache" @@ -41,8 +42,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/prices" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata" - - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" ) func TestExecutionReportingPlugin_Observation(t *testing.T) { @@ -428,8 +427,10 @@ func TestExecutionReportingPlugin_buildReport(t *testing.T) { p.metricsCollector = ccip.NoopMetricsCollector p.commitStoreReader = commitStore + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + lp := lpMocks.NewLogPoller(t) - offRampReader, err := v1_0_0.NewOffRamp(logger.TestLogger(t), utils.RandomAddress(), nil, lp, nil, nil) + offRampReader, err := v1_0_0.NewOffRamp(logger.TestLogger(t), utils.RandomAddress(), nil, lp, nil, nil, feeEstimatorConfig) assert.NoError(t, err) p.offRampReader = offRampReader @@ -1376,7 +1377,9 @@ func Test_prepareTokenExecData(t *testing.T) { } func encodeExecutionReport(t *testing.T, report cciptypes.ExecReport) []byte { - reader, err := v1_2_0.NewOffRamp(logger.TestLogger(t), utils.RandomAddress(), nil, nil, nil, nil) + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + + reader, err := v1_2_0.NewOffRamp(logger.TestLogger(t), utils.RandomAddress(), nil, nil, nil, nil, feeEstimatorConfig) require.NoError(t, err) ctx := testutils.Context(t) encodedReport, err := reader.EncodeExecutionReport(ctx, report) diff --git a/core/services/ocr2/plugins/ccip/config/type_and_version.go b/core/services/ocr2/plugins/ccip/config/type_and_version.go index fdfd892b087..00e3b26d4e7 100644 --- a/core/services/ocr2/plugins/ccip/config/type_and_version.go +++ b/core/services/ocr2/plugins/ccip/config/type_and_version.go @@ -19,6 +19,7 @@ var ( EVM2EVMOffRamp ContractType = "EVM2EVMOffRamp" CommitStore ContractType = "CommitStore" PriceRegistry ContractType = "PriceRegistry" + Unknown ContractType = "Unknown" // 1.0.0 Contracts which have no TypeAndVersion ContractTypes = mapset.NewSet[ContractType]( EVM2EVMOffRamp, EVM2EVMOnRamp, @@ -63,7 +64,13 @@ func TypeAndVersion(addr common.Address, client bind.ContractBackend) (ContractT return ContractType(contractType), *v, nil } +// default version to use when TypeAndVersion is missing. +const defaultVersion = "1.0.0" + func ParseTypeAndVersion(tvStr string) (string, string, error) { + if tvStr == "" { + tvStr = string(Unknown) + " " + defaultVersion + } typeAndVersionValues := strings.Split(tvStr, " ") if len(typeAndVersionValues) < 2 { diff --git a/core/services/ocr2/plugins/ccip/config/type_and_version_test.go b/core/services/ocr2/plugins/ccip/config/type_and_version_test.go new file mode 100644 index 00000000000..f626389dcdc --- /dev/null +++ b/core/services/ocr2/plugins/ccip/config/type_and_version_test.go @@ -0,0 +1,49 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseTypeAndVersion(t *testing.T) { + tests := []struct { + name string + input string + expectedType string + expectedVersion string + expectedError string + }{ + { + name: "Valid input", + input: string(EVM2EVMOnRamp) + " 1.2.0", + expectedType: string(EVM2EVMOnRamp), + expectedVersion: "1.2.0", + }, + { + name: "Empty input", + input: "", + expectedType: string(Unknown), + expectedVersion: defaultVersion, + }, + { + name: "Invalid input", + input: "InvalidInput", + expectedError: "invalid type and version InvalidInput", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actualType, actualVersion, err := ParseTypeAndVersion(tc.input) + + if tc.expectedError != "" { + assert.EqualError(t, err, tc.expectedError) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expectedType, actualType) + assert.Equal(t, tc.expectedVersion, actualVersion) + } + }) + } +} diff --git a/core/services/ocr2/plugins/ccip/estimatorconfig/config.go b/core/services/ocr2/plugins/ccip/estimatorconfig/config.go new file mode 100644 index 00000000000..4737bd33e89 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/estimatorconfig/config.go @@ -0,0 +1,83 @@ +package estimatorconfig + +import ( + "context" + "math/big" + + "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" +) + +// FeeEstimatorConfigProvider implements abstract storage for the DataAvailability settings in onRamp dynamic Config. +// It's implemented to transfer DA config from different entities offRamp, onRamp, commitStore without injecting the +// strong dependency between modules. ConfigProvider fetch ccip.OnRampReader object reads and returns only relevant +// fields for the daGasEstimator from the encapsulated onRampReader. +type FeeEstimatorConfigProvider interface { + SetOnRampReader(reader ccip.OnRampReader) + AddGasPriceInterceptor(GasPriceInterceptor) + ModifyGasPriceComponents(ctx context.Context, execGasPrice, daGasPrice *big.Int) (modExecGasPrice, modDAGasPrice *big.Int, err error) + GetDataAvailabilityConfig(ctx context.Context) (destDataAvailabilityOverheadGas, destGasPerDataAvailabilityByte, destDataAvailabilityMultiplierBps int64, err error) +} + +type GasPriceInterceptor interface { + ModifyGasPriceComponents(ctx context.Context, execGasPrice, daGasPrice *big.Int) (modExecGasPrice, modDAGasPrice *big.Int, err error) +} + +type FeeEstimatorConfigService struct { + onRampReader ccip.OnRampReader + gasPriceInterceptors []GasPriceInterceptor +} + +func NewFeeEstimatorConfigService() *FeeEstimatorConfigService { + return &FeeEstimatorConfigService{} +} + +// SetOnRampReader Sets the onRamp reader instance. +// must be called once for each instance. +func (c *FeeEstimatorConfigService) SetOnRampReader(reader ccip.OnRampReader) { + c.onRampReader = reader +} + +// GetDataAvailabilityConfig Returns dynamic config data availability parameters. +// GetDynamicConfig should be cached in the onRamp reader to avoid unnecessary on-chain calls +func (c *FeeEstimatorConfigService) GetDataAvailabilityConfig(ctx context.Context) (destDataAvailabilityOverheadGas, destGasPerDataAvailabilityByte, destDataAvailabilityMultiplierBps int64, err error) { + if c.onRampReader == nil { + return 0, 0, 0, nil + } + + cfg, err := c.onRampReader.GetDynamicConfig(ctx) + if err != nil { + return 0, 0, 0, err + } + + return int64(cfg.DestDataAvailabilityOverheadGas), + int64(cfg.DestGasPerDataAvailabilityByte), + int64(cfg.DestDataAvailabilityMultiplierBps), + err +} + +// AddGasPriceInterceptor adds price interceptors that can modify gas price. +func (c *FeeEstimatorConfigService) AddGasPriceInterceptor(gpi GasPriceInterceptor) { + if gpi != nil { + c.gasPriceInterceptors = append(c.gasPriceInterceptors, gpi) + } +} + +// ModifyGasPriceComponents applies gasPrice interceptors and returns modified gasPrice. +func (c *FeeEstimatorConfigService) ModifyGasPriceComponents(ctx context.Context, gasPrice, daGasPrice *big.Int) (*big.Int, *big.Int, error) { + if len(c.gasPriceInterceptors) == 0 { + return gasPrice, daGasPrice, nil + } + + // values are mutable, it is necessary to copy the values to protect the arguments from modification. + cpGasPrice := new(big.Int).Set(gasPrice) + cpDAGasPrice := new(big.Int).Set(daGasPrice) + + var err error + for _, interceptor := range c.gasPriceInterceptors { + if cpGasPrice, cpDAGasPrice, err = interceptor.ModifyGasPriceComponents(ctx, cpGasPrice, cpDAGasPrice); err != nil { + return nil, nil, err + } + } + + return cpGasPrice, cpDAGasPrice, nil +} diff --git a/core/services/ocr2/plugins/ccip/estimatorconfig/config_test.go b/core/services/ocr2/plugins/ccip/estimatorconfig/config_test.go new file mode 100644 index 00000000000..3ecd88ae3bd --- /dev/null +++ b/core/services/ocr2/plugins/ccip/estimatorconfig/config_test.go @@ -0,0 +1,112 @@ +package estimatorconfig_test + +import ( + "context" + "errors" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + mocks2 "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig/mocks" + + "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" +) + +func TestFeeEstimatorConfigService(t *testing.T) { + svc := estimatorconfig.NewFeeEstimatorConfigService() + ctx := context.Background() + + var expectedDestDataAvailabilityOverheadGas int64 = 1 + var expectedDestGasPerDataAvailabilityByte int64 = 2 + var expectedDestDataAvailabilityMultiplierBps int64 = 3 + + onRampReader := mocks.NewOnRampReader(t) + destDataAvailabilityOverheadGas, destGasPerDataAvailabilityByte, destDataAvailabilityMultiplierBps, err := svc.GetDataAvailabilityConfig(ctx) + require.NoError(t, err) // if onRampReader not set, return nil error and 0 values + require.EqualValues(t, 0, destDataAvailabilityOverheadGas) + require.EqualValues(t, 0, destGasPerDataAvailabilityByte) + require.EqualValues(t, 0, destDataAvailabilityMultiplierBps) + svc.SetOnRampReader(onRampReader) + + onRampReader.On("GetDynamicConfig", ctx). + Return(ccip.OnRampDynamicConfig{ + DestDataAvailabilityOverheadGas: uint32(expectedDestDataAvailabilityOverheadGas), + DestGasPerDataAvailabilityByte: uint16(expectedDestGasPerDataAvailabilityByte), + DestDataAvailabilityMultiplierBps: uint16(expectedDestDataAvailabilityMultiplierBps), + }, nil).Once() + + destDataAvailabilityOverheadGas, destGasPerDataAvailabilityByte, destDataAvailabilityMultiplierBps, err = svc.GetDataAvailabilityConfig(ctx) + require.NoError(t, err) + require.Equal(t, expectedDestDataAvailabilityOverheadGas, destDataAvailabilityOverheadGas) + require.Equal(t, expectedDestGasPerDataAvailabilityByte, destGasPerDataAvailabilityByte) + require.Equal(t, expectedDestDataAvailabilityMultiplierBps, destDataAvailabilityMultiplierBps) + + onRampReader.On("GetDynamicConfig", ctx). + Return(ccip.OnRampDynamicConfig{}, errors.New("test")).Once() + _, _, _, err = svc.GetDataAvailabilityConfig(ctx) + require.Error(t, err) +} + +func TestModifyGasPriceComponents(t *testing.T) { + t.Run("success modification", func(t *testing.T) { + svc := estimatorconfig.NewFeeEstimatorConfigService() + ctx := context.Background() + + initialExecGasPrice, initialDaGasPrice := big.NewInt(10), big.NewInt(1) + + gpi1 := mocks2.NewGasPriceInterceptor(t) + svc.AddGasPriceInterceptor(gpi1) + + // change in first interceptor + firstModExecGasPrice, firstModDaGasPrice := big.NewInt(5), big.NewInt(2) + gpi1.On("ModifyGasPriceComponents", ctx, initialExecGasPrice, initialDaGasPrice). + Return(firstModExecGasPrice, firstModDaGasPrice, nil) + + gpi2 := mocks2.NewGasPriceInterceptor(t) + svc.AddGasPriceInterceptor(gpi2) + + // change in second iterceptor + secondModExecGasPrice, secondModDaGasPrice := big.NewInt(50), big.NewInt(20) + gpi2.On("ModifyGasPriceComponents", ctx, firstModExecGasPrice, firstModDaGasPrice). + Return(secondModExecGasPrice, secondModDaGasPrice, nil) + + // has to return second interceptor values + resGasPrice, resDAGasPrice, err := svc.ModifyGasPriceComponents(ctx, initialExecGasPrice, initialDaGasPrice) + require.NoError(t, err) + require.Equal(t, secondModExecGasPrice.Int64(), resGasPrice.Int64()) + require.Equal(t, secondModDaGasPrice.Int64(), resDAGasPrice.Int64()) + }) + + t.Run("error modification", func(t *testing.T) { + svc := estimatorconfig.NewFeeEstimatorConfigService() + ctx := context.Background() + + initialExecGasPrice, initialDaGasPrice := big.NewInt(10), big.NewInt(1) + gpi1 := mocks2.NewGasPriceInterceptor(t) + svc.AddGasPriceInterceptor(gpi1) + gpi1.On("ModifyGasPriceComponents", ctx, initialExecGasPrice, initialDaGasPrice). + Return(nil, nil, errors.New("test")) + + // has to return second interceptor values + _, _, err := svc.ModifyGasPriceComponents(ctx, initialExecGasPrice, initialDaGasPrice) + require.Error(t, err) + }) + + t.Run("without interceptors", func(t *testing.T) { + svc := estimatorconfig.NewFeeEstimatorConfigService() + ctx := context.Background() + + initialExecGasPrice, initialDaGasPrice := big.NewInt(10), big.NewInt(1) + + // has to return second interceptor values + resGasPrice, resDAGasPrice, err := svc.ModifyGasPriceComponents(ctx, initialExecGasPrice, initialDaGasPrice) + require.NoError(t, err) + + // values should not be modified + require.Equal(t, initialExecGasPrice, resGasPrice) + require.Equal(t, initialDaGasPrice, resDAGasPrice) + }) +} diff --git a/core/services/ocr2/plugins/ccip/estimatorconfig/interceptors/mantle/interceptor.go b/core/services/ocr2/plugins/ccip/estimatorconfig/interceptors/mantle/interceptor.go new file mode 100644 index 00000000000..8d5d3bb6c2f --- /dev/null +++ b/core/services/ocr2/plugins/ccip/estimatorconfig/interceptors/mantle/interceptor.go @@ -0,0 +1,80 @@ +package mantle + +import ( + "context" + "fmt" + "math/big" + "strings" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + evmClient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" +) + +const ( + // tokenRatio is not volatile and can be requested not often. + tokenRatioUpdateInterval = 60 * time.Minute + // tokenRatio fetches the tokenRatio used for Mantle's gas price calculation + tokenRatioMethod = "tokenRatio" + mantleTokenRatioAbiString = `[{"inputs":[],"name":"tokenRatio","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]` +) + +type Interceptor struct { + client evmClient.Client + tokenRatioCallData []byte + tokenRatio *big.Int + tokenRatioLastUpdate time.Time +} + +func NewInterceptor(_ context.Context, client evmClient.Client) (*Interceptor, error) { + // Encode calldata for tokenRatio method + tokenRatioMethodAbi, err := abi.JSON(strings.NewReader(mantleTokenRatioAbiString)) + if err != nil { + return nil, fmt.Errorf("failed to parse GasPriceOracle %s() method ABI for Mantle; %v", tokenRatioMethod, err) + } + tokenRatioCallData, err := tokenRatioMethodAbi.Pack(tokenRatioMethod) + if err != nil { + return nil, fmt.Errorf("failed to parse GasPriceOracle %s() calldata for Mantle; %v", tokenRatioMethod, err) + } + + return &Interceptor{ + client: client, + tokenRatioCallData: tokenRatioCallData, + }, nil +} + +// ModifyGasPriceComponents returns modified gasPrice. +func (i *Interceptor) ModifyGasPriceComponents(ctx context.Context, execGasPrice, daGasPrice *big.Int) (*big.Int, *big.Int, error) { + if time.Since(i.tokenRatioLastUpdate) > tokenRatioUpdateInterval { + mantleTokenRatio, err := i.getMantleTokenRatio(ctx) + if err != nil { + return nil, nil, err + } + + i.tokenRatio, i.tokenRatioLastUpdate = mantleTokenRatio, time.Now() + } + + // multiply daGasPrice and execGas price by tokenRatio + newExecGasPrice := new(big.Int).Mul(execGasPrice, i.tokenRatio) + newDAGasPrice := new(big.Int).Mul(daGasPrice, i.tokenRatio) + return newExecGasPrice, newDAGasPrice, nil +} + +// getMantleTokenRatio Requests and returns a token ratio value for the Mantle chain. +func (i *Interceptor) getMantleTokenRatio(ctx context.Context) (*big.Int, error) { + precompile := common.HexToAddress(rollups.OPGasOracleAddress) + tokenRatio, err := i.client.CallContract(ctx, ethereum.CallMsg{ + To: &precompile, + Data: i.tokenRatioCallData, + }, nil) + + if err != nil { + return nil, fmt.Errorf("getMantleTokenRatio call failed: %w", err) + } + + return new(big.Int).SetBytes(tokenRatio), nil +} diff --git a/core/services/ocr2/plugins/ccip/estimatorconfig/interceptors/mantle/interceptor_test.go b/core/services/ocr2/plugins/ccip/estimatorconfig/interceptors/mantle/interceptor_test.go new file mode 100644 index 00000000000..9134d996c27 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/estimatorconfig/interceptors/mantle/interceptor_test.go @@ -0,0 +1,96 @@ +package mantle + +import ( + "context" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" +) + +func TestInterceptor(t *testing.T) { + ethClient := mocks.NewClient(t) + ctx := context.Background() + + tokenRatio := big.NewInt(10) + interceptor, err := NewInterceptor(ctx, ethClient) + require.NoError(t, err) + + // request token ratio + ethClient.On("CallContract", ctx, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})). + Return(common.BigToHash(tokenRatio).Bytes(), nil).Once() + + modExecGasPrice, modDAGasPrice, err := interceptor.ModifyGasPriceComponents(ctx, big.NewInt(1), big.NewInt(1)) + require.NoError(t, err) + require.Equal(t, int64(10), modExecGasPrice.Int64()) + require.Equal(t, int64(10), modDAGasPrice.Int64()) + + // second call won't invoke eth client + modExecGasPrice, modDAGasPrice, err = interceptor.ModifyGasPriceComponents(ctx, big.NewInt(2), big.NewInt(1)) + require.NoError(t, err) + require.Equal(t, int64(20), modExecGasPrice.Int64()) + require.Equal(t, int64(10), modDAGasPrice.Int64()) +} + +func TestModifyGasPriceComponents(t *testing.T) { + testCases := map[string]struct { + execGasPrice *big.Int + daGasPrice *big.Int + tokenRatio *big.Int + resultExecGasPrice *big.Int + resultDAGasPrice *big.Int + }{ + "regular": { + execGasPrice: big.NewInt(1000), + daGasPrice: big.NewInt(100), + resultExecGasPrice: big.NewInt(2000), + resultDAGasPrice: big.NewInt(200), + tokenRatio: big.NewInt(2), + }, + "zero DAGasPrice": { + execGasPrice: big.NewInt(1000), + daGasPrice: big.NewInt(0), + resultExecGasPrice: big.NewInt(5000), + resultDAGasPrice: big.NewInt(0), + tokenRatio: big.NewInt(5), + }, + "zero ExecGasPrice": { + execGasPrice: big.NewInt(0), + daGasPrice: big.NewInt(10), + resultExecGasPrice: big.NewInt(0), + resultDAGasPrice: big.NewInt(50), + tokenRatio: big.NewInt(5), + }, + "zero token ratio": { + execGasPrice: big.NewInt(15), + daGasPrice: big.NewInt(10), + resultExecGasPrice: big.NewInt(0), + resultDAGasPrice: big.NewInt(0), + tokenRatio: big.NewInt(0), + }, + } + + for tcName, tc := range testCases { + t.Run(tcName, func(t *testing.T) { + ethClient := mocks.NewClient(t) + ctx := context.Background() + + interceptor, err := NewInterceptor(ctx, ethClient) + require.NoError(t, err) + + // request token ratio + ethClient.On("CallContract", ctx, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})). + Return(common.BigToHash(tc.tokenRatio).Bytes(), nil).Once() + + modExecGasPrice, modDAGasPrice, err := interceptor.ModifyGasPriceComponents(ctx, tc.execGasPrice, tc.daGasPrice) + require.NoError(t, err) + require.Equal(t, tc.resultExecGasPrice.Int64(), modExecGasPrice.Int64()) + require.Equal(t, tc.resultDAGasPrice.Int64(), modDAGasPrice.Int64()) + }) + } +} diff --git a/core/services/ocr2/plugins/ccip/estimatorconfig/mocks/gas_price_interceptor_mock.go b/core/services/ocr2/plugins/ccip/estimatorconfig/mocks/gas_price_interceptor_mock.go new file mode 100644 index 00000000000..62ec5a2207b --- /dev/null +++ b/core/services/ocr2/plugins/ccip/estimatorconfig/mocks/gas_price_interceptor_mock.go @@ -0,0 +1,106 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + big "math/big" + + mock "github.com/stretchr/testify/mock" +) + +// GasPriceInterceptor is an autogenerated mock type for the GasPriceInterceptor type +type GasPriceInterceptor struct { + mock.Mock +} + +type GasPriceInterceptor_Expecter struct { + mock *mock.Mock +} + +func (_m *GasPriceInterceptor) EXPECT() *GasPriceInterceptor_Expecter { + return &GasPriceInterceptor_Expecter{mock: &_m.Mock} +} + +// ModifyGasPriceComponents provides a mock function with given fields: ctx, execGasPrice, daGasPrice +func (_m *GasPriceInterceptor) ModifyGasPriceComponents(ctx context.Context, execGasPrice *big.Int, daGasPrice *big.Int) (*big.Int, *big.Int, error) { + ret := _m.Called(ctx, execGasPrice, daGasPrice) + + if len(ret) == 0 { + panic("no return value specified for ModifyGasPriceComponents") + } + + var r0 *big.Int + var r1 *big.Int + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, *big.Int, *big.Int) (*big.Int, *big.Int, error)); ok { + return rf(ctx, execGasPrice, daGasPrice) + } + if rf, ok := ret.Get(0).(func(context.Context, *big.Int, *big.Int) *big.Int); ok { + r0 = rf(ctx, execGasPrice, daGasPrice) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *big.Int, *big.Int) *big.Int); ok { + r1 = rf(ctx, execGasPrice, daGasPrice) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*big.Int) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, *big.Int, *big.Int) error); ok { + r2 = rf(ctx, execGasPrice, daGasPrice) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// GasPriceInterceptor_ModifyGasPriceComponents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ModifyGasPriceComponents' +type GasPriceInterceptor_ModifyGasPriceComponents_Call struct { + *mock.Call +} + +// ModifyGasPriceComponents is a helper method to define mock.On call +// - ctx context.Context +// - execGasPrice *big.Int +// - daGasPrice *big.Int +func (_e *GasPriceInterceptor_Expecter) ModifyGasPriceComponents(ctx interface{}, execGasPrice interface{}, daGasPrice interface{}) *GasPriceInterceptor_ModifyGasPriceComponents_Call { + return &GasPriceInterceptor_ModifyGasPriceComponents_Call{Call: _e.mock.On("ModifyGasPriceComponents", ctx, execGasPrice, daGasPrice)} +} + +func (_c *GasPriceInterceptor_ModifyGasPriceComponents_Call) Run(run func(ctx context.Context, execGasPrice *big.Int, daGasPrice *big.Int)) *GasPriceInterceptor_ModifyGasPriceComponents_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*big.Int), args[2].(*big.Int)) + }) + return _c +} + +func (_c *GasPriceInterceptor_ModifyGasPriceComponents_Call) Return(modExecGasPrice *big.Int, modDAGasPrice *big.Int, err error) *GasPriceInterceptor_ModifyGasPriceComponents_Call { + _c.Call.Return(modExecGasPrice, modDAGasPrice, err) + return _c +} + +func (_c *GasPriceInterceptor_ModifyGasPriceComponents_Call) RunAndReturn(run func(context.Context, *big.Int, *big.Int) (*big.Int, *big.Int, error)) *GasPriceInterceptor_ModifyGasPriceComponents_Call { + _c.Call.Return(run) + return _c +} + +// NewGasPriceInterceptor creates a new instance of GasPriceInterceptor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewGasPriceInterceptor(t interface { + mock.TestingT + Cleanup(func()) +}) *GasPriceInterceptor { + mock := &GasPriceInterceptor{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/services/ocr2/plugins/ccip/exportinternal.go b/core/services/ocr2/plugins/ccip/exportinternal.go index 2a5767ac85d..2f924085fb2 100644 --- a/core/services/ocr2/plugins/ccip/exportinternal.go +++ b/core/services/ocr2/plugins/ccip/exportinternal.go @@ -37,20 +37,20 @@ func NewEvmPriceRegistry(lp logpoller.LogPoller, ec client.Client, lggr logger.L type VersionFinder = factory.VersionFinder -func NewCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address ccip.Address, ec client.Client, lp logpoller.LogPoller) (ccipdata.CommitStoreReader, error) { - return factory.NewCommitStoreReader(lggr, versionFinder, address, ec, lp) +func NewCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address ccip.Address, ec client.Client, lp logpoller.LogPoller, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) (ccipdata.CommitStoreReader, error) { + return factory.NewCommitStoreReader(lggr, versionFinder, address, ec, lp, feeEstimatorConfig) } -func CloseCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address ccip.Address, ec client.Client, lp logpoller.LogPoller) error { - return factory.CloseCommitStoreReader(lggr, versionFinder, address, ec, lp) +func CloseCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address ccip.Address, ec client.Client, lp logpoller.LogPoller, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) error { + return factory.CloseCommitStoreReader(lggr, versionFinder, address, ec, lp, feeEstimatorConfig) } -func NewOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr ccip.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int, registerFilters bool) (ccipdata.OffRampReader, error) { - return factory.NewOffRampReader(lggr, versionFinder, addr, destClient, lp, estimator, destMaxGasPrice, registerFilters) +func NewOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr ccip.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int, registerFilters bool, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) (ccipdata.OffRampReader, error) { + return factory.NewOffRampReader(lggr, versionFinder, addr, destClient, lp, estimator, destMaxGasPrice, registerFilters, feeEstimatorConfig) } -func CloseOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr ccip.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int) error { - return factory.CloseOffRampReader(lggr, versionFinder, addr, destClient, lp, estimator, destMaxGasPrice) +func CloseOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr ccip.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) error { + return factory.CloseOffRampReader(lggr, versionFinder, addr, destClient, lp, estimator, destMaxGasPrice, feeEstimatorConfig) } func NewEvmVersionFinder() factory.EvmVersionFinder { diff --git a/core/services/ocr2/plugins/ccip/internal/cache/commit_roots_test.go b/core/services/ocr2/plugins/ccip/internal/cache/commit_roots_test.go index dc0a8443497..d8289212e8a 100644 --- a/core/services/ocr2/plugins/ccip/internal/cache/commit_roots_test.go +++ b/core/services/ocr2/plugins/ccip/internal/cache/commit_roots_test.go @@ -9,7 +9,6 @@ import ( "github.com/stretchr/testify/require" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" @@ -18,6 +17,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/cache" + ccipdatamocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0" ) @@ -54,7 +54,9 @@ func Test_RootsEligibleForExecution(t *testing.T) { } require.NoError(t, orm.InsertLogsWithBlock(ctx, inputLogs, logpoller.NewLogPollerBlock(utils.RandomBytes32(), 2, time.Now(), 1))) - commitStore, err := v1_2_0.NewCommitStore(logger.TestLogger(t), commitStoreAddr, nil, lp) + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + + commitStore, err := v1_2_0.NewCommitStore(logger.TestLogger(t), commitStoreAddr, nil, lp, feeEstimatorConfig) require.NoError(t, err) rootsCache := cache.NewCommitRootsCache(logger.TestLogger(t), commitStore, 10*time.Hour, time.Second) @@ -162,7 +164,9 @@ func Test_RootsEligibleForExecutionWithReorgs(t *testing.T) { } require.NoError(t, orm.InsertLogsWithBlock(ctx, inputLogs, logpoller.NewLogPollerBlock(utils.RandomBytes32(), 3, time.Now(), 1))) - commitStore, err := v1_2_0.NewCommitStore(logger.TestLogger(t), commitStoreAddr, nil, lp) + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + + commitStore, err := v1_2_0.NewCommitStore(logger.TestLogger(t), commitStoreAddr, nil, lp, feeEstimatorConfig) require.NoError(t, err) rootsCache := cache.NewCommitRootsCache(logger.TestLogger(t), commitStore, 10*time.Hour, time.Second) @@ -221,7 +225,9 @@ func Test_BlocksWithTheSameTimestamps(t *testing.T) { } require.NoError(t, orm.InsertLogsWithBlock(ctx, inputLogs, logpoller.NewLogPollerBlock(utils.RandomBytes32(), 2, time.Now(), 2))) - commitStore, err := v1_2_0.NewCommitStore(logger.TestLogger(t), commitStoreAddr, nil, lp) + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + + commitStore, err := v1_2_0.NewCommitStore(logger.TestLogger(t), commitStoreAddr, nil, lp, feeEstimatorConfig) require.NoError(t, err) rootsCache := cache.NewCommitRootsCache(logger.TestLogger(t), commitStore, 10*time.Hour, time.Second) diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/commit_store_reader_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/commit_store_reader_test.go index 7f6b1c1bb75..81807e66feb 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/commit_store_reader_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/commit_store_reader_test.go @@ -13,7 +13,6 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/pkg/config" - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" @@ -38,6 +37,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipcalc" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/factory" + ccipdatamocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0" ) @@ -193,15 +193,24 @@ func TestCommitStoreReaders(t *testing.T) { lm := new(rollupMocks.L1Oracle) ge.On("L1Oracle").Return(lm) + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + feeEstimatorConfig.On( + "ModifyGasPriceComponents", + mock.AnythingOfType("context.backgroundCtx"), + mock.AnythingOfType("*big.Int"), + mock.AnythingOfType("*big.Int"), + ).Return(func(ctx context.Context, x, y *big.Int) (*big.Int, *big.Int, error) { + return x, y, nil + }) maxGasPrice := big.NewInt(1e8) - c10r, err := factory.NewCommitStoreReader(lggr, factory.NewEvmVersionFinder(), ccipcalc.EvmAddrToGeneric(addr), ec, lp) // ge, maxGasPrice + c10r, err := factory.NewCommitStoreReader(lggr, factory.NewEvmVersionFinder(), ccipcalc.EvmAddrToGeneric(addr), ec, lp, feeEstimatorConfig) // ge, maxGasPrice require.NoError(t, err) err = c10r.SetGasEstimator(ctx, ge) require.NoError(t, err) err = c10r.SetSourceMaxGasPrice(ctx, maxGasPrice) require.NoError(t, err) assert.Equal(t, reflect.TypeOf(c10r).String(), reflect.TypeOf(&v1_0_0.CommitStore{}).String()) - c12r, err := factory.NewCommitStoreReader(lggr, factory.NewEvmVersionFinder(), ccipcalc.EvmAddrToGeneric(addr2), ec, lp) + c12r, err := factory.NewCommitStoreReader(lggr, factory.NewEvmVersionFinder(), ccipcalc.EvmAddrToGeneric(addr2), ec, lp, feeEstimatorConfig) require.NoError(t, err) err = c12r.SetGasEstimator(ctx, ge) require.NoError(t, err) @@ -284,7 +293,7 @@ func TestCommitStoreReaders(t *testing.T) { } gasPrice := big.NewInt(10) daPrice := big.NewInt(20) - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, assets.NewWei(maxGasPrice)).Return(gas.EvmFee{Legacy: assets.NewWei(gasPrice)}, uint64(0), nil) + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, assets.NewWei(maxGasPrice), (*common.Address)(nil), (*common.Address)(nil)).Return(gas.EvmFee{Legacy: assets.NewWei(gasPrice)}, uint64(0), nil) lm.On("GasPrice", mock.Anything).Return(assets.NewWei(daPrice), nil) for v, cr := range crs { @@ -370,8 +379,10 @@ func TestCommitStoreReaders(t *testing.T) { require.NoError(t, err) assert.Equal(t, commonOffchain, c2) // We should be able to query for gas prices now. + gpe, err := cr.GasPriceEstimator(ctx) require.NoError(t, err) + gp, err := gpe.GetGasPrice(context.Background()) require.NoError(t, err) assert.True(t, gp.Cmp(big.NewInt(0)) > 0) @@ -412,7 +423,10 @@ func TestNewCommitStoreReader(t *testing.T) { if tc.expectedErr == "" { lp.On("RegisterFilter", mock.Anything, mock.Anything).Return(nil) } - _, err = factory.NewCommitStoreReader(logger.TestLogger(t), factory.NewEvmVersionFinder(), addr, c, lp) + + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + + _, err = factory.NewCommitStoreReader(logger.TestLogger(t), factory.NewEvmVersionFinder(), addr, c, lp, feeEstimatorConfig) if tc.expectedErr != "" { require.EqualError(t, err, tc.expectedErr) } else { diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/commit_store.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/commit_store.go index d431d2863a0..c5f32ba91cd 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/commit_store.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/commit_store.go @@ -21,16 +21,16 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0" ) -func NewCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address cciptypes.Address, ec client.Client, lp logpoller.LogPoller) (ccipdata.CommitStoreReader, error) { - return initOrCloseCommitStoreReader(lggr, versionFinder, address, ec, lp, false) +func NewCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address cciptypes.Address, ec client.Client, lp logpoller.LogPoller, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) (ccipdata.CommitStoreReader, error) { + return initOrCloseCommitStoreReader(lggr, versionFinder, address, ec, lp, feeEstimatorConfig, false) } -func CloseCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address cciptypes.Address, ec client.Client, lp logpoller.LogPoller) error { - _, err := initOrCloseCommitStoreReader(lggr, versionFinder, address, ec, lp, true) +func CloseCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address cciptypes.Address, ec client.Client, lp logpoller.LogPoller, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) error { + _, err := initOrCloseCommitStoreReader(lggr, versionFinder, address, ec, lp, feeEstimatorConfig, true) return err } -func initOrCloseCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address cciptypes.Address, ec client.Client, lp logpoller.LogPoller, closeReader bool) (ccipdata.CommitStoreReader, error) { +func initOrCloseCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address cciptypes.Address, ec client.Client, lp logpoller.LogPoller, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader, closeReader bool) (ccipdata.CommitStoreReader, error) { contractType, version, err := versionFinder.TypeAndVersion(address, ec) if err != nil { return nil, errors.Wrapf(err, "unable to read type and version") @@ -57,7 +57,7 @@ func initOrCloseCommitStoreReader(lggr logger.Logger, versionFinder VersionFinde } return cs, cs.RegisterFilters() case ccipdata.V1_2_0: - cs, err := v1_2_0.NewCommitStore(lggr, evmAddr, ec, lp) + cs, err := v1_2_0.NewCommitStore(lggr, evmAddr, ec, lp, feeEstimatorConfig) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func initOrCloseCommitStoreReader(lggr logger.Logger, versionFinder VersionFinde } return cs, cs.RegisterFilters() case ccipdata.V1_5_0: - cs, err := v1_5_0.NewCommitStore(lggr, evmAddr, ec, lp) + cs, err := v1_5_0.NewCommitStore(lggr, evmAddr, ec, lp, feeEstimatorConfig) if err != nil { return nil, err } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/commit_store_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/commit_store_test.go index e1b8ff929c3..987b6f848e4 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/commit_store_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/commit_store_test.go @@ -8,13 +8,13 @@ import ( "github.com/stretchr/testify/mock" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" mocks2 "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" "github.com/smartcontractkit/chainlink/v2/core/logger" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" + ccipdatamocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0" ) @@ -24,14 +24,16 @@ func TestCommitStore(t *testing.T) { addr := cciptypes.Address(utils.RandomAddress().String()) lp := mocks2.NewLogPoller(t) + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + lp.On("RegisterFilter", mock.Anything, mock.Anything).Return(nil) versionFinder := newMockVersionFinder(ccipconfig.CommitStore, *semver.MustParse(versionStr), nil) - _, err := NewCommitStoreReader(lggr, versionFinder, addr, nil, lp) + _, err := NewCommitStoreReader(lggr, versionFinder, addr, nil, lp, feeEstimatorConfig) assert.NoError(t, err) expFilterName := logpoller.FilterName(v1_0_0.EXEC_REPORT_ACCEPTS, addr) lp.On("UnregisterFilter", mock.Anything, expFilterName).Return(nil) - err = CloseCommitStoreReader(lggr, versionFinder, addr, nil, lp) + err = CloseCommitStoreReader(lggr, versionFinder, addr, nil, lp, feeEstimatorConfig) assert.NoError(t, err) } } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/offramp.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/offramp.go index c6fa63ee820..5d9b751d0bd 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/offramp.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/offramp.go @@ -26,16 +26,16 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0" ) -func NewOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr cciptypes.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int, registerFilters bool) (ccipdata.OffRampReader, error) { - return initOrCloseOffRampReader(lggr, versionFinder, addr, destClient, lp, estimator, destMaxGasPrice, false, registerFilters) +func NewOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr cciptypes.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int, registerFilters bool, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) (ccipdata.OffRampReader, error) { + return initOrCloseOffRampReader(lggr, versionFinder, addr, destClient, lp, estimator, destMaxGasPrice, false, registerFilters, feeEstimatorConfig) } -func CloseOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr cciptypes.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int) error { - _, err := initOrCloseOffRampReader(lggr, versionFinder, addr, destClient, lp, estimator, destMaxGasPrice, true, false) +func CloseOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr cciptypes.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) error { + _, err := initOrCloseOffRampReader(lggr, versionFinder, addr, destClient, lp, estimator, destMaxGasPrice, true, false, feeEstimatorConfig) return err } -func initOrCloseOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr cciptypes.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int, closeReader bool, registerFilters bool) (ccipdata.OffRampReader, error) { +func initOrCloseOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr cciptypes.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int, closeReader bool, registerFilters bool, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) (ccipdata.OffRampReader, error) { contractType, version, err := versionFinder.TypeAndVersion(addr, destClient) if err != nil { return nil, errors.Wrapf(err, "unable to read type and version") @@ -53,7 +53,7 @@ func initOrCloseOffRampReader(lggr logger.Logger, versionFinder VersionFinder, a switch version.String() { case ccipdata.V1_0_0, ccipdata.V1_1_0: - offRamp, err := v1_0_0.NewOffRamp(lggr, evmAddr, destClient, lp, estimator, destMaxGasPrice) + offRamp, err := v1_0_0.NewOffRamp(lggr, evmAddr, destClient, lp, estimator, destMaxGasPrice, feeEstimatorConfig) if err != nil { return nil, err } @@ -62,7 +62,7 @@ func initOrCloseOffRampReader(lggr logger.Logger, versionFinder VersionFinder, a } return offRamp, offRamp.RegisterFilters() case ccipdata.V1_2_0: - offRamp, err := v1_2_0.NewOffRamp(lggr, evmAddr, destClient, lp, estimator, destMaxGasPrice) + offRamp, err := v1_2_0.NewOffRamp(lggr, evmAddr, destClient, lp, estimator, destMaxGasPrice, feeEstimatorConfig) if err != nil { return nil, err } @@ -71,7 +71,7 @@ func initOrCloseOffRampReader(lggr logger.Logger, versionFinder VersionFinder, a } return offRamp, offRamp.RegisterFilters() case ccipdata.V1_5_0: - offRamp, err := v1_5_0.NewOffRamp(lggr, evmAddr, destClient, lp, estimator, destMaxGasPrice) + offRamp, err := v1_5_0.NewOffRamp(lggr, evmAddr, destClient, lp, estimator, destMaxGasPrice, feeEstimatorConfig) if err != nil { return nil, err } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/offramp_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/offramp_test.go index 4b9e57ecfbd..c00d9e134b5 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/offramp_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/offramp_test.go @@ -15,6 +15,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/logger" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" + ccipdatamocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0" ) @@ -24,6 +25,8 @@ func TestOffRamp(t *testing.T) { addr := cciptypes.Address(utils.RandomAddress().String()) lp := mocks2.NewLogPoller(t) + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + expFilterNames := []string{ logpoller.FilterName(v1_0_0.EXEC_EXECUTION_STATE_CHANGES, addr), logpoller.FilterName(v1_0_0.EXEC_TOKEN_POOL_ADDED, addr), @@ -32,13 +35,13 @@ func TestOffRamp(t *testing.T) { versionFinder := newMockVersionFinder(ccipconfig.EVM2EVMOffRamp, *semver.MustParse(versionStr), nil) lp.On("RegisterFilter", mock.Anything, mock.Anything).Return(nil).Times(len(expFilterNames)) - _, err := NewOffRampReader(lggr, versionFinder, addr, nil, lp, nil, nil, true) + _, err := NewOffRampReader(lggr, versionFinder, addr, nil, lp, nil, nil, true, feeEstimatorConfig) assert.NoError(t, err) for _, f := range expFilterNames { lp.On("UnregisterFilter", mock.Anything, f).Return(nil) } - err = CloseOffRampReader(lggr, versionFinder, addr, nil, lp, nil, nil) + err = CloseOffRampReader(lggr, versionFinder, addr, nil, lp, nil, nil, feeEstimatorConfig) assert.NoError(t, err) } } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/fee_estimator_config.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/fee_estimator_config.go new file mode 100644 index 00000000000..0b4d1ce75ab --- /dev/null +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/fee_estimator_config.go @@ -0,0 +1,11 @@ +package ccipdata + +import ( + "context" + "math/big" +) + +type FeeEstimatorConfigReader interface { + GetDataAvailabilityConfig(ctx context.Context) (destDAOverheadGas, destGasPerDAByte, destDAMultiplierBps int64, err error) + ModifyGasPriceComponents(ctx context.Context, execGasPrice, daGasPrice *big.Int) (modExecGasPrice, modDAGasPrice *big.Int, err error) +} diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks/fee_estimator_config_mock.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks/fee_estimator_config_mock.go new file mode 100644 index 00000000000..e62cdec87a8 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks/fee_estimator_config_mock.go @@ -0,0 +1,177 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mocks + +import ( + big "math/big" + + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// FeeEstimatorConfigReader is an autogenerated mock type for the FeeEstimatorConfigReader type +type FeeEstimatorConfigReader struct { + mock.Mock +} + +type FeeEstimatorConfigReader_Expecter struct { + mock *mock.Mock +} + +func (_m *FeeEstimatorConfigReader) EXPECT() *FeeEstimatorConfigReader_Expecter { + return &FeeEstimatorConfigReader_Expecter{mock: &_m.Mock} +} + +// GetDataAvailabilityConfig provides a mock function with given fields: ctx +func (_m *FeeEstimatorConfigReader) GetDataAvailabilityConfig(ctx context.Context) (int64, int64, int64, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetDataAvailabilityConfig") + } + + var r0 int64 + var r1 int64 + var r2 int64 + var r3 error + if rf, ok := ret.Get(0).(func(context.Context) (int64, int64, int64, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) int64); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context) int64); ok { + r1 = rf(ctx) + } else { + r1 = ret.Get(1).(int64) + } + + if rf, ok := ret.Get(2).(func(context.Context) int64); ok { + r2 = rf(ctx) + } else { + r2 = ret.Get(2).(int64) + } + + if rf, ok := ret.Get(3).(func(context.Context) error); ok { + r3 = rf(ctx) + } else { + r3 = ret.Error(3) + } + + return r0, r1, r2, r3 +} + +// FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDataAvailabilityConfig' +type FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call struct { + *mock.Call +} + +// GetDataAvailabilityConfig is a helper method to define mock.On call +// - ctx context.Context +func (_e *FeeEstimatorConfigReader_Expecter) GetDataAvailabilityConfig(ctx interface{}) *FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call { + return &FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call{Call: _e.mock.On("GetDataAvailabilityConfig", ctx)} +} + +func (_c *FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call) Run(run func(ctx context.Context)) *FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call) Return(destDAOverheadGas int64, destGasPerDAByte int64, destDAMultiplierBps int64, err error) *FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call { + _c.Call.Return(destDAOverheadGas, destGasPerDAByte, destDAMultiplierBps, err) + return _c +} + +func (_c *FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call) RunAndReturn(run func(context.Context) (int64, int64, int64, error)) *FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call { + _c.Call.Return(run) + return _c +} + +// ModifyGasPriceComponents provides a mock function with given fields: ctx, execGasPrice, daGasPrice +func (_m *FeeEstimatorConfigReader) ModifyGasPriceComponents(ctx context.Context, execGasPrice *big.Int, daGasPrice *big.Int) (*big.Int, *big.Int, error) { + ret := _m.Called(ctx, execGasPrice, daGasPrice) + + if len(ret) == 0 { + panic("no return value specified for ModifyGasPriceComponents") + } + + var r0 *big.Int + var r1 *big.Int + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, *big.Int, *big.Int) (*big.Int, *big.Int, error)); ok { + return rf(ctx, execGasPrice, daGasPrice) + } + if rf, ok := ret.Get(0).(func(context.Context, *big.Int, *big.Int) *big.Int); ok { + r0 = rf(ctx, execGasPrice, daGasPrice) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *big.Int, *big.Int) *big.Int); ok { + r1 = rf(ctx, execGasPrice, daGasPrice) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*big.Int) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, *big.Int, *big.Int) error); ok { + r2 = rf(ctx, execGasPrice, daGasPrice) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// FeeEstimatorConfigReader_ModifyGasPriceComponents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ModifyGasPriceComponents' +type FeeEstimatorConfigReader_ModifyGasPriceComponents_Call struct { + *mock.Call +} + +// ModifyGasPriceComponents is a helper method to define mock.On call +// - ctx context.Context +// - execGasPrice *big.Int +// - daGasPrice *big.Int +func (_e *FeeEstimatorConfigReader_Expecter) ModifyGasPriceComponents(ctx interface{}, execGasPrice interface{}, daGasPrice interface{}) *FeeEstimatorConfigReader_ModifyGasPriceComponents_Call { + return &FeeEstimatorConfigReader_ModifyGasPriceComponents_Call{Call: _e.mock.On("ModifyGasPriceComponents", ctx, execGasPrice, daGasPrice)} +} + +func (_c *FeeEstimatorConfigReader_ModifyGasPriceComponents_Call) Run(run func(ctx context.Context, execGasPrice *big.Int, daGasPrice *big.Int)) *FeeEstimatorConfigReader_ModifyGasPriceComponents_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*big.Int), args[2].(*big.Int)) + }) + return _c +} + +func (_c *FeeEstimatorConfigReader_ModifyGasPriceComponents_Call) Return(modExecGasPrice *big.Int, modDAGasPrice *big.Int, err error) *FeeEstimatorConfigReader_ModifyGasPriceComponents_Call { + _c.Call.Return(modExecGasPrice, modDAGasPrice, err) + return _c +} + +func (_c *FeeEstimatorConfigReader_ModifyGasPriceComponents_Call) RunAndReturn(run func(context.Context, *big.Int, *big.Int) (*big.Int, *big.Int, error)) *FeeEstimatorConfigReader_ModifyGasPriceComponents_Call { + _c.Call.Return(run) + return _c +} + +// NewFeeEstimatorConfigReader creates a new instance of FeeEstimatorConfigReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewFeeEstimatorConfigReader(t interface { + mock.TestingT + Cleanup(func()) +}) *FeeEstimatorConfigReader { + mock := &FeeEstimatorConfigReader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/offramp_reader_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/offramp_reader_test.go index f5711422bdf..df405a5a615 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/offramp_reader_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/offramp_reader_test.go @@ -13,7 +13,6 @@ import ( "github.com/stretchr/testify/require" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" evmclientmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker" @@ -32,6 +31,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipcalc" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/factory" + ccipdatamocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0" ) @@ -194,8 +194,10 @@ func setupOffRampReaderTH(t *testing.T, version string) offRampReaderTH { require.Fail(t, "Unknown version: ", version) } + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + // Create the version-specific reader. - reader, err := factory.NewOffRampReader(log, factory.NewEvmVersionFinder(), ccipcalc.EvmAddrToGeneric(offRampAddress), bc, lp, nil, nil, true) + reader, err := factory.NewOffRampReader(log, factory.NewEvmVersionFinder(), ccipcalc.EvmAddrToGeneric(offRampAddress), bc, lp, nil, nil, true, feeEstimatorConfig) require.NoError(t, err) addr, err := reader.Address(ctx) require.NoError(t, err) @@ -401,11 +403,14 @@ func TestNewOffRampReader(t *testing.T) { b, err := utils.ABIEncode(`[{"type":"string"}]`, tc.typeAndVersion) require.NoError(t, err) c := evmclientmocks.NewClient(t) + + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + c.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Return(b, nil) addr := ccipcalc.EvmAddrToGeneric(utils.RandomAddress()) lp := lpmocks.NewLogPoller(t) lp.On("RegisterFilter", mock.Anything, mock.Anything).Return(nil).Maybe() - _, err = factory.NewOffRampReader(logger.TestLogger(t), factory.NewEvmVersionFinder(), addr, c, lp, nil, nil, true) + _, err = factory.NewOffRampReader(logger.TestLogger(t), factory.NewEvmVersionFinder(), addr, c, lp, nil, nil, true, feeEstimatorConfig) if tc.expectedErr != "" { assert.EqualError(t, err, tc.expectedErr) } else { diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp.go index 137cbaf451d..b5625c59d09 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp.go @@ -156,6 +156,7 @@ type OffRamp struct { eventSig common.Hash cachedOffRampTokens cache.AutoSync[cciptypes.OffRampTokens] sourceToDestTokensCache sync.Map + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader // Dynamic config // configMu guards all the dynamic config fields. @@ -627,7 +628,7 @@ func (o *OffRamp) RegisterFilters() error { return logpollerutil.RegisterLpFilters(o.lp, o.filters) } -func NewOffRamp(lggr logger.Logger, addr common.Address, ec client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int) (*OffRamp, error) { +func NewOffRamp(lggr logger.Logger, addr common.Address, ec client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) (*OffRamp, error) { offRamp, err := evm_2_evm_offramp_1_0_0.NewEVM2EVMOffRamp(addr, ec) if err != nil { return nil, err @@ -682,8 +683,9 @@ func NewOffRamp(lggr logger.Logger, addr common.Address, ec client.Client, lp lo offRamp.Address(), ), // values set on the fly after ChangeConfig is called - gasPriceEstimator: prices.ExecGasPriceEstimator{}, - offchainConfig: cciptypes.ExecOffchainConfig{}, - onchainConfig: cciptypes.ExecOnchainConfig{}, + gasPriceEstimator: prices.ExecGasPriceEstimator{}, + offchainConfig: cciptypes.ExecOffchainConfig{}, + onchainConfig: cciptypes.ExecOnchainConfig{}, + feeEstimatorConfig: feeEstimatorConfig, }, nil } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp_reader_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp_reader_test.go index d834b792ce4..455c1dbcb82 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp_reader_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp_reader_test.go @@ -11,6 +11,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0" ) @@ -25,7 +26,9 @@ func TestExecutionReportEncodingV100(t *testing.T) { ProofFlagBits: big.NewInt(133), } - offRamp, err := v1_0_0.NewOffRamp(logger.TestLogger(t), utils.RandomAddress(), nil, lpmocks.NewLogPoller(t), nil, nil) + feeEstimatorConfig := mocks.NewFeeEstimatorConfigReader(t) + + offRamp, err := v1_0_0.NewOffRamp(logger.TestLogger(t), utils.RandomAddress(), nil, lpmocks.NewLogPoller(t), nil, nil, feeEstimatorConfig) require.NoError(t, err) ctx := testutils.Context(t) diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp_reader_unit_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp_reader_unit_test.go index f8b1dc4e615..6cde3753b71 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp_reader_unit_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp_reader_unit_test.go @@ -6,16 +6,12 @@ import ( "slices" "testing" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/rpclib" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/rpclib/rpclibmocks" - "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" - evmclimocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" @@ -27,6 +23,9 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/cache" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipcalc" + ccipdatamocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/rpclib" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/rpclib/rpclibmocks" ) func TestOffRampGetDestinationTokensFromSourceTokens(t *testing.T) { @@ -190,13 +189,15 @@ func Test_LogsAreProperlyMarkedAsFinalized(t *testing.T) { t.Run(tt.name, func(t *testing.T) { offrampAddress := utils.RandomAddress() + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + lp := mocks.NewLogPoller(t) lp.On("LatestBlock", mock.Anything). Return(logpoller.LogPollerBlock{FinalizedBlockNumber: int64(tt.lastFinalizedBlock)}, nil) lp.On("IndexedLogsTopicRange", mock.Anything, ExecutionStateChangedEvent, offrampAddress, 1, logpoller.EvmWord(minSeqNr), logpoller.EvmWord(maxSeqNr), evmtypes.Confirmations(0)). Return(inputLogs, nil) - offRamp, err := NewOffRamp(logger.TestLogger(t), offrampAddress, evmclimocks.NewClient(t), lp, nil, nil) + offRamp, err := NewOffRamp(logger.TestLogger(t), offrampAddress, evmclimocks.NewClient(t), lp, nil, nil, feeEstimatorConfig) require.NoError(t, err) logs, err := offRamp.GetExecutionStateChangesBetweenSeqNums(testutils.Context(t), minSeqNr, maxSeqNr, 0) require.NoError(t, err) diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/onramp.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/onramp.go index 6737abe64c4..d6f3094af78 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/onramp.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/onramp.go @@ -30,16 +30,16 @@ const ( var _ ccipdata.OnRampReader = &OnRamp{} type OnRamp struct { - address common.Address - onRamp *evm_2_evm_onramp_1_0_0.EVM2EVMOnRamp - lp logpoller.LogPoller - lggr logger.Logger - client client.Client - leafHasher ccipdata.LeafHasherInterface[[32]byte] - sendRequestedEventSig common.Hash - sendRequestedSeqNumberWord int - filters []logpoller.Filter - cachedSourcePriceRegistryAddress cache.AutoSync[cciptypes.Address] + address common.Address + onRamp *evm_2_evm_onramp_1_0_0.EVM2EVMOnRamp + lp logpoller.LogPoller + lggr logger.Logger + client client.Client + leafHasher ccipdata.LeafHasherInterface[[32]byte] + sendRequestedEventSig common.Hash + sendRequestedSeqNumberWord int + filters []logpoller.Filter + cachedOnRampDynamicConfig cache.AutoSync[cciptypes.OnRampDynamicConfig] // Static config can be cached, because it's never expected to change. // The only way to change that is through the contract's constructor (redeployment) cachedStaticConfig cache.OnceCtxFunction[evm_2_evm_onramp_1_0_0.EVM2EVMOnRampStaticConfig] @@ -90,7 +90,7 @@ func NewOnRamp(lggr logger.Logger, sourceSelector, destSelector uint64, onRampAd // offset || sourceChainID || seqNum || ... sendRequestedSeqNumberWord: 2, sendRequestedEventSig: eventSig, - cachedSourcePriceRegistryAddress: cache.NewLogpollerEventsBased[cciptypes.Address]( + cachedOnRampDynamicConfig: cache.NewLogpollerEventsBased[cciptypes.OnRampDynamicConfig]( sourceLP, []common.Hash{configSetEventSig}, onRampAddress, @@ -104,38 +104,38 @@ func (o *OnRamp) Address(context.Context) (cciptypes.Address, error) { return cciptypes.Address(o.onRamp.Address().String()), nil } -func (o *OnRamp) GetDynamicConfig(context.Context) (cciptypes.OnRampDynamicConfig, error) { - if o.onRamp == nil { - return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("onramp not initialized") - } - legacyDynamicConfig, err := o.onRamp.GetDynamicConfig(nil) - if err != nil { - return cciptypes.OnRampDynamicConfig{}, err - } - return cciptypes.OnRampDynamicConfig{ - Router: cciptypes.Address(legacyDynamicConfig.Router.String()), - MaxNumberOfTokensPerMsg: legacyDynamicConfig.MaxTokensLength, - DestGasOverhead: 0, - DestGasPerPayloadByte: 0, - DestDataAvailabilityOverheadGas: 0, - DestGasPerDataAvailabilityByte: 0, - DestDataAvailabilityMultiplierBps: 0, - PriceRegistry: cciptypes.Address(legacyDynamicConfig.PriceRegistry.String()), - MaxDataBytes: legacyDynamicConfig.MaxDataSize, - MaxPerMsgGasLimit: uint32(legacyDynamicConfig.MaxGasLimit), - }, nil -} - -func (o *OnRamp) SourcePriceRegistryAddress(ctx context.Context) (cciptypes.Address, error) { - return o.cachedSourcePriceRegistryAddress.Get(ctx, func(ctx context.Context) (cciptypes.Address, error) { - c, err := o.GetDynamicConfig(ctx) +func (o *OnRamp) GetDynamicConfig(ctx context.Context) (cciptypes.OnRampDynamicConfig, error) { + return o.cachedOnRampDynamicConfig.Get(ctx, func(ctx context.Context) (cciptypes.OnRampDynamicConfig, error) { + if o.onRamp == nil { + return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("onramp not initialized") + } + legacyDynamicConfig, err := o.onRamp.GetDynamicConfig(&bind.CallOpts{Context: ctx}) if err != nil { - return "", err + return cciptypes.OnRampDynamicConfig{}, err } - return c.PriceRegistry, nil + return cciptypes.OnRampDynamicConfig{ + Router: cciptypes.Address(legacyDynamicConfig.Router.String()), + MaxNumberOfTokensPerMsg: legacyDynamicConfig.MaxTokensLength, + DestGasOverhead: 0, + DestGasPerPayloadByte: 0, + DestDataAvailabilityOverheadGas: 0, + DestGasPerDataAvailabilityByte: 0, + DestDataAvailabilityMultiplierBps: 0, + PriceRegistry: cciptypes.Address(legacyDynamicConfig.PriceRegistry.String()), + MaxDataBytes: legacyDynamicConfig.MaxDataSize, + MaxPerMsgGasLimit: uint32(legacyDynamicConfig.MaxGasLimit), + }, nil }) } +func (o *OnRamp) SourcePriceRegistryAddress(ctx context.Context) (cciptypes.Address, error) { + c, err := o.GetDynamicConfig(ctx) + if err != nil { + return "", err + } + return c.PriceRegistry, nil +} + func (o *OnRamp) GetSendRequestsBetweenSeqNums(ctx context.Context, seqNumMin, seqNumMax uint64, finalized bool) ([]cciptypes.EVM2EVMMessageWithTxMeta, error) { logs, err := o.lp.LogsDataWordRange( ctx, @@ -165,8 +165,8 @@ func (o *OnRamp) GetSendRequestsBetweenSeqNums(ctx context.Context, seqNumMin, s return res, nil } -func (o *OnRamp) RouterAddress(context.Context) (cciptypes.Address, error) { - config, err := o.onRamp.GetDynamicConfig(nil) +func (o *OnRamp) RouterAddress(ctx context.Context) (cciptypes.Address, error) { + config, err := o.onRamp.GetDynamicConfig(&bind.CallOpts{Context: ctx}) if err != nil { return "", err } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/commit_store.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/commit_store.go index 7612e544195..ecc8acb5765 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/commit_store.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/commit_store.go @@ -49,9 +49,10 @@ type CommitStore struct { commitReportArgs abi.Arguments // Dynamic config - configMu sync.RWMutex - gasPriceEstimator *prices.DAGasPriceEstimator - offchainConfig cciptypes.CommitOffchainConfig + configMu sync.RWMutex + gasPriceEstimator *prices.DAGasPriceEstimator + offchainConfig cciptypes.CommitOffchainConfig + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader } func (c *CommitStore) GetCommitStoreStaticConfig(ctx context.Context) (cciptypes.CommitStoreStaticConfig, error) { @@ -255,6 +256,7 @@ func (c *CommitStore) ChangeConfig(_ context.Context, onchainConfig []byte, offc c.sourceMaxGasPrice, int64(offchainConfigParsed.ExecGasPriceDeviationPPB), int64(offchainConfigParsed.DAGasPriceDeviationPPB), + c.feeEstimatorConfig, ) c.offchainConfig = ccipdata.NewCommitOffchainConfig( offchainConfigParsed.ExecGasPriceDeviationPPB, @@ -430,7 +432,7 @@ func (c *CommitStore) RegisterFilters() error { return logpollerutil.RegisterLpFilters(c.lp, c.filters) } -func NewCommitStore(lggr logger.Logger, addr common.Address, ec client.Client, lp logpoller.LogPoller) (*CommitStore, error) { +func NewCommitStore(lggr logger.Logger, addr common.Address, ec client.Client, lp logpoller.LogPoller, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) (*CommitStore, error) { commitStore, err := commit_store_1_2_0.NewCommitStore(addr, ec) if err != nil { return nil, err @@ -463,7 +465,8 @@ func NewCommitStore(lggr logger.Logger, addr common.Address, ec client.Client, l configMu: sync.RWMutex{}, // The fields below are initially empty and set on ChangeConfig method - offchainConfig: cciptypes.CommitOffchainConfig{}, - gasPriceEstimator: nil, + offchainConfig: cciptypes.CommitOffchainConfig{}, + gasPriceEstimator: nil, + feeEstimatorConfig: feeEstimatorConfig, }, nil } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/commit_store_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/commit_store_test.go index 8b293096339..4307be0353c 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/commit_store_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/commit_store_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/pkg/config" - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" @@ -18,6 +17,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" + ccipdatamocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" ) func TestCommitReportEncoding(t *testing.T) { @@ -48,7 +48,9 @@ func TestCommitReportEncoding(t *testing.T) { Interval: cciptypes.CommitStoreInterval{Min: 1, Max: 10}, } - c, err := NewCommitStore(logger.TestLogger(t), utils.RandomAddress(), nil, mocks.NewLogPoller(t)) + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + + c, err := NewCommitStore(logger.TestLogger(t), utils.RandomAddress(), nil, mocks.NewLogPoller(t), feeEstimatorConfig) assert.NoError(t, err) encodedReport, err := c.EncodeCommitReport(ctx, report) diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/offramp.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/offramp.go index fa00894b380..f853adfb6fd 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/offramp.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/offramp.go @@ -123,7 +123,8 @@ func (c JSONExecOffchainConfig) Validate() error { // OffRamp In 1.2 we have a different estimator impl type OffRamp struct { *v1_0_0.OffRamp - offRampV120 evm_2_evm_offramp_1_2_0.EVM2EVMOffRampInterface + offRampV120 evm_2_evm_offramp_1_2_0.EVM2EVMOffRampInterface + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader } func (o *OffRamp) CurrentRateLimiterState(ctx context.Context) (cciptypes.TokenBucketRateLimit, error) { @@ -180,7 +181,7 @@ func (o *OffRamp) ChangeConfig(ctx context.Context, onchainConfigBytes []byte, o PermissionLessExecutionThresholdSeconds: time.Second * time.Duration(onchainConfigParsed.PermissionLessExecutionThresholdSeconds), Router: cciptypes.Address(onchainConfigParsed.Router.String()), } - priceEstimator := prices.NewDAGasPriceEstimator(o.Estimator, o.DestMaxGasPrice, 0, 0) + priceEstimator := prices.NewDAGasPriceEstimator(o.Estimator, o.DestMaxGasPrice, 0, 0, o.feeEstimatorConfig) o.UpdateDynamicConfig(onchainConfig, offchainConfig, priceEstimator) @@ -320,8 +321,16 @@ func (o *OffRamp) DecodeExecutionReport(ctx context.Context, report []byte) (cci return DecodeExecReport(ctx, o.ExecutionReportArgs, report) } -func NewOffRamp(lggr logger.Logger, addr common.Address, ec client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int) (*OffRamp, error) { - v100, err := v1_0_0.NewOffRamp(lggr, addr, ec, lp, estimator, destMaxGasPrice) +func NewOffRamp( + lggr logger.Logger, + addr common.Address, + ec client.Client, + lp logpoller.LogPoller, + estimator gas.EvmFeeEstimator, + destMaxGasPrice *big.Int, + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader, +) (*OffRamp, error) { + v100, err := v1_0_0.NewOffRamp(lggr, addr, ec, lp, estimator, destMaxGasPrice, feeEstimatorConfig) if err != nil { return nil, err } @@ -334,7 +343,8 @@ func NewOffRamp(lggr logger.Logger, addr common.Address, ec client.Client, lp lo v100.ExecutionReportArgs = abihelpers.MustGetMethodInputs("manuallyExecute", abiOffRamp)[:1] return &OffRamp{ - OffRamp: v100, - offRampV120: offRamp, + OffRamp: v100, + offRampV120: offRamp, + feeEstimatorConfig: feeEstimatorConfig, }, nil } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/offramp_reader_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/offramp_reader_test.go index f87fc8842f6..c2983492618 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/offramp_reader_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/offramp_reader_test.go @@ -11,6 +11,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0" ) @@ -25,7 +26,9 @@ func TestExecutionReportEncodingV120(t *testing.T) { ProofFlagBits: big.NewInt(133), } - offRamp, err := v1_2_0.NewOffRamp(logger.TestLogger(t), utils.RandomAddress(), nil, lpmocks.NewLogPoller(t), nil, nil) + feeEstimatorConfig := mocks.NewFeeEstimatorConfigReader(t) + + offRamp, err := v1_2_0.NewOffRamp(logger.TestLogger(t), utils.RandomAddress(), nil, lpmocks.NewLogPoller(t), nil, nil, feeEstimatorConfig) require.NoError(t, err) ctx := testutils.Context(t) diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/onramp.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/onramp.go index 29396823479..2de2b104c99 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/onramp.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/onramp.go @@ -49,16 +49,16 @@ var _ ccipdata.OnRampReader = &OnRamp{} // Significant change in 1.2: // - CCIPSendRequested event signature has changed type OnRamp struct { - onRamp *evm_2_evm_onramp_1_2_0.EVM2EVMOnRamp - address common.Address - lggr logger.Logger - lp logpoller.LogPoller - leafHasher ccipdata.LeafHasherInterface[[32]byte] - client client.Client - sendRequestedEventSig common.Hash - sendRequestedSeqNumberWord int - filters []logpoller.Filter - cachedSourcePriceRegistryAddress cache.AutoSync[cciptypes.Address] + onRamp *evm_2_evm_onramp_1_2_0.EVM2EVMOnRamp + address common.Address + lggr logger.Logger + lp logpoller.LogPoller + leafHasher ccipdata.LeafHasherInterface[[32]byte] + client client.Client + sendRequestedEventSig common.Hash + sendRequestedSeqNumberWord int + filters []logpoller.Filter + cachedOnRampDynamicConfig cache.AutoSync[cciptypes.OnRampDynamicConfig] // Static config can be cached, because it's never expected to change. // The only way to change that is through the contract's constructor (redeployment) cachedStaticConfig cache.OnceCtxFunction[evm_2_evm_onramp_1_2_0.EVM2EVMOnRampStaticConfig] @@ -107,7 +107,7 @@ func NewOnRamp(lggr logger.Logger, sourceSelector, destSelector uint64, onRampAd address: onRampAddress, sendRequestedSeqNumberWord: CCIPSendRequestSeqNumIndex, sendRequestedEventSig: CCIPSendRequestEventSig, - cachedSourcePriceRegistryAddress: cache.NewLogpollerEventsBased[cciptypes.Address]( + cachedOnRampDynamicConfig: cache.NewLogpollerEventsBased[cciptypes.OnRampDynamicConfig]( sourceLP, []common.Hash{ConfigSetEventSig}, onRampAddress, @@ -121,38 +121,39 @@ func (o *OnRamp) Address(context.Context) (cciptypes.Address, error) { return cciptypes.Address(o.onRamp.Address().String()), nil } -func (o *OnRamp) GetDynamicConfig(context.Context) (cciptypes.OnRampDynamicConfig, error) { - if o.onRamp == nil { - return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("onramp not initialized") - } - config, err := o.onRamp.GetDynamicConfig(&bind.CallOpts{}) - if err != nil { - return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("get dynamic config v1.2: %w", err) - } - return cciptypes.OnRampDynamicConfig{ - Router: cciptypes.Address(config.Router.String()), - MaxNumberOfTokensPerMsg: config.MaxNumberOfTokensPerMsg, - DestGasOverhead: config.DestGasOverhead, - DestGasPerPayloadByte: config.DestGasPerPayloadByte, - DestDataAvailabilityOverheadGas: config.DestDataAvailabilityOverheadGas, - DestGasPerDataAvailabilityByte: config.DestGasPerDataAvailabilityByte, - DestDataAvailabilityMultiplierBps: config.DestDataAvailabilityMultiplierBps, - PriceRegistry: cciptypes.Address(config.PriceRegistry.String()), - MaxDataBytes: config.MaxDataBytes, - MaxPerMsgGasLimit: config.MaxPerMsgGasLimit, - }, nil -} - -func (o *OnRamp) SourcePriceRegistryAddress(ctx context.Context) (cciptypes.Address, error) { - return o.cachedSourcePriceRegistryAddress.Get(ctx, func(ctx context.Context) (cciptypes.Address, error) { - c, err := o.GetDynamicConfig(ctx) +func (o *OnRamp) GetDynamicConfig(ctx context.Context) (cciptypes.OnRampDynamicConfig, error) { + return o.cachedOnRampDynamicConfig.Get(ctx, func(ctx context.Context) (cciptypes.OnRampDynamicConfig, error) { + if o.onRamp == nil { + return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("onramp not initialized") + } + config, err := o.onRamp.GetDynamicConfig(&bind.CallOpts{Context: ctx}) if err != nil { - return "", err + return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("get dynamic config v1.2: %w", err) } - return c.PriceRegistry, nil + + return cciptypes.OnRampDynamicConfig{ + Router: cciptypes.Address(config.Router.String()), + MaxNumberOfTokensPerMsg: config.MaxNumberOfTokensPerMsg, + DestGasOverhead: config.DestGasOverhead, + DestGasPerPayloadByte: config.DestGasPerPayloadByte, + DestDataAvailabilityOverheadGas: config.DestDataAvailabilityOverheadGas, + DestGasPerDataAvailabilityByte: config.DestGasPerDataAvailabilityByte, + DestDataAvailabilityMultiplierBps: config.DestDataAvailabilityMultiplierBps, + PriceRegistry: cciptypes.Address(config.PriceRegistry.String()), + MaxDataBytes: config.MaxDataBytes, + MaxPerMsgGasLimit: config.MaxPerMsgGasLimit, + }, nil }) } +func (o *OnRamp) SourcePriceRegistryAddress(ctx context.Context) (cciptypes.Address, error) { + c, err := o.GetDynamicConfig(ctx) + if err != nil { + return "", err + } + return c.PriceRegistry, nil +} + func (o *OnRamp) GetSendRequestsBetweenSeqNums(ctx context.Context, seqNumMin, seqNumMax uint64, finalized bool) ([]cciptypes.EVM2EVMMessageWithTxMeta, error) { logs, err := o.lp.LogsDataWordRange( ctx, diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/commit_store.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/commit_store.go index 3bb582f3a21..d5545174cb8 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/commit_store.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/commit_store.go @@ -3,6 +3,8 @@ package v1_5_0 import ( "context" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -41,8 +43,14 @@ func (c *CommitStore) IsDown(ctx context.Context) (bool, error) { return !unPausedAndNotCursed, nil } -func NewCommitStore(lggr logger.Logger, addr common.Address, ec client.Client, lp logpoller.LogPoller) (*CommitStore, error) { - v120, err := v1_2_0.NewCommitStore(lggr, addr, ec, lp) +func NewCommitStore( + lggr logger.Logger, + addr common.Address, + ec client.Client, + lp logpoller.LogPoller, + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader, +) (*CommitStore, error) { + v120, err := v1_2_0.NewCommitStore(lggr, addr, ec, lp, feeEstimatorConfig) if err != nil { return nil, err } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/offramp.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/offramp.go index 15f52bd7139..11e7be1a55c 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/offramp.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/offramp.go @@ -70,6 +70,7 @@ type OffRamp struct { *v1_2_0.OffRamp offRampV150 evm_2_evm_offramp.EVM2EVMOffRampInterface cachedRateLimitTokens cache.AutoSync[cciptypes.OffRampTokens] + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader } // GetTokens Returns no data as the offRamps no longer have this information. @@ -155,7 +156,7 @@ func (o *OffRamp) ChangeConfig(ctx context.Context, onchainConfigBytes []byte, o PermissionLessExecutionThresholdSeconds: time.Second * time.Duration(onchainConfigParsed.PermissionLessExecutionThresholdSeconds), Router: cciptypes.Address(onchainConfigParsed.Router.String()), } - priceEstimator := prices.NewDAGasPriceEstimator(o.Estimator, o.DestMaxGasPrice, 0, 0) + priceEstimator := prices.NewDAGasPriceEstimator(o.Estimator, o.DestMaxGasPrice, 0, 0, o.feeEstimatorConfig) o.UpdateDynamicConfig(onchainConfig, offchainConfig, priceEstimator) @@ -166,8 +167,16 @@ func (o *OffRamp) ChangeConfig(ctx context.Context, onchainConfigBytes []byte, o cciptypes.Address(destWrappedNative.String()), nil } -func NewOffRamp(lggr logger.Logger, addr common.Address, ec client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int) (*OffRamp, error) { - v120, err := v1_2_0.NewOffRamp(lggr, addr, ec, lp, estimator, destMaxGasPrice) +func NewOffRamp( + lggr logger.Logger, + addr common.Address, + ec client.Client, + lp logpoller.LogPoller, + estimator gas.EvmFeeEstimator, + destMaxGasPrice *big.Int, + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader, +) (*OffRamp, error) { + v120, err := v1_2_0.NewOffRamp(lggr, addr, ec, lp, estimator, destMaxGasPrice, feeEstimatorConfig) if err != nil { return nil, err } @@ -180,8 +189,9 @@ func NewOffRamp(lggr logger.Logger, addr common.Address, ec client.Client, lp lo v120.ExecutionReportArgs = abihelpers.MustGetMethodInputs("manuallyExecute", abiOffRamp)[:1] return &OffRamp{ - OffRamp: v120, - offRampV150: offRamp, + feeEstimatorConfig: feeEstimatorConfig, + OffRamp: v120, + offRampV150: offRamp, cachedRateLimitTokens: cache.NewLogpollerEventsBased[cciptypes.OffRampTokens]( lp, []common.Hash{RateLimitTokenAddedEvent, RateLimitTokenRemovedEvent}, diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/onramp.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/onramp.go index 354a5defdde..5a9377858d4 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/onramp.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/onramp.go @@ -50,17 +50,17 @@ func init() { var _ ccipdata.OnRampReader = &OnRamp{} type OnRamp struct { - onRamp *evm_2_evm_onramp.EVM2EVMOnRamp - address common.Address - destChainSelectorBytes [16]byte - lggr logger.Logger - lp logpoller.LogPoller - leafHasher ccipdata.LeafHasherInterface[[32]byte] - client client.Client - sendRequestedEventSig common.Hash - sendRequestedSeqNumberWord int - filters []logpoller.Filter - cachedSourcePriceRegistryAddress cache.AutoSync[cciptypes.Address] + onRamp *evm_2_evm_onramp.EVM2EVMOnRamp + address common.Address + destChainSelectorBytes [16]byte + lggr logger.Logger + lp logpoller.LogPoller + leafHasher ccipdata.LeafHasherInterface[[32]byte] + client client.Client + sendRequestedEventSig common.Hash + sendRequestedSeqNumberWord int + filters []logpoller.Filter + cachedOnRampDynamicConfig cache.AutoSync[cciptypes.OnRampDynamicConfig] // Static config can be cached, because it's never expected to change. // The only way to change that is through the contract's constructor (redeployment) cachedStaticConfig cache.OnceCtxFunction[evm_2_evm_onramp.EVM2EVMOnRampStaticConfig] @@ -112,7 +112,7 @@ func NewOnRamp(lggr logger.Logger, sourceSelector, destSelector uint64, onRampAd address: onRampAddress, sendRequestedSeqNumberWord: CCIPSendRequestSeqNumIndex, sendRequestedEventSig: CCIPSendRequestEventSig, - cachedSourcePriceRegistryAddress: cache.NewLogpollerEventsBased[cciptypes.Address]( + cachedOnRampDynamicConfig: cache.NewLogpollerEventsBased[cciptypes.OnRampDynamicConfig]( sourceLP, []common.Hash{ConfigSetEventSig}, onRampAddress, @@ -126,38 +126,39 @@ func (o *OnRamp) Address(context.Context) (cciptypes.Address, error) { return ccipcalc.EvmAddrToGeneric(o.onRamp.Address()), nil } -func (o *OnRamp) GetDynamicConfig(context.Context) (cciptypes.OnRampDynamicConfig, error) { - if o.onRamp == nil { - return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("onramp not initialized") - } - config, err := o.onRamp.GetDynamicConfig(&bind.CallOpts{}) - if err != nil { - return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("get dynamic config v1.5: %w", err) - } - return cciptypes.OnRampDynamicConfig{ - Router: ccipcalc.EvmAddrToGeneric(config.Router), - MaxNumberOfTokensPerMsg: config.MaxNumberOfTokensPerMsg, - DestGasOverhead: config.DestGasOverhead, - DestGasPerPayloadByte: config.DestGasPerPayloadByte, - DestDataAvailabilityOverheadGas: config.DestDataAvailabilityOverheadGas, - DestGasPerDataAvailabilityByte: config.DestGasPerDataAvailabilityByte, - DestDataAvailabilityMultiplierBps: config.DestDataAvailabilityMultiplierBps, - PriceRegistry: ccipcalc.EvmAddrToGeneric(config.PriceRegistry), - MaxDataBytes: config.MaxDataBytes, - MaxPerMsgGasLimit: config.MaxPerMsgGasLimit, - }, nil -} - -func (o *OnRamp) SourcePriceRegistryAddress(ctx context.Context) (cciptypes.Address, error) { - return o.cachedSourcePriceRegistryAddress.Get(ctx, func(ctx context.Context) (cciptypes.Address, error) { - c, err := o.GetDynamicConfig(ctx) +func (o *OnRamp) GetDynamicConfig(ctx context.Context) (cciptypes.OnRampDynamicConfig, error) { + return o.cachedOnRampDynamicConfig.Get(ctx, func(ctx context.Context) (cciptypes.OnRampDynamicConfig, error) { + if o.onRamp == nil { + return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("onramp not initialized") + } + config, err := o.onRamp.GetDynamicConfig(&bind.CallOpts{}) if err != nil { - return "", err + return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("get dynamic config v1.5: %w", err) } - return c.PriceRegistry, nil + + return cciptypes.OnRampDynamicConfig{ + Router: ccipcalc.EvmAddrToGeneric(config.Router), + MaxNumberOfTokensPerMsg: config.MaxNumberOfTokensPerMsg, + DestGasOverhead: config.DestGasOverhead, + DestGasPerPayloadByte: config.DestGasPerPayloadByte, + DestDataAvailabilityOverheadGas: config.DestDataAvailabilityOverheadGas, + DestGasPerDataAvailabilityByte: config.DestGasPerDataAvailabilityByte, + DestDataAvailabilityMultiplierBps: config.DestDataAvailabilityMultiplierBps, + PriceRegistry: ccipcalc.EvmAddrToGeneric(config.PriceRegistry), + MaxDataBytes: config.MaxDataBytes, + MaxPerMsgGasLimit: config.MaxPerMsgGasLimit, + }, nil }) } +func (o *OnRamp) SourcePriceRegistryAddress(ctx context.Context) (cciptypes.Address, error) { + c, err := o.GetDynamicConfig(ctx) + if err != nil { + return "", err + } + return c.PriceRegistry, nil +} + func (o *OnRamp) GetSendRequestsBetweenSeqNums(ctx context.Context, seqNumMin, seqNumMax uint64, finalized bool) ([]cciptypes.EVM2EVMMessageWithTxMeta, error) { logs, err := o.lp.LogsDataWordRange( ctx, diff --git a/core/services/ocr2/plugins/ccip/prices/da_price_estimator.go b/core/services/ocr2/plugins/ccip/prices/da_price_estimator.go index 7c75b9bdd99..34239398c81 100644 --- a/core/services/ocr2/plugins/ccip/prices/da_price_estimator.go +++ b/core/services/ocr2/plugins/ccip/prices/da_price_estimator.go @@ -5,6 +5,8 @@ import ( "fmt" "math/big" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" @@ -14,11 +16,9 @@ import ( type DAGasPriceEstimator struct { execEstimator GasPriceEstimator l1Oracle rollups.L1Oracle + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader priceEncodingLength uint daDeviationPPB int64 - daOverheadGas int64 - gasPerDAByte int64 - daMultiplier int64 } func NewDAGasPriceEstimator( @@ -26,12 +26,14 @@ func NewDAGasPriceEstimator( maxGasPrice *big.Int, deviationPPB int64, daDeviationPPB int64, + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader, // DA Config Cache updates in the onRamp reader and shares the state ) *DAGasPriceEstimator { return &DAGasPriceEstimator{ execEstimator: NewExecGasPriceEstimator(estimator, maxGasPrice, deviationPPB), l1Oracle: estimator.L1Oracle(), priceEncodingLength: daGasPriceEncodingLength, daDeviationPPB: daDeviationPPB, + feeEstimatorConfig: feeEstimatorConfig, } } @@ -41,6 +43,7 @@ func (g DAGasPriceEstimator) GetGasPrice(ctx context.Context) (*big.Int, error) return nil, err } var gasPrice *big.Int = execGasPrice + if gasPrice.BitLen() > int(g.priceEncodingLength) { return nil, fmt.Errorf("native gas price exceeded max range %+v", gasPrice) } @@ -54,7 +57,14 @@ func (g DAGasPriceEstimator) GetGasPrice(ctx context.Context) (*big.Int, error) return nil, err } - if daGasPrice := daGasPriceWei.ToInt(); daGasPrice.Cmp(big.NewInt(0)) > 0 { + daGasPrice := daGasPriceWei.ToInt() + + gasPrice, daGasPrice, err = g.feeEstimatorConfig.ModifyGasPriceComponents(ctx, gasPrice, daGasPrice) + if err != nil { + return nil, fmt.Errorf("gasPrice modification failed: %v", err) + } + + if daGasPrice.Cmp(big.NewInt(0)) > 0 { if daGasPrice.BitLen() > int(g.priceEncodingLength) { return nil, fmt.Errorf("data availability gas price exceeded max range %+v", daGasPrice) } @@ -141,7 +151,10 @@ func (g DAGasPriceEstimator) EstimateMsgCostUSD(p *big.Int, wrappedNativePrice * // If there is data availability price component, then include data availability cost in fee estimation if daGasPrice.Cmp(big.NewInt(0)) > 0 { - daGasCostUSD := g.estimateDACostUSD(daGasPrice, wrappedNativePrice, msg) + daGasCostUSD, err := g.estimateDACostUSD(daGasPrice, wrappedNativePrice, msg) + if err != nil { + return nil, err + } execCostUSD = new(big.Int).Add(daGasCostUSD, execCostUSD) } return execCostUSD, nil @@ -160,17 +173,22 @@ func (g DAGasPriceEstimator) parseEncodedGasPrice(p *big.Int) (*big.Int, *big.In return daGasPrice, execGasPrice, nil } -func (g DAGasPriceEstimator) estimateDACostUSD(daGasPrice *big.Int, wrappedNativePrice *big.Int, msg cciptypes.EVM2EVMOnRampCCIPSendRequestedWithMeta) *big.Int { +func (g DAGasPriceEstimator) estimateDACostUSD(daGasPrice *big.Int, wrappedNativePrice *big.Int, msg cciptypes.EVM2EVMOnRampCCIPSendRequestedWithMeta) (*big.Int, error) { var sourceTokenDataLen int for _, tokenData := range msg.SourceTokenData { sourceTokenDataLen += len(tokenData) } + daOverheadGas, gasPerDAByte, daMultiplier, err := g.feeEstimatorConfig.GetDataAvailabilityConfig(context.Background()) + if err != nil { + return nil, err + } + dataLen := evmMessageFixedBytes + len(msg.Data) + len(msg.TokenAmounts)*evmMessageBytesPerToken + sourceTokenDataLen - dataGas := big.NewInt(int64(dataLen)*g.gasPerDAByte + g.daOverheadGas) + dataGas := big.NewInt(int64(dataLen)*gasPerDAByte + daOverheadGas) dataGasEstimate := new(big.Int).Mul(dataGas, daGasPrice) - dataGasEstimate = new(big.Int).Div(new(big.Int).Mul(dataGasEstimate, big.NewInt(g.daMultiplier)), big.NewInt(daMultiplierBase)) + dataGasEstimate = new(big.Int).Div(new(big.Int).Mul(dataGasEstimate, big.NewInt(daMultiplier)), big.NewInt(daMultiplierBase)) - return ccipcalc.CalculateUsdPerUnitGas(dataGasEstimate, wrappedNativePrice) + return ccipcalc.CalculateUsdPerUnitGas(dataGasEstimate, wrappedNativePrice), nil } diff --git a/core/services/ocr2/plugins/ccip/prices/da_price_estimator_test.go b/core/services/ocr2/plugins/ccip/prices/da_price_estimator_test.go index 2f8616a8669..52ef8c3800a 100644 --- a/core/services/ocr2/plugins/ccip/prices/da_price_estimator_test.go +++ b/core/services/ocr2/plugins/ccip/prices/da_price_estimator_test.go @@ -2,6 +2,7 @@ package prices import ( "context" + "errors" "math/big" "testing" @@ -11,6 +12,7 @@ import ( cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups/mocks" + ccipdatamocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" ) func encodeGasPrice(daPrice, execPrice *big.Int) *big.Int { @@ -21,11 +23,13 @@ func TestDAPriceEstimator_GetGasPrice(t *testing.T) { ctx := context.Background() testCases := []struct { - name string - daGasPrice *big.Int - execGasPrice *big.Int - expPrice *big.Int - expErr bool + name string + daGasPrice *big.Int + execGasPrice *big.Int + expPrice *big.Int + modExecGasPrice *big.Int + modDAGasPrice *big.Int + expErr bool }{ { name: "base", @@ -55,6 +59,31 @@ func TestDAPriceEstimator_GetGasPrice(t *testing.T) { expPrice: encodeGasPrice(big.NewInt(1e9), big.NewInt(0)), expErr: false, }, + { + name: "execGasPrice Modified", + daGasPrice: big.NewInt(1e9), + execGasPrice: big.NewInt(0), + modExecGasPrice: big.NewInt(1), + expPrice: encodeGasPrice(big.NewInt(1e9), big.NewInt(1)), + expErr: false, + }, + { + name: "daGasPrice Modified", + daGasPrice: big.NewInt(1e9), + execGasPrice: big.NewInt(0), + modDAGasPrice: big.NewInt(1), + expPrice: encodeGasPrice(big.NewInt(1), big.NewInt(0)), + expErr: false, + }, + { + name: "daGasPrice and execGasPrice Modified", + daGasPrice: big.NewInt(1e9), + execGasPrice: big.NewInt(0), + modDAGasPrice: big.NewInt(1), + modExecGasPrice: big.NewInt(2), + expPrice: encodeGasPrice(big.NewInt(1), big.NewInt(2)), + expErr: false, + }, { name: "price out of bounds", daGasPrice: new(big.Int).Lsh(big.NewInt(1), daGasPriceEncodingLength), @@ -72,10 +101,25 @@ func TestDAPriceEstimator_GetGasPrice(t *testing.T) { l1Oracle := mocks.NewL1Oracle(t) l1Oracle.On("GasPrice", ctx).Return(assets.NewWei(tc.daGasPrice), nil) + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + + modRespExecGasPrice := tc.execGasPrice + if tc.modExecGasPrice != nil { + modRespExecGasPrice = tc.modExecGasPrice + } + + modRespDAGasPrice := tc.daGasPrice + if tc.modDAGasPrice != nil { + modRespDAGasPrice = tc.modDAGasPrice + } + feeEstimatorConfig.On("ModifyGasPriceComponents", mock.Anything, tc.execGasPrice, tc.daGasPrice). + Return(modRespExecGasPrice, modRespDAGasPrice, nil) + g := DAGasPriceEstimator{ execEstimator: execEstimator, l1Oracle: l1Oracle, priceEncodingLength: daGasPriceEncodingLength, + feeEstimatorConfig: feeEstimatorConfig, } gasPrice, err := g.GetGasPrice(ctx) @@ -325,14 +369,17 @@ func TestDAPriceEstimator_EstimateMsgCostUSD(t *testing.T) { execCostUSD := big.NewInt(100_000) testCases := []struct { - name string - gasPrice *big.Int - wrappedNativePrice *big.Int - msg cciptypes.EVM2EVMOnRampCCIPSendRequestedWithMeta - daOverheadGas int64 - gasPerDAByte int64 - daMultiplier int64 - expUSD *big.Int + name string + gasPrice *big.Int + wrappedNativePrice *big.Int + msg cciptypes.EVM2EVMOnRampCCIPSendRequestedWithMeta + daOverheadGas int64 + gasPerDAByte int64 + daMultiplier int64 + expUSD *big.Int + onRampConfig cciptypes.OnRampDynamicConfig + execEstimatorResponse []any + execEstimatorErr error }{ { name: "only DA overhead", @@ -345,10 +392,8 @@ func TestDAPriceEstimator_EstimateMsgCostUSD(t *testing.T) { SourceTokenData: [][]byte{}, }, }, - daOverheadGas: 100_000, - gasPerDAByte: 0, - daMultiplier: 10_000, // 1x multiplier - expUSD: new(big.Int).Add(execCostUSD, big.NewInt(100_000e9)), + expUSD: new(big.Int).Add(execCostUSD, big.NewInt(100_000e9)), + execEstimatorResponse: []any{int64(100_000), int64(0), int64(10_000), nil}, }, { name: "include message data gas", @@ -363,10 +408,8 @@ func TestDAPriceEstimator_EstimateMsgCostUSD(t *testing.T) { }, }, }, - daOverheadGas: 100_000, - gasPerDAByte: 16, - daMultiplier: 10_000, // 1x multiplier - expUSD: new(big.Int).Add(execCostUSD, big.NewInt(134_208e9)), + expUSD: new(big.Int).Add(execCostUSD, big.NewInt(134_208e9)), + execEstimatorResponse: []any{int64(100_000), int64(16), int64(10_000), nil}, }, { name: "zero DA price", @@ -379,10 +422,7 @@ func TestDAPriceEstimator_EstimateMsgCostUSD(t *testing.T) { SourceTokenData: [][]byte{}, }, }, - daOverheadGas: 100_000, - gasPerDAByte: 16, - daMultiplier: 10_000, // 1x multiplier - expUSD: execCostUSD, + expUSD: execCostUSD, }, { name: "double native price", @@ -395,10 +435,8 @@ func TestDAPriceEstimator_EstimateMsgCostUSD(t *testing.T) { SourceTokenData: [][]byte{}, }, }, - daOverheadGas: 100_000, - gasPerDAByte: 0, - daMultiplier: 10_000, // 1x multiplier - expUSD: new(big.Int).Add(execCostUSD, big.NewInt(200_000e9)), + expUSD: new(big.Int).Add(execCostUSD, big.NewInt(200_000e9)), + execEstimatorResponse: []any{int64(100_000), int64(0), int64(10_000), nil}, }, { name: "half multiplier", @@ -411,30 +449,66 @@ func TestDAPriceEstimator_EstimateMsgCostUSD(t *testing.T) { SourceTokenData: [][]byte{}, }, }, - daOverheadGas: 100_000, - gasPerDAByte: 0, - daMultiplier: 5_000, // 0.5x multiplier - expUSD: new(big.Int).Add(execCostUSD, big.NewInt(50_000e9)), + expUSD: new(big.Int).Add(execCostUSD, big.NewInt(50_000e9)), + execEstimatorResponse: []any{int64(100_000), int64(0), int64(5_000), nil}, + }, + { + name: "onRamp reader error", + gasPrice: encodeGasPrice(big.NewInt(1e9), big.NewInt(0)), // 1 gwei DA price, 0 exec price + wrappedNativePrice: big.NewInt(1e18), // $1 + msg: cciptypes.EVM2EVMOnRampCCIPSendRequestedWithMeta{ + EVM2EVMMessage: cciptypes.EVM2EVMMessage{ + Data: []byte{}, + TokenAmounts: []cciptypes.TokenAmount{}, + SourceTokenData: [][]byte{}, + }, + }, + execEstimatorResponse: []any{int64(0), int64(0), int64(0), errors.New("some reader error")}, + }, + { + name: "execEstimator error", + gasPrice: encodeGasPrice(big.NewInt(1e9), big.NewInt(0)), // 1 gwei DA price, 0 exec price + wrappedNativePrice: big.NewInt(1e18), // $1 + msg: cciptypes.EVM2EVMOnRampCCIPSendRequestedWithMeta{ + EVM2EVMMessage: cciptypes.EVM2EVMMessage{ + Data: []byte{}, + TokenAmounts: []cciptypes.TokenAmount{}, + SourceTokenData: [][]byte{}, + }, + }, + execEstimatorErr: errors.New("some estimator error"), }, } for _, tc := range testCases { - execEstimator := NewMockGasPriceEstimator(t) - execEstimator.On("EstimateMsgCostUSD", mock.Anything, tc.wrappedNativePrice, tc.msg).Return(execCostUSD, nil) - t.Run(tc.name, func(t *testing.T) { + execEstimator := NewMockGasPriceEstimator(t) + execEstimator.On("EstimateMsgCostUSD", mock.Anything, tc.wrappedNativePrice, tc.msg). + Return(execCostUSD, tc.execEstimatorErr) + + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + if len(tc.execEstimatorResponse) > 0 { + feeEstimatorConfig.On("GetDataAvailabilityConfig", mock.Anything). + Return(tc.execEstimatorResponse...) + } + g := DAGasPriceEstimator{ execEstimator: execEstimator, l1Oracle: nil, priceEncodingLength: daGasPriceEncodingLength, - daOverheadGas: tc.daOverheadGas, - gasPerDAByte: tc.gasPerDAByte, - daMultiplier: tc.daMultiplier, + feeEstimatorConfig: feeEstimatorConfig, } costUSD, err := g.EstimateMsgCostUSD(tc.gasPrice, tc.wrappedNativePrice, tc.msg) - assert.NoError(t, err) - assert.Equal(t, tc.expUSD, costUSD) + + switch { + case len(tc.execEstimatorResponse) == 4 && tc.execEstimatorResponse[3] != nil, + tc.execEstimatorErr != nil: + assert.Error(t, err) + default: + assert.NoError(t, err) + assert.Equal(t, tc.expUSD, costUSD) + } }) } } diff --git a/core/services/ocr2/plugins/ccip/prices/exec_price_estimator.go b/core/services/ocr2/plugins/ccip/prices/exec_price_estimator.go index 56e1ddb583e..84a6014bef3 100644 --- a/core/services/ocr2/plugins/ccip/prices/exec_price_estimator.go +++ b/core/services/ocr2/plugins/ccip/prices/exec_price_estimator.go @@ -26,7 +26,7 @@ func NewExecGasPriceEstimator(estimator gas.EvmFeeEstimator, maxGasPrice *big.In } func (g ExecGasPriceEstimator) GetGasPrice(ctx context.Context) (*big.Int, error) { - gasPriceWei, _, err := g.estimator.GetFee(ctx, nil, 0, assets.NewWei(g.maxGasPrice)) + gasPriceWei, _, err := g.estimator.GetFee(ctx, nil, 0, assets.NewWei(g.maxGasPrice), nil, nil) if err != nil { return nil, err } diff --git a/core/services/ocr2/plugins/ccip/prices/exec_price_estimator_test.go b/core/services/ocr2/plugins/ccip/prices/exec_price_estimator_test.go index e1c2fa03981..f9ba1523e52 100644 --- a/core/services/ocr2/plugins/ccip/prices/exec_price_estimator_test.go +++ b/core/services/ocr2/plugins/ccip/prices/exec_price_estimator_test.go @@ -5,6 +5,7 @@ import ( "math/big" "testing" + "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" "github.com/stretchr/testify/assert" @@ -85,7 +86,7 @@ func TestExecPriceEstimator_GetGasPrice(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { sourceFeeEstimator := mocks.NewEvmFeeEstimator(t) - sourceFeeEstimator.On("GetFee", ctx, []byte(nil), uint64(0), assets.NewWei(tc.maxGasPrice)).Return( + sourceFeeEstimator.On("GetFee", ctx, []byte(nil), uint64(0), assets.NewWei(tc.maxGasPrice), (*common.Address)(nil), (*common.Address)(nil)).Return( tc.sourceFeeEstimatorRespFee, uint64(0), tc.sourceFeeEstimatorRespErr) g := ExecGasPriceEstimator{ diff --git a/core/services/ocr2/plugins/ccip/prices/gas_price_estimator.go b/core/services/ocr2/plugins/ccip/prices/gas_price_estimator.go index 49a6fbcc4ad..4aac664e33e 100644 --- a/core/services/ocr2/plugins/ccip/prices/gas_price_estimator.go +++ b/core/services/ocr2/plugins/ccip/prices/gas_price_estimator.go @@ -3,6 +3,8 @@ package prices import ( "math/big" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" + "github.com/Masterminds/semver/v3" "github.com/pkg/errors" @@ -47,12 +49,13 @@ func NewGasPriceEstimatorForCommitPlugin( maxExecGasPrice *big.Int, daDeviationPPB int64, execDeviationPPB int64, + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader, ) (GasPriceEstimatorCommit, error) { switch commitStoreVersion.String() { case "1.0.0", "1.1.0": return NewExecGasPriceEstimator(estimator, maxExecGasPrice, execDeviationPPB), nil case "1.2.0": - return NewDAGasPriceEstimator(estimator, maxExecGasPrice, execDeviationPPB, daDeviationPPB), nil + return NewDAGasPriceEstimator(estimator, maxExecGasPrice, execDeviationPPB, daDeviationPPB, feeEstimatorConfig), nil default: return nil, errors.Errorf("Invalid commitStore version: %s", commitStoreVersion) } diff --git a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice.go b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice.go index f84a48c1ff8..36460683d43 100644 --- a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice.go +++ b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice.go @@ -45,7 +45,7 @@ func CheckGasPrice(ctx context.Context, upkeepId *big.Int, offchainConfigBytes [ } lggr.Debugf("successfully decode offchain config for %s, max gas price is %s", upkeepId.String(), offchainConfig.MaxGasPrice.String()) - fee, _, err := ge.GetFee(ctx, []byte{}, feeLimit, assets.NewWei(big.NewInt(maxFeePrice))) + fee, _, err := ge.GetFee(ctx, []byte{}, feeLimit, assets.NewWei(big.NewInt(maxFeePrice)), nil, nil) if err != nil { lggr.Errorw("failed to get fee, gas price check is disabled", "upkeepId", upkeepId.String(), "err", err) return encoding.UpkeepFailureReasonNone diff --git a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice_test.go b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice_test.go index 9b5640051df..7b5ef999f3d 100644 --- a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice_test.go +++ b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice_test.go @@ -86,13 +86,13 @@ func TestGasPrice_Check(t *testing.T) { ctx := testutils.Context(t) ge := gasMocks.NewEvmFeeEstimator(t) if test.FailedToGetFee { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( gas.EvmFee{}, feeLimit, errors.New("failed to retrieve gas price"), ) } else if test.CurrentLegacyGasPrice != nil { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( gas.EvmFee{ Legacy: assets.NewWei(test.CurrentLegacyGasPrice), }, @@ -100,7 +100,7 @@ func TestGasPrice_Check(t *testing.T) { nil, ) } else if test.CurrentDynamicGasPrice != nil { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( gas.EvmFee{ DynamicFeeCap: assets.NewWei(test.CurrentDynamicGasPrice), DynamicTipCap: assets.NewWei(big.NewInt(1_000_000_000)), diff --git a/core/services/relay/evm/ccip.go b/core/services/relay/evm/ccip.go index 34a732e1454..945763de85a 100644 --- a/core/services/relay/evm/ccip.go +++ b/core/services/relay/evm/ccip.go @@ -6,18 +6,16 @@ import ( "math/big" "time" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" - + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/logger" - - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/prices" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" ) var _ cciptypes.CommitStoreReader = (*IncompleteSourceCommitStoreReader)(nil) @@ -26,16 +24,18 @@ var _ cciptypes.CommitStoreReader = (*IncompleteDestCommitStoreReader)(nil) // IncompleteSourceCommitStoreReader is an implementation of CommitStoreReader with the only valid methods being // GasPriceEstimator, ChangeConfig, and OffchainConfig type IncompleteSourceCommitStoreReader struct { - estimator gas.EvmFeeEstimator - gasPriceEstimator *prices.DAGasPriceEstimator - sourceMaxGasPrice *big.Int - offchainConfig cciptypes.CommitOffchainConfig + estimator gas.EvmFeeEstimator + gasPriceEstimator *prices.DAGasPriceEstimator + sourceMaxGasPrice *big.Int + offchainConfig cciptypes.CommitOffchainConfig + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider } -func NewIncompleteSourceCommitStoreReader(estimator gas.EvmFeeEstimator, sourceMaxGasPrice *big.Int) *IncompleteSourceCommitStoreReader { +func NewIncompleteSourceCommitStoreReader(estimator gas.EvmFeeEstimator, sourceMaxGasPrice *big.Int, feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider) *IncompleteSourceCommitStoreReader { return &IncompleteSourceCommitStoreReader{ - estimator: estimator, - sourceMaxGasPrice: sourceMaxGasPrice, + estimator: estimator, + sourceMaxGasPrice: sourceMaxGasPrice, + feeEstimatorConfig: feeEstimatorConfig, } } @@ -55,6 +55,7 @@ func (i *IncompleteSourceCommitStoreReader) ChangeConfig(ctx context.Context, on i.sourceMaxGasPrice, int64(offchainConfigParsed.ExecGasPriceDeviationPPB), int64(offchainConfigParsed.DAGasPriceDeviationPPB), + i.feeEstimatorConfig, ) i.offchainConfig = ccip.NewCommitOffchainConfig( offchainConfigParsed.ExecGasPriceDeviationPPB, @@ -133,8 +134,15 @@ type IncompleteDestCommitStoreReader struct { cs cciptypes.CommitStoreReader } -func NewIncompleteDestCommitStoreReader(lggr logger.Logger, versionFinder ccip.VersionFinder, address cciptypes.Address, ec client.Client, lp logpoller.LogPoller) (*IncompleteDestCommitStoreReader, error) { - cs, err := ccip.NewCommitStoreReader(lggr, versionFinder, address, ec, lp) +func NewIncompleteDestCommitStoreReader( + lggr logger.Logger, + versionFinder ccip.VersionFinder, + address cciptypes.Address, + ec client.Client, + lp logpoller.LogPoller, + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider, +) (*IncompleteDestCommitStoreReader, error) { + cs, err := ccip.NewCommitStoreReader(lggr, versionFinder, address, ec, lp, feeEstimatorConfig) if err != nil { return nil, err } diff --git a/core/services/relay/evm/chain_writer.go b/core/services/relay/evm/chain_writer.go index f188ffeced2..466811d1151 100644 --- a/core/services/relay/evm/chain_writer.go +++ b/core/services/relay/evm/chain_writer.go @@ -186,7 +186,7 @@ func (w *chainWriter) GetFeeComponents(ctx context.Context) (*commontypes.ChainF return nil, fmt.Errorf("gas estimator not available") } - fee, _, err := w.ge.GetFee(ctx, nil, 0, w.maxGasPrice) + fee, _, err := w.ge.GetFee(ctx, nil, 0, w.maxGasPrice, nil, nil) if err != nil { return nil, err } diff --git a/core/services/relay/evm/chain_writer_test.go b/core/services/relay/evm/chain_writer_test.go index 66c85bfc2c3..e3fc8f8e226 100644 --- a/core/services/relay/evm/chain_writer_test.go +++ b/core/services/relay/evm/chain_writer_test.go @@ -86,7 +86,7 @@ func TestChainWriter(t *testing.T) { }) t.Run("GetFeeComponents", func(t *testing.T) { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ Legacy: assets.NewWei(big.NewInt(1000000001)), DynamicFeeCap: assets.NewWei(big.NewInt(1000000002)), DynamicTipCap: assets.NewWei(big.NewInt(1000000003)), @@ -112,7 +112,7 @@ func TestChainWriter(t *testing.T) { }) t.Run("Returns Legacy Fee in absence of Dynamic Fee", func(t *testing.T) { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ Legacy: assets.NewWei(big.NewInt(1000000001)), DynamicFeeCap: nil, DynamicTipCap: assets.NewWei(big.NewInt(1000000003)), @@ -124,7 +124,7 @@ func TestChainWriter(t *testing.T) { }) t.Run("Fails when neither legacy or dynamic fee is available", func(t *testing.T) { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ Legacy: nil, DynamicFeeCap: nil, DynamicTipCap: nil, @@ -136,7 +136,7 @@ func TestChainWriter(t *testing.T) { t.Run("Fails when GetFee returns an error", func(t *testing.T) { expectedErr := fmt.Errorf("GetFee error") - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ Legacy: nil, DynamicFeeCap: nil, DynamicTipCap: nil, @@ -146,7 +146,7 @@ func TestChainWriter(t *testing.T) { }) t.Run("Fails when L1Oracle returns error", func(t *testing.T) { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ Legacy: assets.NewWei(big.NewInt(1000000001)), DynamicFeeCap: assets.NewWei(big.NewInt(1000000002)), DynamicTipCap: assets.NewWei(big.NewInt(1000000003)), diff --git a/core/services/relay/evm/commit_provider.go b/core/services/relay/evm/commit_provider.go index 35de8efbd89..780d1cee7b3 100644 --- a/core/services/relay/evm/commit_provider.go +++ b/core/services/relay/evm/commit_provider.go @@ -18,18 +18,20 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig" ) var _ commontypes.CCIPCommitProvider = (*SrcCommitProvider)(nil) var _ commontypes.CCIPCommitProvider = (*DstCommitProvider)(nil) type SrcCommitProvider struct { - lggr logger.Logger - startBlock uint64 - client client.Client - lp logpoller.LogPoller - estimator gas.EvmFeeEstimator - maxGasPrice *big.Int + lggr logger.Logger + startBlock uint64 + client client.Client + lp logpoller.LogPoller + estimator gas.EvmFeeEstimator + maxGasPrice *big.Int + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider // these values will be lazily initialized seenOnRampAddress *cciptypes.Address @@ -44,14 +46,16 @@ func NewSrcCommitProvider( lp logpoller.LogPoller, srcEstimator gas.EvmFeeEstimator, maxGasPrice *big.Int, + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider, ) commontypes.CCIPCommitProvider { return &SrcCommitProvider{ - lggr: lggr, - startBlock: startBlock, - client: client, - lp: lp, - estimator: srcEstimator, - maxGasPrice: maxGasPrice, + lggr: lggr, + startBlock: startBlock, + client: client, + lp: lp, + estimator: srcEstimator, + maxGasPrice: maxGasPrice, + feeEstimatorConfig: feeEstimatorConfig, } } @@ -65,6 +69,7 @@ type DstCommitProvider struct { configWatcher *configWatcher gasEstimator gas.EvmFeeEstimator maxGasPrice big.Int + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider // these values will be lazily initialized seenCommitStoreAddress *cciptypes.Address @@ -81,6 +86,7 @@ func NewDstCommitProvider( maxGasPrice big.Int, contractTransmitter contractTransmitter, configWatcher *configWatcher, + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider, ) commontypes.CCIPCommitProvider { return &DstCommitProvider{ lggr: lggr, @@ -92,6 +98,7 @@ func NewDstCommitProvider( configWatcher: configWatcher, gasEstimator: gasEstimator, maxGasPrice: maxGasPrice, + feeEstimatorConfig: feeEstimatorConfig, } } @@ -170,13 +177,13 @@ func (P *DstCommitProvider) Close() error { if P.seenCommitStoreAddress == nil { return nil } - return ccip.CloseCommitStoreReader(P.lggr, versionFinder, *P.seenCommitStoreAddress, P.client, P.lp) + return ccip.CloseCommitStoreReader(P.lggr, versionFinder, *P.seenCommitStoreAddress, P.client, P.lp, P.feeEstimatorConfig) }) unregisterFuncs = append(unregisterFuncs, func() error { if P.seenOffRampAddress == nil { return nil } - return ccip.CloseOffRampReader(P.lggr, versionFinder, *P.seenOffRampAddress, P.client, P.lp, nil, big.NewInt(0)) + return ccip.CloseOffRampReader(P.lggr, versionFinder, *P.seenOffRampAddress, P.client, P.lp, nil, big.NewInt(0), P.feeEstimatorConfig) }) var multiErr error @@ -241,7 +248,7 @@ func (P *DstCommitProvider) NewPriceGetter(ctx context.Context) (priceGetter cci } func (P *SrcCommitProvider) NewCommitStoreReader(ctx context.Context, commitStoreAddress cciptypes.Address) (commitStoreReader cciptypes.CommitStoreReader, err error) { - commitStoreReader = NewIncompleteSourceCommitStoreReader(P.estimator, P.maxGasPrice) + commitStoreReader = NewIncompleteSourceCommitStoreReader(P.estimator, P.maxGasPrice, P.feeEstimatorConfig) return } @@ -249,7 +256,7 @@ func (P *DstCommitProvider) NewCommitStoreReader(ctx context.Context, commitStor P.seenCommitStoreAddress = &commitStoreAddress versionFinder := ccip.NewEvmVersionFinder() - commitStoreReader, err = NewIncompleteDestCommitStoreReader(P.lggr, versionFinder, commitStoreAddress, P.client, P.lp) + commitStoreReader, err = NewIncompleteDestCommitStoreReader(P.lggr, versionFinder, commitStoreAddress, P.client, P.lp, P.feeEstimatorConfig) return } @@ -259,7 +266,12 @@ func (P *SrcCommitProvider) NewOnRampReader(ctx context.Context, onRampAddress c P.seenDestChainSelector = &destChainSelector versionFinder := ccip.NewEvmVersionFinder() + onRampReader, err = ccip.NewOnRampReader(P.lggr, versionFinder, sourceChainSelector, destChainSelector, onRampAddress, P.lp, P.client) + if err != nil { + return nil, err + } + P.feeEstimatorConfig.SetOnRampReader(onRampReader) return } @@ -272,7 +284,7 @@ func (P *SrcCommitProvider) NewOffRampReader(ctx context.Context, offRampAddr cc } func (P *DstCommitProvider) NewOffRampReader(ctx context.Context, offRampAddr cciptypes.Address) (offRampReader cciptypes.OffRampReader, err error) { - offRampReader, err = ccip.NewOffRampReader(P.lggr, P.versionFinder, offRampAddr, P.client, P.lp, P.gasEstimator, &P.maxGasPrice, true) + offRampReader, err = ccip.NewOffRampReader(P.lggr, P.versionFinder, offRampAddr, P.client, P.lp, P.gasEstimator, &P.maxGasPrice, true, P.feeEstimatorConfig) return } diff --git a/core/services/relay/evm/evm.go b/core/services/relay/evm/evm.go index fe849499031..f2143cededa 100644 --- a/core/services/relay/evm/evm.go +++ b/core/services/relay/evm/evm.go @@ -13,6 +13,8 @@ import ( cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig/interceptors/mantle" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipcommit" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipexec" @@ -45,6 +47,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/keystore" "github.com/smartcontractkit/chainlink/v2/core/services/llo" "github.com/smartcontractkit/chainlink/v2/core/services/llo/bm" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig" lloconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/llo/config" mercuryconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/mercury/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" @@ -406,6 +409,20 @@ func (r *Relayer) NewCCIPCommitProvider(rargs commontypes.RelayArgs, pargs commo sourceStartBlock := commitPluginConfig.SourceStartBlock destStartBlock := commitPluginConfig.DestStartBlock + feeEstimatorConfig := estimatorconfig.NewFeeEstimatorConfigService() + + // CCIPCommit reads only when source chain is Mantle, then reports to dest chain + // to minimize misconfigure risk, might make sense to wire Mantle only when Commit + Mantle + IsSourceProvider + if r.chain.Config().EVM().ChainID().Uint64() == 5003 || r.chain.Config().EVM().ChainID().Uint64() == 5000 { + if commitPluginConfig.IsSourceProvider { + mantleInterceptor, iErr := mantle.NewInterceptor(ctx, r.chain.Client()) + if iErr != nil { + return nil, iErr + } + feeEstimatorConfig.AddGasPriceInterceptor(mantleInterceptor) + } + } + // The src chain implementation of this provider does not need a configWatcher or contractTransmitter; // bail early. if commitPluginConfig.IsSourceProvider { @@ -416,6 +433,7 @@ func (r *Relayer) NewCCIPCommitProvider(rargs commontypes.RelayArgs, pargs commo r.chain.LogPoller(), r.chain.GasEstimator(), r.chain.Config().EVM().GasEstimator().PriceMax().ToInt(), + feeEstimatorConfig, ), nil } @@ -451,6 +469,7 @@ func (r *Relayer) NewCCIPCommitProvider(rargs commontypes.RelayArgs, pargs commo *r.chain.Config().EVM().GasEstimator().PriceMax().ToInt(), *contractTransmitter, configWatcher, + feeEstimatorConfig, ), nil } @@ -473,6 +492,20 @@ func (r *Relayer) NewCCIPExecProvider(rargs commontypes.RelayArgs, pargs commont usdcConfig := execPluginConfig.USDCConfig + feeEstimatorConfig := estimatorconfig.NewFeeEstimatorConfigService() + + // CCIPExec reads when dest chain is mantle, and uses it to calc boosting in batching + // to minimize misconfigure risk, make sense to wire Mantle only when Exec + Mantle + !IsSourceProvider + if r.chain.Config().EVM().ChainID().Uint64() == 5003 || r.chain.Config().EVM().ChainID().Uint64() == 5000 { + if !execPluginConfig.IsSourceProvider { + mantleInterceptor, iErr := mantle.NewInterceptor(ctx, r.chain.Client()) + if iErr != nil { + return nil, iErr + } + feeEstimatorConfig.AddGasPriceInterceptor(mantleInterceptor) + } + } + // The src chain implementation of this provider does not need a configWatcher or contractTransmitter; // bail early. if execPluginConfig.IsSourceProvider { @@ -489,6 +522,7 @@ func (r *Relayer) NewCCIPExecProvider(rargs commontypes.RelayArgs, pargs commont int(usdcConfig.AttestationAPITimeoutSeconds), usdcConfig.AttestationAPIIntervalMilliseconds, usdcConfig.SourceMessageTransmitterAddress, + feeEstimatorConfig, ) } @@ -524,6 +558,7 @@ func (r *Relayer) NewCCIPExecProvider(rargs commontypes.RelayArgs, pargs commont configWatcher, r.chain.GasEstimator(), *r.chain.Config().EVM().GasEstimator().PriceMax().ToInt(), + feeEstimatorConfig, r.chain.TxManager(), cciptypes.Address(rargs.ContractID), ) diff --git a/core/services/relay/evm/exec_provider.go b/core/services/relay/evm/exec_provider.go index d0e567af730..e50ae41351d 100644 --- a/core/services/relay/evm/exec_provider.go +++ b/core/services/relay/evm/exec_provider.go @@ -23,6 +23,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata/usdc" ) @@ -40,6 +41,8 @@ type SrcExecProvider struct { usdcAttestationAPIIntervalMilliseconds int usdcSrcMsgTransmitterAddr common.Address + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider + // these values are nil and are updated for Close() seenOnRampAddress *cciptypes.Address seenSourceChainSelector *uint64 @@ -59,6 +62,7 @@ func NewSrcExecProvider( usdcAttestationAPITimeoutSeconds int, usdcAttestationAPIIntervalMilliseconds int, usdcSrcMsgTransmitterAddr common.Address, + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider, ) (commontypes.CCIPExecProvider, error) { var usdcReader *ccip.USDCReaderImpl var err error @@ -82,6 +86,7 @@ func NewSrcExecProvider( usdcAttestationAPITimeoutSeconds: usdcAttestationAPITimeoutSeconds, usdcAttestationAPIIntervalMilliseconds: usdcAttestationAPIIntervalMilliseconds, usdcSrcMsgTransmitterAddr: usdcSrcMsgTransmitterAddr, + feeEstimatorConfig: feeEstimatorConfig, }, nil } @@ -163,7 +168,7 @@ func (s *SrcExecProvider) GetTransactionStatus(ctx context.Context, transactionI } func (s *SrcExecProvider) NewCommitStoreReader(ctx context.Context, addr cciptypes.Address) (commitStoreReader cciptypes.CommitStoreReader, err error) { - commitStoreReader = NewIncompleteSourceCommitStoreReader(s.estimator, s.maxGasPrice) + commitStoreReader = NewIncompleteSourceCommitStoreReader(s.estimator, s.maxGasPrice, s.feeEstimatorConfig) return } @@ -176,6 +181,10 @@ func (s *SrcExecProvider) NewOnRampReader(ctx context.Context, onRampAddress cci versionFinder := ccip.NewEvmVersionFinder() onRampReader, err = ccip.NewOnRampReader(s.lggr, versionFinder, sourceChainSelector, destChainSelector, onRampAddress, s.lp, s.client) + if err != nil { + return nil, err + } + s.feeEstimatorConfig.SetOnRampReader(onRampReader) return } @@ -236,6 +245,7 @@ type DstExecProvider struct { configWatcher *configWatcher gasEstimator gas.EvmFeeEstimator maxGasPrice big.Int + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider txm txmgr.TxManager offRampAddress cciptypes.Address @@ -253,6 +263,7 @@ func NewDstExecProvider( configWatcher *configWatcher, gasEstimator gas.EvmFeeEstimator, maxGasPrice big.Int, + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider, txm txmgr.TxManager, offRampAddress cciptypes.Address, ) (commontypes.CCIPExecProvider, error) { @@ -266,6 +277,7 @@ func NewDstExecProvider( configWatcher: configWatcher, gasEstimator: gasEstimator, maxGasPrice: maxGasPrice, + feeEstimatorConfig: feeEstimatorConfig, txm: txm, offRampAddress: offRampAddress, }, nil @@ -295,10 +307,10 @@ func (d *DstExecProvider) Close() error { if d.seenCommitStoreAddr == nil { return nil } - return ccip.CloseCommitStoreReader(d.lggr, versionFinder, *d.seenCommitStoreAddr, d.client, d.lp) + return ccip.CloseCommitStoreReader(d.lggr, versionFinder, *d.seenCommitStoreAddr, d.client, d.lp, d.feeEstimatorConfig) }) unregisterFuncs = append(unregisterFuncs, func() error { - return ccip.CloseOffRampReader(d.lggr, versionFinder, d.offRampAddress, d.client, d.lp, nil, big.NewInt(0)) + return ccip.CloseOffRampReader(d.lggr, versionFinder, d.offRampAddress, d.client, d.lp, nil, big.NewInt(0), d.feeEstimatorConfig) }) var multiErr error @@ -346,12 +358,12 @@ func (d *DstExecProvider) NewCommitStoreReader(ctx context.Context, addr cciptyp d.seenCommitStoreAddr = &addr versionFinder := ccip.NewEvmVersionFinder() - commitStoreReader, err = NewIncompleteDestCommitStoreReader(d.lggr, versionFinder, addr, d.client, d.lp) + commitStoreReader, err = NewIncompleteDestCommitStoreReader(d.lggr, versionFinder, addr, d.client, d.lp, d.feeEstimatorConfig) return } func (d *DstExecProvider) NewOffRampReader(ctx context.Context, offRampAddress cciptypes.Address) (offRampReader cciptypes.OffRampReader, err error) { - offRampReader, err = ccip.NewOffRampReader(d.lggr, d.versionFinder, offRampAddress, d.client, d.lp, d.gasEstimator, &d.maxGasPrice, true) + offRampReader, err = ccip.NewOffRampReader(d.lggr, d.versionFinder, offRampAddress, d.client, d.lp, d.gasEstimator, &d.maxGasPrice, true, d.feeEstimatorConfig) return } diff --git a/core/services/relay/evm/interceptors/mantle/interceptor.go b/core/services/relay/evm/interceptors/mantle/interceptor.go new file mode 100644 index 00000000000..8d5d3bb6c2f --- /dev/null +++ b/core/services/relay/evm/interceptors/mantle/interceptor.go @@ -0,0 +1,80 @@ +package mantle + +import ( + "context" + "fmt" + "math/big" + "strings" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + evmClient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" +) + +const ( + // tokenRatio is not volatile and can be requested not often. + tokenRatioUpdateInterval = 60 * time.Minute + // tokenRatio fetches the tokenRatio used for Mantle's gas price calculation + tokenRatioMethod = "tokenRatio" + mantleTokenRatioAbiString = `[{"inputs":[],"name":"tokenRatio","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]` +) + +type Interceptor struct { + client evmClient.Client + tokenRatioCallData []byte + tokenRatio *big.Int + tokenRatioLastUpdate time.Time +} + +func NewInterceptor(_ context.Context, client evmClient.Client) (*Interceptor, error) { + // Encode calldata for tokenRatio method + tokenRatioMethodAbi, err := abi.JSON(strings.NewReader(mantleTokenRatioAbiString)) + if err != nil { + return nil, fmt.Errorf("failed to parse GasPriceOracle %s() method ABI for Mantle; %v", tokenRatioMethod, err) + } + tokenRatioCallData, err := tokenRatioMethodAbi.Pack(tokenRatioMethod) + if err != nil { + return nil, fmt.Errorf("failed to parse GasPriceOracle %s() calldata for Mantle; %v", tokenRatioMethod, err) + } + + return &Interceptor{ + client: client, + tokenRatioCallData: tokenRatioCallData, + }, nil +} + +// ModifyGasPriceComponents returns modified gasPrice. +func (i *Interceptor) ModifyGasPriceComponents(ctx context.Context, execGasPrice, daGasPrice *big.Int) (*big.Int, *big.Int, error) { + if time.Since(i.tokenRatioLastUpdate) > tokenRatioUpdateInterval { + mantleTokenRatio, err := i.getMantleTokenRatio(ctx) + if err != nil { + return nil, nil, err + } + + i.tokenRatio, i.tokenRatioLastUpdate = mantleTokenRatio, time.Now() + } + + // multiply daGasPrice and execGas price by tokenRatio + newExecGasPrice := new(big.Int).Mul(execGasPrice, i.tokenRatio) + newDAGasPrice := new(big.Int).Mul(daGasPrice, i.tokenRatio) + return newExecGasPrice, newDAGasPrice, nil +} + +// getMantleTokenRatio Requests and returns a token ratio value for the Mantle chain. +func (i *Interceptor) getMantleTokenRatio(ctx context.Context) (*big.Int, error) { + precompile := common.HexToAddress(rollups.OPGasOracleAddress) + tokenRatio, err := i.client.CallContract(ctx, ethereum.CallMsg{ + To: &precompile, + Data: i.tokenRatioCallData, + }, nil) + + if err != nil { + return nil, fmt.Errorf("getMantleTokenRatio call failed: %w", err) + } + + return new(big.Int).SetBytes(tokenRatio), nil +} diff --git a/core/services/relay/evm/interceptors/mantle/interceptor_test.go b/core/services/relay/evm/interceptors/mantle/interceptor_test.go new file mode 100644 index 00000000000..9134d996c27 --- /dev/null +++ b/core/services/relay/evm/interceptors/mantle/interceptor_test.go @@ -0,0 +1,96 @@ +package mantle + +import ( + "context" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" +) + +func TestInterceptor(t *testing.T) { + ethClient := mocks.NewClient(t) + ctx := context.Background() + + tokenRatio := big.NewInt(10) + interceptor, err := NewInterceptor(ctx, ethClient) + require.NoError(t, err) + + // request token ratio + ethClient.On("CallContract", ctx, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})). + Return(common.BigToHash(tokenRatio).Bytes(), nil).Once() + + modExecGasPrice, modDAGasPrice, err := interceptor.ModifyGasPriceComponents(ctx, big.NewInt(1), big.NewInt(1)) + require.NoError(t, err) + require.Equal(t, int64(10), modExecGasPrice.Int64()) + require.Equal(t, int64(10), modDAGasPrice.Int64()) + + // second call won't invoke eth client + modExecGasPrice, modDAGasPrice, err = interceptor.ModifyGasPriceComponents(ctx, big.NewInt(2), big.NewInt(1)) + require.NoError(t, err) + require.Equal(t, int64(20), modExecGasPrice.Int64()) + require.Equal(t, int64(10), modDAGasPrice.Int64()) +} + +func TestModifyGasPriceComponents(t *testing.T) { + testCases := map[string]struct { + execGasPrice *big.Int + daGasPrice *big.Int + tokenRatio *big.Int + resultExecGasPrice *big.Int + resultDAGasPrice *big.Int + }{ + "regular": { + execGasPrice: big.NewInt(1000), + daGasPrice: big.NewInt(100), + resultExecGasPrice: big.NewInt(2000), + resultDAGasPrice: big.NewInt(200), + tokenRatio: big.NewInt(2), + }, + "zero DAGasPrice": { + execGasPrice: big.NewInt(1000), + daGasPrice: big.NewInt(0), + resultExecGasPrice: big.NewInt(5000), + resultDAGasPrice: big.NewInt(0), + tokenRatio: big.NewInt(5), + }, + "zero ExecGasPrice": { + execGasPrice: big.NewInt(0), + daGasPrice: big.NewInt(10), + resultExecGasPrice: big.NewInt(0), + resultDAGasPrice: big.NewInt(50), + tokenRatio: big.NewInt(5), + }, + "zero token ratio": { + execGasPrice: big.NewInt(15), + daGasPrice: big.NewInt(10), + resultExecGasPrice: big.NewInt(0), + resultDAGasPrice: big.NewInt(0), + tokenRatio: big.NewInt(0), + }, + } + + for tcName, tc := range testCases { + t.Run(tcName, func(t *testing.T) { + ethClient := mocks.NewClient(t) + ctx := context.Background() + + interceptor, err := NewInterceptor(ctx, ethClient) + require.NoError(t, err) + + // request token ratio + ethClient.On("CallContract", ctx, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})). + Return(common.BigToHash(tc.tokenRatio).Bytes(), nil).Once() + + modExecGasPrice, modDAGasPrice, err := interceptor.ModifyGasPriceComponents(ctx, tc.execGasPrice, tc.daGasPrice) + require.NoError(t, err) + require.Equal(t, tc.resultExecGasPrice.Int64(), modExecGasPrice.Int64()) + require.Equal(t, tc.resultDAGasPrice.Int64(), modDAGasPrice.Int64()) + }) + } +} diff --git a/core/web/evm_transfer_controller.go b/core/web/evm_transfer_controller.go index 88d3dead4c8..75f6c07b6d9 100644 --- a/core/web/evm_transfer_controller.go +++ b/core/web/evm_transfer_controller.go @@ -54,7 +54,7 @@ func (tc *EVMTransfersController) Create(c *gin.Context) { } if !tr.AllowHigherAmounts { - err = ValidateEthBalanceForTransfer(c, chain, tr.FromAddress, tr.Amount) + err = ValidateEthBalanceForTransfer(c, chain, tr.FromAddress, tr.Amount, tr.DestinationAddress) if err != nil { jsonAPIError(c, http.StatusUnprocessableEntity, errors.Errorf("transaction failed: %v", err)) return @@ -92,7 +92,7 @@ func (tc *EVMTransfersController) Create(c *gin.Context) { } // ValidateEthBalanceForTransfer validates that the current balance can cover the transaction amount -func ValidateEthBalanceForTransfer(c *gin.Context, chain legacyevm.Chain, fromAddr common.Address, amount assets.Eth) error { +func ValidateEthBalanceForTransfer(c *gin.Context, chain legacyevm.Chain, fromAddr common.Address, amount assets.Eth, toAddr common.Address) error { var err error var balance *big.Int @@ -116,7 +116,7 @@ func ValidateEthBalanceForTransfer(c *gin.Context, chain legacyevm.Chain, fromAd gasLimit := chain.Config().EVM().GasEstimator().LimitTransfer() estimator := chain.GasEstimator() - amountWithFees, err := estimator.GetMaxCost(c, amount, nil, gasLimit, chain.Config().EVM().GasEstimator().PriceMaxKey(fromAddr)) + amountWithFees, err := estimator.GetMaxCost(c, amount, nil, gasLimit, chain.Config().EVM().GasEstimator().PriceMaxKey(fromAddr), &fromAddr, &toAddr) if err != nil { return err } diff --git a/core/web/resolver/testdata/config-full.toml b/core/web/resolver/testdata/config-full.toml index c5547f8698b..b5460427568 100644 --- a/core/web/resolver/testdata/config-full.toml +++ b/core/web/resolver/testdata/config-full.toml @@ -316,6 +316,7 @@ LimitDefault = 12 LimitMax = 17 LimitMultiplier = '1.234' LimitTransfer = 100 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 10 BumpThreshold = 6 @@ -340,6 +341,9 @@ CheckInclusionPercentile = 19 EIP1559FeeCapBufferBlocks = 13 TransactionPercentile = 15 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '1s' + [EVM.HeadTracker] HistoryDepth = 15 MaxBufferSize = 17 diff --git a/core/web/resolver/testdata/config-multi-chain-effective.toml b/core/web/resolver/testdata/config-multi-chain-effective.toml index 8c063d0e9e7..ca9edca06fd 100644 --- a/core/web/resolver/testdata/config-multi-chain-effective.toml +++ b/core/web/resolver/testdata/config-multi-chain-effective.toml @@ -303,6 +303,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -318,6 +319,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -403,6 +407,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -418,6 +423,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -497,6 +505,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -512,6 +521,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 diff --git a/docs/CONFIG.md b/docs/CONFIG.md index e18abcf811a..f27f092e4e5 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -1818,6 +1818,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -1833,6 +1834,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -1912,6 +1916,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -1927,6 +1932,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2006,6 +2014,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2021,6 +2030,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2100,6 +2112,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2115,6 +2128,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2195,6 +2211,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -2210,6 +2227,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -2289,6 +2309,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2304,6 +2325,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2383,6 +2407,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2398,6 +2423,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2478,6 +2506,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2493,6 +2522,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2572,6 +2604,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -2587,6 +2620,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2665,6 +2701,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2680,6 +2717,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2851,6 +2891,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2866,6 +2907,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2945,6 +2989,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -2960,6 +3005,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -3040,6 +3088,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -3055,6 +3104,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -3134,6 +3186,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -3149,6 +3202,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -3228,6 +3284,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -3243,6 +3300,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -3322,6 +3382,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 mwei' BumpPercent = 40 BumpThreshold = 3 @@ -3337,6 +3398,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -3416,6 +3480,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 mwei' BumpPercent = 40 BumpThreshold = 3 @@ -3431,6 +3496,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -3510,6 +3578,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -3525,6 +3594,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -3604,6 +3676,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -3619,6 +3692,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 400 MaxBufferSize = 3 @@ -3698,6 +3774,7 @@ LimitDefault = 100000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -3713,6 +3790,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 50 MaxBufferSize = 3 @@ -3792,6 +3872,7 @@ LimitDefault = 2500000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -3807,6 +3888,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 250 MaxBufferSize = 3 @@ -3886,6 +3970,7 @@ LimitDefault = 2500000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -3901,6 +3986,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 1500 MaxBufferSize = 3 @@ -3981,6 +4069,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -3996,6 +4085,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -4075,6 +4167,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -4090,6 +4183,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -4129,6 +4225,7 @@ GasLimit = 5400000 AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false +ChainType = 'astar' FinalityDepth = 100 FinalityTagEnabled = false LogBackfillBatchSize = 1000 @@ -4356,6 +4453,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -4371,6 +4469,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -4410,7 +4511,7 @@ GasLimit = 5400000 AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false -ChainType = 'metis' +ChainType = 'optimismBedrock' FinalityDepth = 10 FinalityTagEnabled = true LogBackfillBatchSize = 1000 @@ -4450,6 +4551,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -4465,6 +4567,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -4536,15 +4641,16 @@ Enabled = false Enabled = true [GasEstimator] -Mode = 'BlockHistory' +Mode = 'FeeHistory' PriceDefault = '20 gwei' PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' -PriceMin = '100 mwei' +PriceMin = '0' LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 -BumpMin = '100 mwei' +EstimateLimit = false +BumpMin = '5 gwei' BumpPercent = 40 BumpThreshold = 3 EIP1559DynamicFees = false @@ -4554,11 +4660,14 @@ TipCapMin = '1 wei' [GasEstimator.BlockHistory] BatchSize = 25 -BlockHistorySize = 12 +BlockHistorySize = 8 CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '4s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -4638,6 +4747,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -4653,6 +4763,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -4732,6 +4845,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -4747,6 +4861,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -4825,6 +4942,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 0 @@ -4840,6 +4958,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 10 MaxBufferSize = 100 @@ -4919,6 +5040,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 mwei' BumpPercent = 40 BumpThreshold = 3 @@ -4934,6 +5056,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -5013,6 +5138,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -5028,6 +5154,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 400 MaxBufferSize = 3 @@ -5099,15 +5228,16 @@ Enabled = false Enabled = true [GasEstimator] -Mode = 'BlockHistory' +Mode = 'FeeHistory' PriceDefault = '20 gwei' PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' -PriceMin = '1 mwei' +PriceMin = '0' LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 -BumpMin = '20 mwei' +EstimateLimit = false +BumpMin = '5 gwei' BumpPercent = 40 BumpThreshold = 3 EIP1559DynamicFees = false @@ -5117,11 +5247,14 @@ TipCapMin = '1 wei' [GasEstimator.BlockHistory] BatchSize = 25 -BlockHistorySize = 12 +BlockHistorySize = 8 CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '4s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -5201,6 +5334,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -5216,6 +5350,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -5389,6 +5526,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -5404,6 +5542,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -5483,6 +5624,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -5498,6 +5640,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -5577,6 +5722,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -5592,6 +5738,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -5672,6 +5821,7 @@ LimitDefault = 8000000 LimitMax = 1000000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -5687,6 +5837,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -5767,6 +5920,7 @@ LimitDefault = 8000000 LimitMax = 1000000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -5782,6 +5936,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -5957,6 +6114,7 @@ LimitDefault = 8000000 LimitMax = 1000000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -5972,6 +6130,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -6051,6 +6212,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '2 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -6066,6 +6228,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 50 MaxBufferSize = 3 @@ -6145,6 +6310,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -6160,6 +6326,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -6239,6 +6408,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -6254,6 +6424,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -6333,6 +6506,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '2 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -6348,6 +6522,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 50 MaxBufferSize = 3 @@ -6381,6 +6558,218 @@ GasLimit = 5400000

+
Zircuit Sepolia (48899)

+ +```toml +AutoCreateKey = true +BlockBackfillDepth = 10 +BlockBackfillSkip = false +ChainType = 'optimismBedrock' +FinalityDepth = 1000 +FinalityTagEnabled = true +LinkContractAddress = '0xDEE94506570cA186BC1e3516fCf4fd719C312cCD' +LogBackfillBatchSize = 1000 +LogPollInterval = '2s' +LogKeepBlocksDepth = 100000 +LogPrunePageSize = 0 +BackupLogPollerBlockDelay = 100 +MinIncomingConfirmations = 3 +MinContractPayment = '0.00001 link' +NonceAutoSync = true +NoNewHeadsThreshold = '40s' +LogBroadcasterEnabled = true +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 +FinalizedBlockOffset = 0 +NoNewFinalizedHeadsThreshold = '15m0s' + +[Transactions] +ForwardersEnabled = false +MaxInFlight = 16 +MaxQueued = 250 +ReaperInterval = '1h0m0s' +ReaperThreshold = '168h0m0s' +ResendAfterThreshold = '30s' + +[Transactions.AutoPurge] +Enabled = true +Threshold = 90 +MinAttempts = 3 + +[BalanceMonitor] +Enabled = true + +[GasEstimator] +Mode = 'BlockHistory' +PriceDefault = '20 gwei' +PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' +PriceMin = '1 wei' +LimitDefault = 500000 +LimitMax = 500000 +LimitMultiplier = '1' +LimitTransfer = 21000 +EstimateLimit = false +BumpMin = '100 wei' +BumpPercent = 20 +BumpThreshold = 3 +EIP1559DynamicFees = true +FeeCapDefault = '100 gwei' +TipCapDefault = '1 wei' +TipCapMin = '1 wei' + +[GasEstimator.BlockHistory] +BatchSize = 25 +BlockHistorySize = 60 +CheckInclusionBlocks = 12 +CheckInclusionPercentile = 90 +TransactionPercentile = 60 + +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + +[HeadTracker] +HistoryDepth = 2000 +MaxBufferSize = 3 +SamplingInterval = '1s' +MaxAllowedFinalityDepth = 10000 +FinalityTagBypass = true + +[NodePool] +PollFailureThreshold = 5 +PollInterval = '10s' +SelectionMode = 'HighestHead' +SyncThreshold = 10 +LeaseDuration = '0s' +NodeIsSyncingEnabled = false +FinalizedBlockPollInterval = '5s' +EnforceRepeatableRead = false +DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' + +[OCR] +ContractConfirmations = 1 +ContractTransmitterTransmitTimeout = '10s' +DatabaseTimeout = '10s' +DeltaCOverride = '168h0m0s' +DeltaCJitterOverride = '1h0m0s' +ObservationGracePeriod = '1s' + +[OCR2] +[OCR2.Automation] +GasLimit = 6500000 + +[Workflow] +GasLimitDefault = 400000 +``` + +

+ +
Zircuit Mainnet (48900)

+ +```toml +AutoCreateKey = true +BlockBackfillDepth = 10 +BlockBackfillSkip = false +ChainType = 'optimismBedrock' +FinalityDepth = 1000 +FinalityTagEnabled = true +LinkContractAddress = '0x5D6d033B4FbD2190D99D930719fAbAcB64d2439a' +LogBackfillBatchSize = 1000 +LogPollInterval = '2s' +LogKeepBlocksDepth = 100000 +LogPrunePageSize = 0 +BackupLogPollerBlockDelay = 100 +MinIncomingConfirmations = 3 +MinContractPayment = '0.00001 link' +NonceAutoSync = true +NoNewHeadsThreshold = '40s' +LogBroadcasterEnabled = true +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 +FinalizedBlockOffset = 0 +NoNewFinalizedHeadsThreshold = '15m0s' + +[Transactions] +ForwardersEnabled = false +MaxInFlight = 16 +MaxQueued = 250 +ReaperInterval = '1h0m0s' +ReaperThreshold = '168h0m0s' +ResendAfterThreshold = '30s' + +[Transactions.AutoPurge] +Enabled = true +Threshold = 90 +MinAttempts = 3 + +[BalanceMonitor] +Enabled = true + +[GasEstimator] +Mode = 'BlockHistory' +PriceDefault = '20 gwei' +PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' +PriceMin = '1 wei' +LimitDefault = 500000 +LimitMax = 500000 +LimitMultiplier = '1' +LimitTransfer = 21000 +EstimateLimit = false +BumpMin = '100 wei' +BumpPercent = 20 +BumpThreshold = 3 +EIP1559DynamicFees = true +FeeCapDefault = '100 gwei' +TipCapDefault = '1 wei' +TipCapMin = '1 wei' + +[GasEstimator.BlockHistory] +BatchSize = 25 +BlockHistorySize = 24 +CheckInclusionBlocks = 12 +CheckInclusionPercentile = 90 +TransactionPercentile = 60 + +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + +[HeadTracker] +HistoryDepth = 2000 +MaxBufferSize = 3 +SamplingInterval = '1s' +MaxAllowedFinalityDepth = 10000 +FinalityTagBypass = true + +[NodePool] +PollFailureThreshold = 5 +PollInterval = '10s' +SelectionMode = 'HighestHead' +SyncThreshold = 10 +LeaseDuration = '0s' +NodeIsSyncingEnabled = false +FinalizedBlockPollInterval = '5s' +EnforceRepeatableRead = false +DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' + +[OCR] +ContractConfirmations = 1 +ContractTransmitterTransmitTimeout = '10s' +DatabaseTimeout = '10s' +DeltaCOverride = '168h0m0s' +DeltaCJitterOverride = '1h0m0s' +ObservationGracePeriod = '1s' + +[OCR2] +[OCR2.Automation] +GasLimit = 6500000 + +[Workflow] +GasLimitDefault = 400000 +``` + +

+
Linea Goerli (59140)

```toml @@ -6426,6 +6815,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 40 BumpThreshold = 3 @@ -6441,6 +6831,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -6519,6 +6912,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -6534,6 +6928,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 1000 MaxBufferSize = 3 @@ -6612,6 +7009,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 40 BumpThreshold = 3 @@ -6627,6 +7025,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 350 MaxBufferSize = 3 @@ -6666,7 +7067,7 @@ GasLimit = 5400000 AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false -ChainType = 'metis' +ChainType = 'optimismBedrock' FinalityDepth = 10 FinalityTagEnabled = true LogBackfillBatchSize = 1000 @@ -6706,6 +7107,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -6721,6 +7123,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -6894,6 +7299,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -6909,6 +7315,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -6987,6 +7396,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -7002,6 +7412,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -7176,6 +7589,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -7191,6 +7605,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -7271,6 +7688,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -7286,6 +7704,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -7366,6 +7787,7 @@ LimitDefault = 8000000 LimitMax = 1000000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -7381,6 +7803,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -7461,6 +7886,7 @@ LimitDefault = 8000000 LimitMax = 1000000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -7476,6 +7902,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -7556,6 +7985,7 @@ LimitDefault = 8000000 LimitMax = 1000000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -7571,6 +8001,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -7650,6 +8083,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '1 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -7665,6 +8099,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 50 MaxBufferSize = 3 @@ -7744,6 +8181,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '1 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -7759,6 +8197,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 50 MaxBufferSize = 3 @@ -7838,6 +8279,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -7853,6 +8295,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -7932,6 +8377,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -7947,6 +8393,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -8121,6 +8570,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -8136,6 +8586,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -8215,6 +8668,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -8230,6 +8684,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -8577,6 +9034,7 @@ LimitDefault = 8_000_000 # Default LimitMax = 8_000_000 # Default LimitMultiplier = '1.0' # Default LimitTransfer = 21_000 # Default +EstimateLimit = false # Default BumpMin = '5 gwei' # Default BumpPercent = 20 # Default BumpThreshold = 3 # Default @@ -8671,6 +9129,12 @@ LimitTransfer = 21_000 # Default ``` LimitTransfer is the gas limit used for an ordinary ETH transfer. +### EstimateLimit +```toml +EstimateLimit = false # Default +``` +EstimateLimit enables estimating gas limits for transactions. This feature respects the gas limit provided during transaction creation as an upper bound. + ### BumpMin ```toml BumpMin = '5 gwei' # Default @@ -8884,6 +9348,24 @@ Setting this number higher will cause the Chainlink node to select higher gas pr Setting it lower will tend to set lower gas prices. +## EVM.GasEstimator.FeeHistory +```toml +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' # Default +``` + + +### CacheTimeout +```toml +CacheTimeout = '10s' # Default +``` +CacheTimeout is the time to wait in order to refresh the cached values stored in the FeeHistory estimator. A small jitter is applied so the timeout won't be exactly the same each time. + +You want this value to be close to the block time. For slower chains, like Ethereum, you can set it to 12s, the same as the block time. For faster chains you can skip a block or two +and set it to two times the block time i.e. on Optimism you can set it to 4s. Ideally, you don't want to go lower than 1s since the RTT times of the RPC requests will be comparable to +the timeout. The estimator is already adding a buffer to account for a potential increase in prices within one or two blocks. On the other hand, slower frequency will fail to refresh +the prices and end up in stale values. + ## EVM.HeadTracker ```toml [EVM.HeadTracker] diff --git a/go.mod b/go.mod index 683793cb534..af1174beab0 100644 --- a/go.mod +++ b/go.mod @@ -72,7 +72,7 @@ require ( github.com/scylladb/go-reflectx v1.0.1 github.com/shirou/gopsutil/v3 v3.24.3 github.com/shopspring/decimal v1.4.0 - github.com/smartcontractkit/chain-selectors v1.0.21 + github.com/smartcontractkit/chain-selectors v1.0.27 github.com/smartcontractkit/chainlink-automation v1.0.4 github.com/smartcontractkit/chainlink-ccip v0.0.0-20240816163757-48726fd8165f github.com/smartcontractkit/chainlink-common v0.2.1-0.20240717132349-ee5af9b79834 diff --git a/go.sum b/go.sum index 5ad6772ecd4..b81f4930a06 100644 --- a/go.sum +++ b/go.sum @@ -1023,8 +1023,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartcontractkit/chain-selectors v1.0.21 h1:KCR9SA7PhOexaBzFieHoLv1WonwhVOPtOStpqTmLC4E= -github.com/smartcontractkit/chain-selectors v1.0.21/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= +github.com/smartcontractkit/chain-selectors v1.0.27 h1:VE/ftX9Aae4gnw67yR1raKi+30iWKL/sWq8uyiLHM8k= +github.com/smartcontractkit/chain-selectors v1.0.27/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8umfIfVVlwC7+n5izbLSFgjw8= github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= github.com/smartcontractkit/chainlink-ccip v0.0.0-20240816163757-48726fd8165f h1:lQZBOjeYFpCdk0mGQUhbrJipd00tu49xK4zSijC/9Co= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 7abf19dc826..c26b3d27e99 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -34,7 +34,7 @@ require ( github.com/shopspring/decimal v1.4.0 github.com/slack-go/slack v0.12.2 github.com/smartcontractkit/ccip-owner-contracts v0.0.0-20240808195812-ae0378684685 - github.com/smartcontractkit/chain-selectors v1.0.21 + github.com/smartcontractkit/chain-selectors v1.0.27 github.com/smartcontractkit/chainlink-automation v1.0.4 github.com/smartcontractkit/chainlink-ccip v0.0.0-20240816163757-48726fd8165f github.com/smartcontractkit/chainlink-common v0.2.1-0.20240717132349-ee5af9b79834 diff --git a/integration-tests/go.sum b/integration-tests/go.sum index babc3fc8c6b..381abac5be7 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1390,8 +1390,8 @@ github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/smartcontractkit/ccip-owner-contracts v0.0.0-20240808195812-ae0378684685 h1:jakAsdhDxV4cMgRAcSvHraXjyePi8umG5SEUTGFvuy8= github.com/smartcontractkit/ccip-owner-contracts v0.0.0-20240808195812-ae0378684685/go.mod h1:p7L/xNEQpHDdZtgFA6/FavuZHqvV3kYhQysxBywmq1k= -github.com/smartcontractkit/chain-selectors v1.0.21 h1:KCR9SA7PhOexaBzFieHoLv1WonwhVOPtOStpqTmLC4E= -github.com/smartcontractkit/chain-selectors v1.0.21/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= +github.com/smartcontractkit/chain-selectors v1.0.27 h1:VE/ftX9Aae4gnw67yR1raKi+30iWKL/sWq8uyiLHM8k= +github.com/smartcontractkit/chain-selectors v1.0.27/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8umfIfVVlwC7+n5izbLSFgjw8= github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= github.com/smartcontractkit/chainlink-ccip v0.0.0-20240816163757-48726fd8165f h1:lQZBOjeYFpCdk0mGQUhbrJipd00tu49xK4zSijC/9Co= diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod index a90826f599a..d086677434f 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -374,7 +374,7 @@ require ( github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/smartcontractkit/chain-selectors v1.0.21 // indirect + github.com/smartcontractkit/chain-selectors v1.0.27 // indirect github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240718160222-2dc0c8136bfa // indirect github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827 // indirect github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240712132946-267a37c5ac6e // indirect diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index 536b96f8b60..b181bfbaae1 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -1354,8 +1354,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ= github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= -github.com/smartcontractkit/chain-selectors v1.0.21 h1:KCR9SA7PhOexaBzFieHoLv1WonwhVOPtOStpqTmLC4E= -github.com/smartcontractkit/chain-selectors v1.0.21/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= +github.com/smartcontractkit/chain-selectors v1.0.27 h1:VE/ftX9Aae4gnw67yR1raKi+30iWKL/sWq8uyiLHM8k= +github.com/smartcontractkit/chain-selectors v1.0.27/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8umfIfVVlwC7+n5izbLSFgjw8= github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= github.com/smartcontractkit/chainlink-ccip v0.0.0-20240816163757-48726fd8165f h1:lQZBOjeYFpCdk0mGQUhbrJipd00tu49xK4zSijC/9Co= diff --git a/package.json b/package.json index 541cfb1aaf2..907a5bb311d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ccip", - "version": "2.14.0-ccip1.5.0", + "version": "2.14.0-ccip1.5.5", "description": "node of the decentralized oracle network, bridging on and off-chain computation", "main": "index.js", "scripts": { diff --git a/testdata/scripts/node/validate/disk-based-logging-disabled.txtar b/testdata/scripts/node/validate/disk-based-logging-disabled.txtar index fe522ca1bb1..a6612cc8f16 100644 --- a/testdata/scripts/node/validate/disk-based-logging-disabled.txtar +++ b/testdata/scripts/node/validate/disk-based-logging-disabled.txtar @@ -359,6 +359,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -374,6 +375,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 diff --git a/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar b/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar index 5a5ce42d6b5..79dcfa776d9 100644 --- a/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar +++ b/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar @@ -359,6 +359,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -374,6 +375,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 diff --git a/testdata/scripts/node/validate/disk-based-logging.txtar b/testdata/scripts/node/validate/disk-based-logging.txtar index 12773dc99cb..efa27eec118 100644 --- a/testdata/scripts/node/validate/disk-based-logging.txtar +++ b/testdata/scripts/node/validate/disk-based-logging.txtar @@ -359,6 +359,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -374,6 +375,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 diff --git a/testdata/scripts/node/validate/invalid.txtar b/testdata/scripts/node/validate/invalid.txtar index e709b24d5b0..6932eb50385 100644 --- a/testdata/scripts/node/validate/invalid.txtar +++ b/testdata/scripts/node/validate/invalid.txtar @@ -349,6 +349,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -364,6 +365,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 diff --git a/testdata/scripts/node/validate/valid.txtar b/testdata/scripts/node/validate/valid.txtar index 9b22da4850d..7074c82bf51 100644 --- a/testdata/scripts/node/validate/valid.txtar +++ b/testdata/scripts/node/validate/valid.txtar @@ -356,6 +356,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -371,6 +372,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3