Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CCIP-3414: Make gas fee staleness threshold configurable per chain (#1491) #14734

Merged
merged 10 commits into from
Oct 16, 2024
5 changes: 5 additions & 0 deletions .changeset/rude-spies-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": patch
---

#added new config field to FeeQuoter
8 changes: 8 additions & 0 deletions contracts/.changeset/tricky-cups-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@chainlink/contracts': patch
---

Make stalenessThreshold per dest chain and have 0 mean no staleness check.


PR issue: CCIP-3414
211 changes: 106 additions & 105 deletions contracts/gas-snapshots/ccip.gas-snapshot

Large diffs are not rendered by default.

55 changes: 38 additions & 17 deletions contracts/src/v0.8/ccip/FeeQuoter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,

error TokenNotSupported(address token);
error FeeTokenNotSupported(address token);
error ChainNotSupported(uint64 chain);
error StaleGasPrice(uint64 destChainSelector, uint256 threshold, uint256 timePassed);
error StaleKeystoneUpdate(address token, uint256 feedTimestamp, uint256 storedTimeStamp);
error DataFeedValueOutOfUint224Range();
Expand Down Expand Up @@ -76,7 +75,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 tokenPriceStalenessThreshold;
}

/// @dev The struct representing the received CCIP feed report from keystone IReceiver.onReport()
Expand All @@ -103,6 +103,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.
}
Expand Down Expand Up @@ -202,8 +203,8 @@ 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.
uint32 private immutable i_stalenessThreshold;
/// @dev The amount of time a token price can be stale before it is considered invalid.
uint32 private immutable i_tokenPriceStalenessThreshold;

constructor(
StaticConfig memory staticConfig,
Expand All @@ -216,14 +217,14 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
) AuthorizedCallers(priceUpdaters) {
if (
staticConfig.linkToken == address(0) || staticConfig.maxFeeJuelsPerMsg == 0
|| staticConfig.stalenessThreshold == 0
|| staticConfig.tokenPriceStalenessThreshold == 0
) {
revert InvalidStaticConfig();
}

i_linkToken = staticConfig.linkToken;
i_maxFeeJuelsPerMsg = staticConfig.maxFeeJuelsPerMsg;
i_stalenessThreshold = staticConfig.stalenessThreshold;
i_tokenPriceStalenessThreshold = staticConfig.tokenPriceStalenessThreshold;

_applyFeeTokensUpdates(feeTokens, new address[](0));
_updateTokenPriceFeeds(tokenPriceFeeds);
Expand All @@ -243,7 +244,7 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
Internal.TimestampedPackedUint224 memory tokenPrice = s_usdPerToken[token];

// If the token price is not stale, return it
if (block.timestamp - tokenPrice.timestamp < i_stalenessThreshold) {
if (block.timestamp - tokenPrice.timestamp < i_tokenPriceStalenessThreshold) {
return tokenPrice;
}

Expand Down Expand Up @@ -313,14 +314,12 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
function getTokenAndGasPrices(
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);
) external view returns (uint224 tokenPrice, uint224 gasPriceValue) {
if (!s_destChainConfigs[destChainSelector].isEnabled) revert DestinationChainNotEnabled(destChainSelector);
return (
_getValidatedTokenPrice(token),
_getValidatedGasPrice(destChainSelector, s_destChainConfigs[destChainSelector].gasPriceStalenessThreshold)
);
}

/// @notice Convert a given token amount to target token amount.
Expand Down Expand Up @@ -384,6 +383,27 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
return Internal.TimestampedPackedUint224({value: rebasedValue, timestamp: uint32(block.timestamp)});
}

/// @dev Gets the fee token price and the gas price, both denominated in dollars.
/// @param destChainSelector The destination chain to get the gas price for.
/// @param gasPriceStalenessThreshold The amount of time a gas price can be stale before it is considered invalid.
/// @return gasPriceValue The price of gas in 1e18 dollars per base unit.
function _getValidatedGasPrice(
uint64 destChainSelector,
uint32 gasPriceStalenessThreshold
) private view returns (uint224 gasPriceValue) {
Internal.TimestampedPackedUint224 memory gasPrice = s_usdPerUnitGasByDestChainSelector[destChainSelector];
// If the staleness threshold is 0, we consider the gas price to be always valid
if (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 > gasPriceStalenessThreshold) {
revert StaleGasPrice(destChainSelector, gasPriceStalenessThreshold, timePassed);
}
}

return gasPrice.value;
}

// ================================================================
// │ Fee tokens │
// ================================================================
Expand Down Expand Up @@ -523,7 +543,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 = _getValidatedTokenPrice(message.feeToken);
uint224 packedGasPrice = _getValidatedGasPrice(destChainSelector, destChainConfig.gasPriceStalenessThreshold);

// Calculate premiumFee in USD with 18 decimals precision first.
// If message-only and no token transfers, a flat network fee is charged.
Expand Down Expand Up @@ -1014,7 +1035,7 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
return StaticConfig({
maxFeeJuelsPerMsg: i_maxFeeJuelsPerMsg,
linkToken: i_linkToken,
stalenessThreshold: i_stalenessThreshold
tokenPriceStalenessThreshold: i_tokenPriceStalenessThreshold
});
}
}
43 changes: 34 additions & 9 deletions contracts/src/v0.8/ccip/test/feeQuoter/FeeQuoter.t.sol
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -35,7 +33,7 @@ contract FeeQuoter_constructor is FeeQuoterSetup {
FeeQuoter.StaticConfig memory staticConfig = FeeQuoter.StaticConfig({
linkToken: s_sourceTokens[0],
maxFeeJuelsPerMsg: MAX_MSG_FEES_JUELS,
stalenessThreshold: uint32(TWELVE_HOURS)
tokenPriceStalenessThreshold: uint32(TWELVE_HOURS)
});
s_feeQuoter = new FeeQuoterHelper(
staticConfig,
Expand Down Expand Up @@ -93,7 +91,7 @@ contract FeeQuoter_constructor is FeeQuoterSetup {
FeeQuoter.StaticConfig memory staticConfig = FeeQuoter.StaticConfig({
linkToken: s_sourceTokens[0],
maxFeeJuelsPerMsg: MAX_MSG_FEES_JUELS,
stalenessThreshold: 0
tokenPriceStalenessThreshold: 0
});

vm.expectRevert(FeeQuoter.InvalidStaticConfig.selector);
Expand All @@ -113,7 +111,7 @@ contract FeeQuoter_constructor is FeeQuoterSetup {
FeeQuoter.StaticConfig memory staticConfig = FeeQuoter.StaticConfig({
linkToken: address(0),
maxFeeJuelsPerMsg: MAX_MSG_FEES_JUELS,
stalenessThreshold: uint32(TWELVE_HOURS)
tokenPriceStalenessThreshold: uint32(TWELVE_HOURS)
});

vm.expectRevert(FeeQuoter.InvalidStaticConfig.selector);
Expand All @@ -133,7 +131,7 @@ contract FeeQuoter_constructor is FeeQuoterSetup {
FeeQuoter.StaticConfig memory staticConfig = FeeQuoter.StaticConfig({
linkToken: s_sourceTokens[0],
maxFeeJuelsPerMsg: 0,
stalenessThreshold: uint32(TWELVE_HOURS)
tokenPriceStalenessThreshold: uint32(TWELVE_HOURS)
});

vm.expectRevert(FeeQuoter.InvalidStaticConfig.selector);
Expand Down Expand Up @@ -173,7 +171,7 @@ contract FeeQuoter_getTokenPrice is FeeQuoterSetup {
uint256 originalTimestampValue = block.timestamp;

// Above staleness threshold
vm.warp(originalTimestampValue + s_feeQuoter.getStaticConfig().stalenessThreshold + 1);
vm.warp(originalTimestampValue + s_feeQuoter.getStaticConfig().tokenPriceStalenessThreshold + 1);

address sourceToken = _initialiseSingleTokenPriceFeed();
Internal.TimestampedPackedUint224 memory tokenPriceAnswer = s_feeQuoter.getTokenPrice(sourceToken);
Expand Down Expand Up @@ -596,8 +594,35 @@ contract FeeQuoter_getTokenAndGasPrices is FeeQuoterSetup {
assertEq(gasPrice, priceUpdates.gasPriceUpdates[0].usdPerUnitGas);
}

function test_StalenessCheckDisabled_Success() public {
uint64 neverStaleChainSelector = 345678;
FeeQuoter.DestChainConfigArgs[] memory destChainConfigArgs = _generateFeeQuoterDestChainConfigArgs();
destChainConfigArgs[0].destChainSelector = neverStaleChainSelector;
destChainConfigArgs[0].destChainConfig.gasPriceStalenessThreshold = 0; // disables the staleness check

s_feeQuoter.applyDestChainConfigUpdates(destChainConfigArgs);

Internal.GasPriceUpdate[] memory gasPriceUpdates = new Internal.GasPriceUpdate[](1);
gasPriceUpdates[0] = Internal.GasPriceUpdate({destChainSelector: neverStaleChainSelector, usdPerUnitGas: 999});

Internal.PriceUpdates memory priceUpdates =
Internal.PriceUpdates({tokenPriceUpdates: new Internal.TokenPriceUpdate[](0), gasPriceUpdates: gasPriceUpdates});
s_feeQuoter.updatePrices(priceUpdates);

// this should have no affect! But we do it anyway to make sure the staleness check is disabled
vm.warp(block.timestamp + 52_000_000 weeks); // 1M-ish years

(, uint224 gasPrice) = s_feeQuoter.getTokenAndGasPrices(s_sourceFeeToken, neverStaleChainSelector);

assertEq(gasPrice, 999);
}

function test_ZeroGasPrice_Success() public {
uint64 zeroGasDestChainSelector = 345678;
FeeQuoter.DestChainConfigArgs[] memory destChainConfigArgs = _generateFeeQuoterDestChainConfigArgs();
destChainConfigArgs[0].destChainSelector = zeroGasDestChainSelector;

s_feeQuoter.applyDestChainConfigUpdates(destChainConfigArgs);
Internal.GasPriceUpdate[] memory gasPriceUpdates = new Internal.GasPriceUpdate[](1);
gasPriceUpdates[0] = Internal.GasPriceUpdate({destChainSelector: zeroGasDestChainSelector, usdPerUnitGas: 0});

Expand All @@ -607,11 +632,11 @@ contract FeeQuoter_getTokenAndGasPrices is FeeQuoterSetup {

(, uint224 gasPrice) = s_feeQuoter.getTokenAndGasPrices(s_sourceFeeToken, zeroGasDestChainSelector);

assertEq(gasPrice, priceUpdates.gasPriceUpdates[0].usdPerUnitGas);
assertEq(gasPrice, 0);
}

function test_UnsupportedChain_Revert() public {
vm.expectRevert(abi.encodeWithSelector(FeeQuoter.ChainNotSupported.selector, DEST_CHAIN_SELECTOR + 1));
vm.expectRevert(abi.encodeWithSelector(FeeQuoter.DestinationChainNotEnabled.selector, DEST_CHAIN_SELECTOR + 1));
s_feeQuoter.getTokenAndGasPrices(s_sourceTokens[0], DEST_CHAIN_SELECTOR + 1);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ contract FeeQuoterSetup is TokenSetup {
FeeQuoter.StaticConfig({
linkToken: s_sourceTokens[0],
maxFeeJuelsPerMsg: MAX_MSG_FEES_JUELS,
stalenessThreshold: uint32(TWELVE_HOURS)
tokenPriceStalenessThreshold: uint32(TWELVE_HOURS)
}),
priceUpdaters,
feeTokens,
Expand Down Expand Up @@ -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
})
Expand Down
6 changes: 3 additions & 3 deletions core/gethwrappers/ccip/deployment_test/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ func TestDeployAllV1_6(t *testing.T) {
owner,
chain,
fee_quoter.FeeQuoterStaticConfig{
MaxFeeJuelsPerMsg: big.NewInt(1e18),
LinkToken: common.HexToAddress("0x1"),
StalenessThreshold: 10,
MaxFeeJuelsPerMsg: big.NewInt(1e18),
LinkToken: common.HexToAddress("0x1"),
TokenPriceStalenessThreshold: 10,
},
[]common.Address{common.HexToAddress("0x1")},
[]common.Address{common.HexToAddress("0x2")},
Expand Down
15 changes: 8 additions & 7 deletions core/gethwrappers/ccip/generated/fee_quoter/fee_quoter.go

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ burn_with_from_mint_token_pool: ../../../contracts/solc/v0.8.24/BurnWithFromMint
ccip_encoding_utils: ../../../contracts/solc/v0.8.24/ICCIPEncodingUtils/ICCIPEncodingUtils.abi ../../../contracts/solc/v0.8.24/ICCIPEncodingUtils/ICCIPEncodingUtils.bin a074f2ecf2749a1d5afc4cd9bfa48677f09c2be4e076776f87c6feb767432ecb
ccip_home: ../../../contracts/solc/v0.8.24/CCIPHome/CCIPHome.abi ../../../contracts/solc/v0.8.24/CCIPHome/CCIPHome.bin 079b70ad36b4a9522518df82f01bdb8480fb9bb8de5791ef17ea1ddf044814be
ether_sender_receiver: ../../../contracts/solc/v0.8.24/EtherSenderReceiver/EtherSenderReceiver.abi ../../../contracts/solc/v0.8.24/EtherSenderReceiver/EtherSenderReceiver.bin 09510a3f773f108a3c231e8d202835c845ded862d071ec54c4f89c12d868b8de
fee_quoter: ../../../contracts/solc/v0.8.24/FeeQuoter/FeeQuoter.abi ../../../contracts/solc/v0.8.24/FeeQuoter/FeeQuoter.bin 6806f01f305df73a923361f128b8962e9a8d3e7338a9a5b5fbe0636e6c9fc35f
fee_quoter: ../../../contracts/solc/v0.8.24/FeeQuoter/FeeQuoter.abi ../../../contracts/solc/v0.8.24/FeeQuoter/FeeQuoter.bin 503823a939ff99fe3bdaaef7a89cd4bbe475e260d3921335dbf9c80d4f584b76
lock_release_token_pool: ../../../contracts/solc/v0.8.24/LockReleaseTokenPool/LockReleaseTokenPool.abi ../../../contracts/solc/v0.8.24/LockReleaseTokenPool/LockReleaseTokenPool.bin e6a8ec9e8faccb1da7d90e0f702ed72975964f97dc3222b54cfcca0a0ba3fea2
maybe_revert_message_receiver: ../../../contracts/solc/v0.8.24/MaybeRevertMessageReceiver/MaybeRevertMessageReceiver.abi ../../../contracts/solc/v0.8.24/MaybeRevertMessageReceiver/MaybeRevertMessageReceiver.bin d73956c26232ebcc4a5444429fa99cbefed960e323be9b5a24925885c2e477d5
message_hasher: ../../../contracts/solc/v0.8.24/MessageHasher/MessageHasher.abi ../../../contracts/solc/v0.8.24/MessageHasher/MessageHasher.bin ec2d3a92348d8e7b8f0d359b62a45157b9d2c750c01fbcf991826c4392f6e218
Expand Down
6 changes: 3 additions & 3 deletions integration-tests/deployment/ccip/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -567,9 +567,9 @@ func DeployChainContracts(
chain.DeployerKey,
chain.Client,
fee_quoter.FeeQuoterStaticConfig{
MaxFeeJuelsPerMsg: big.NewInt(0).Mul(big.NewInt(2e2), big.NewInt(1e18)),
LinkToken: contractConfig.LinkToken.Address(),
StalenessThreshold: uint32(24 * 60 * 60),
MaxFeeJuelsPerMsg: big.NewInt(0).Mul(big.NewInt(2e2), big.NewInt(1e18)),
LinkToken: contractConfig.LinkToken.Address(),
TokenPriceStalenessThreshold: uint32(24 * 60 * 60),
},
[]common.Address{mcmsContracts.Timelock.Address}, // timelock should be able to update, ramps added after
[]common.Address{contractConfig.Weth9.Address(), contractConfig.LinkToken.Address()}, // fee tokens
Expand Down
12 changes: 6 additions & 6 deletions integration-tests/deployment/ccip/view/v1_6/feequoter.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ type FeeQuoterView struct {
}

type FeeQuoterStaticConfig struct {
MaxFeeJuelsPerMsg string `json:"maxFeeJuelsPerMsg,omitempty"`
LinkToken string `json:"linkToken,omitempty"`
StalenessThreshold uint32 `json:"stalenessThreshold,omitempty"`
MaxFeeJuelsPerMsg string `json:"maxFeeJuelsPerMsg,omitempty"`
LinkToken string `json:"linkToken,omitempty"`
TokenPriceStalenessThreshold uint32 `json:"tokenPriseStalenessThreshold,omitempty"`
}

type FeeQuoterDestChainConfig struct {
Expand Down Expand Up @@ -78,9 +78,9 @@ func GenerateFeeQuoterView(fqContract *fee_quoter.FeeQuoter, router *router1_2.R
return FeeQuoterView{}, err
}
fq.StaticConfig = FeeQuoterStaticConfig{
MaxFeeJuelsPerMsg: staticConfig.MaxFeeJuelsPerMsg.String(),
LinkToken: staticConfig.LinkToken.Hex(),
StalenessThreshold: staticConfig.StalenessThreshold,
MaxFeeJuelsPerMsg: staticConfig.MaxFeeJuelsPerMsg.String(),
LinkToken: staticConfig.LinkToken.Hex(),
TokenPriceStalenessThreshold: staticConfig.TokenPriceStalenessThreshold,
}
// find router contract in dependencies
fq.DestinationChainConfig = make(map[uint64]FeeQuoterDestChainConfig)
Expand Down
Loading