From bd87220d782729d43c951363f02f4e553a1d6659 Mon Sep 17 00:00:00 2001 From: Ryan Hall Date: Mon, 7 Oct 2024 15:27:55 -0400 Subject: [PATCH] make gas fee staleness threshold configurable per chain --- contracts/src/v0.8/ccip/FeeQuoter.sol | 42 ++++++++++++++----- .../v0.8/ccip/test/feeQuoter/FeeQuoter.t.sol | 13 +++--- .../ccip/test/feeQuoter/FeeQuoterSetup.t.sol | 1 + 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/contracts/src/v0.8/ccip/FeeQuoter.sol b/contracts/src/v0.8/ccip/FeeQuoter.sol index d2a6b77eff2..0890169389a 100644 --- a/contracts/src/v0.8/ccip/FeeQuoter.sol +++ b/contracts/src/v0.8/ccip/FeeQuoter.sol @@ -76,7 +76,8 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver, struct StaticConfig { uint96 maxFeeJuelsPerMsg; // ─╮ Maximum fee that can be charged for a message address linkToken; // ────────╯ LINK token address - uint32 stalenessThreshold; // The amount of time a gas price can be stale before it is considered invalid. + // The amount of time a token price can be stale before it is considered invalid (gas price staleness is configured per dest chain) + uint32 stalenessThreshold; } /// @dev The struct representing the received CCIP feed report from keystone IReceiver.onReport() @@ -103,6 +104,7 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver, uint32 defaultTxGasLimit; //─────────────────╮ Default gas limit for a tx uint64 gasMultiplierWeiPerEth; // │ Multiplier for gas costs, 1e18 based so 11e17 = 10% extra cost. uint32 networkFeeUSDCents; // │ Flat network fee to charge for messages, multiples of 0.01 USD + uint32 gasPriceStalenessThreshold; // │ The amount of time a gas price can be stale before it is considered invalid (0 means disabled) bool enforceOutOfOrder; // │ Whether to enforce the allowOutOfOrderExecution extraArg value to be true. bytes4 chainFamilySelector; // ──────────────╯ Selector that identifies the destination chain's family. Used to determine the correct validations to perform for the dest chain. } @@ -202,7 +204,7 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver, /// @dev Subset of tokens which prices tracked by this registry which are fee tokens. EnumerableSet.AddressSet private s_feeTokens; - /// @dev The amount of time a gas price can be stale before it is considered invalid. + /// @dev The amount of time a token price can be stale before it is considered invalid. uint32 private immutable i_stalenessThreshold; constructor( @@ -306,13 +308,7 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver, address token, uint64 destChainSelector ) public view returns (uint224 tokenPrice, uint224 gasPriceValue) { - Internal.TimestampedPackedUint224 memory gasPrice = s_usdPerUnitGasByDestChainSelector[destChainSelector]; - // We do allow a gas price of 0, but no stale or unset gas prices - if (gasPrice.timestamp == 0) revert ChainNotSupported(destChainSelector); - uint256 timePassed = block.timestamp - gasPrice.timestamp; - if (timePassed > i_stalenessThreshold) revert StaleGasPrice(destChainSelector, i_stalenessThreshold, timePassed); - - return (_getValidatedTokenPrice(token), gasPrice.value); + return _getTokenAndGasPrices(token, destChainSelector, s_destChainConfigs[destChainSelector]); } /// @notice Convert a given token amount to target token amount. @@ -374,6 +370,31 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver, return Internal.TimestampedPackedUint224({value: rebasedValue, timestamp: uint32(block.timestamp)}); } + /// @notice Gets the fee token price and the gas price, both denominated in dollars. + /// @param token The source token to get the price for. + /// @param destChainSelector The destination chain to get the gas price for. + /// @param destChainConfig The config struct for the dest chain selector. + /// @return tokenPrice The price of the feeToken in 1e18 dollars per base unit. + /// @return gasPriceValue The price of gas in 1e18 dollars per base unit. + function _getTokenAndGasPrices( + address token, + uint64 destChainSelector, + DestChainConfig memory destChainConfig + ) private view returns (uint224 tokenPrice, uint224 gasPriceValue) { + if (!destChainConfig.isEnabled) revert ChainNotSupported(destChainSelector); + Internal.TimestampedPackedUint224 memory gasPrice = s_usdPerUnitGasByDestChainSelector[destChainSelector]; + // If the staleness threshold is 0, we consider the gas price to be always valid + if (destChainConfig.gasPriceStalenessThreshold != 0) { + // We do allow a gas price of 0, but no stale or unset gas prices + uint256 timePassed = block.timestamp - gasPrice.timestamp; + if (timePassed > destChainConfig.gasPriceStalenessThreshold) { + revert StaleGasPrice(destChainSelector, destChainConfig.gasPriceStalenessThreshold, timePassed); + } + } + + return (_getValidatedTokenPrice(token), gasPrice.value); + } + // ================================================================ // │ Fee tokens │ // ================================================================ @@ -507,7 +528,8 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver, _validateMessage(destChainConfig, message.data.length, numberOfTokens, message.receiver); // The below call asserts that feeToken is a supported token - (uint224 feeTokenPrice, uint224 packedGasPrice) = getTokenAndGasPrices(message.feeToken, destChainSelector); + (uint224 feeTokenPrice, uint224 packedGasPrice) = + _getTokenAndGasPrices(message.feeToken, destChainSelector, destChainConfig); // Calculate premiumFee in USD with 18 decimals precision first. // If message-only and no token transfers, a flat network fee is charged. diff --git a/contracts/src/v0.8/ccip/test/feeQuoter/FeeQuoter.t.sol b/contracts/src/v0.8/ccip/test/feeQuoter/FeeQuoter.t.sol index 41720898dd2..c1e652aeece 100644 --- a/contracts/src/v0.8/ccip/test/feeQuoter/FeeQuoter.t.sol +++ b/contracts/src/v0.8/ccip/test/feeQuoter/FeeQuoter.t.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.24; -import {IFeeQuoter} from "../../interfaces/IFeeQuoter.sol"; - import {KeystoneFeedsPermissionHandler} from "../../../keystone/KeystoneFeedsPermissionHandler.sol"; import {AuthorizedCallers} from "../../../shared/access/AuthorizedCallers.sol"; import {MockV3Aggregator} from "../../../tests/MockV3Aggregator.sol"; @@ -598,16 +596,15 @@ contract FeeQuoter_getTokenAndGasPrices is FeeQuoterSetup { function test_ZeroGasPrice_Success() public { uint64 zeroGasDestChainSelector = 345678; - Internal.GasPriceUpdate[] memory gasPriceUpdates = new Internal.GasPriceUpdate[](1); - gasPriceUpdates[0] = Internal.GasPriceUpdate({destChainSelector: zeroGasDestChainSelector, usdPerUnitGas: 0}); + FeeQuoter.DestChainConfigArgs[] memory destChainConfigArgs = _generateFeeQuoterDestChainConfigArgs(); + destChainConfigArgs[0].destChainSelector = zeroGasDestChainSelector; + destChainConfigArgs[0].destChainConfig.gasPriceStalenessThreshold = 0; - Internal.PriceUpdates memory priceUpdates = - Internal.PriceUpdates({tokenPriceUpdates: new Internal.TokenPriceUpdate[](0), gasPriceUpdates: gasPriceUpdates}); - s_feeQuoter.updatePrices(priceUpdates); + s_feeQuoter.applyDestChainConfigUpdates(destChainConfigArgs); (, uint224 gasPrice) = s_feeQuoter.getTokenAndGasPrices(s_sourceFeeToken, zeroGasDestChainSelector); - assertEq(gasPrice, priceUpdates.gasPriceUpdates[0].usdPerUnitGas); + assertEq(gasPrice, 0); } function test_UnsupportedChain_Revert() public { diff --git a/contracts/src/v0.8/ccip/test/feeQuoter/FeeQuoterSetup.t.sol b/contracts/src/v0.8/ccip/test/feeQuoter/FeeQuoterSetup.t.sol index d630c55c0d7..712682f58b2 100644 --- a/contracts/src/v0.8/ccip/test/feeQuoter/FeeQuoterSetup.t.sol +++ b/contracts/src/v0.8/ccip/test/feeQuoter/FeeQuoterSetup.t.sol @@ -254,6 +254,7 @@ contract FeeQuoterSetup is TokenSetup { defaultTxGasLimit: GAS_LIMIT, gasMultiplierWeiPerEth: 5e17, networkFeeUSDCents: 1_00, + gasPriceStalenessThreshold: uint32(TWELVE_HOURS), enforceOutOfOrder: false, chainFamilySelector: Internal.CHAIN_FAMILY_SELECTOR_EVM })