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

Make gas fee staleness threshold configurable per chain #1491

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
RyanRHall marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -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
Loading