Skip to content

Commit

Permalink
Make gas fee staleness threshold configurable per chain (#1491)
Browse files Browse the repository at this point in the history
On some chains, it is possible the gas price will just always be zero or
will be fixed at some constant fee

Make stalenessThreshold per dest chain and have 0 mean no staleness
check
  • Loading branch information
RyanRHall committed Oct 11, 2024
1 parent a41ce0a commit 7cb01cb
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 159 deletions.
243 changes: 122 additions & 121 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 @@ -241,7 +242,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 @@ -305,14 +306,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 @@ -374,6 +373,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 @@ -507,7 +527,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 @@ -990,7 +1011,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
3 changes: 2 additions & 1 deletion contracts/src/v0.8/ccip/test/feeQuoter/FeeQuoterSetup.t.sol
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 @@ -13,7 +13,7 @@ commit_store_helper: ../../../contracts/solc/v0.8.24/CommitStoreHelper/CommitSto
ether_sender_receiver: ../../../contracts/solc/v0.8.24/EtherSenderReceiver/EtherSenderReceiver.abi ../../../contracts/solc/v0.8.24/EtherSenderReceiver/EtherSenderReceiver.bin 09510a3f773f108a3c231e8d202835c845ded862d071ec54c4f89c12d868b8de
evm_2_evm_offramp: ../../../contracts/solc/v0.8.24/EVM2EVMOffRamp/EVM2EVMOffRamp.abi ../../../contracts/solc/v0.8.24/EVM2EVMOffRamp/EVM2EVMOffRamp.bin b0d77babbe635cd6ba04c2af049badc9e9d28a4b6ed6bb75f830ad902a618beb
evm_2_evm_onramp: ../../../contracts/solc/v0.8.24/EVM2EVMOnRamp/EVM2EVMOnRamp.abi ../../../contracts/solc/v0.8.24/EVM2EVMOnRamp/EVM2EVMOnRamp.bin 5c02c2b167946b3467636ff2bb58594cb4652fc63d8bdfee2488ed562e2a3e50
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
lock_release_token_pool_and_proxy: ../../../contracts/solc/v0.8.24/LockReleaseTokenPoolAndProxy/LockReleaseTokenPoolAndProxy.abi ../../../contracts/solc/v0.8.24/LockReleaseTokenPoolAndProxy/LockReleaseTokenPoolAndProxy.bin e632b08be0fbd1d013e8b3a9d75293d0d532b83071c531ff2be1deec1fa48ec1
maybe_revert_message_receiver: ../../../contracts/solc/v0.8.24/MaybeRevertMessageReceiver/MaybeRevertMessageReceiver.abi ../../../contracts/solc/v0.8.24/MaybeRevertMessageReceiver/MaybeRevertMessageReceiver.bin d73956c26232ebcc4a5444429fa99cbefed960e323be9b5a24925885c2e477d5
Expand Down

0 comments on commit 7cb01cb

Please sign in to comment.