Skip to content
This repository was archived by the owner on Mar 14, 2025. It is now read-only.

Commit 2d5ff29

Browse files
authored
Make gas fee staleness threshold configurable per chain (#1491)
## Motivation On some chains, it is possible the gas price will just always be zero or will be fixed at some constant fee ## Solution Make stalenessThreshold per dest chain and have 0 mean no staleness check
1 parent 0a2e295 commit 2d5ff29

File tree

7 files changed

+208
-159
lines changed

7 files changed

+208
-159
lines changed

contracts/gas-snapshots/ccip.gas-snapshot

Lines changed: 122 additions & 121 deletions
Large diffs are not rendered by default.

contracts/src/v0.8/ccip/FeeQuoter.sol

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
2929

3030
error TokenNotSupported(address token);
3131
error FeeTokenNotSupported(address token);
32-
error ChainNotSupported(uint64 chain);
3332
error StaleGasPrice(uint64 destChainSelector, uint256 threshold, uint256 timePassed);
3433
error StaleKeystoneUpdate(address token, uint256 feedTimestamp, uint256 storedTimeStamp);
3534
error DataFeedValueOutOfUint224Range();
@@ -76,7 +75,8 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
7675
struct StaticConfig {
7776
uint96 maxFeeJuelsPerMsg; // ─╮ Maximum fee that can be charged for a message
7877
address linkToken; // ────────╯ LINK token address
79-
uint32 stalenessThreshold; // The amount of time a gas price can be stale before it is considered invalid.
78+
// The amount of time a token price can be stale before it is considered invalid (gas price staleness is configured per dest chain)
79+
uint32 tokenPriceStalenessThreshold;
8080
}
8181

8282
/// @dev The struct representing the received CCIP feed report from keystone IReceiver.onReport()
@@ -103,6 +103,7 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
103103
uint32 defaultTxGasLimit; //─────────────────╮ Default gas limit for a tx
104104
uint64 gasMultiplierWeiPerEth; // │ Multiplier for gas costs, 1e18 based so 11e17 = 10% extra cost.
105105
uint32 networkFeeUSDCents; // │ Flat network fee to charge for messages, multiples of 0.01 USD
106+
uint32 gasPriceStalenessThreshold; // │ The amount of time a gas price can be stale before it is considered invalid (0 means disabled)
106107
bool enforceOutOfOrder; // │ Whether to enforce the allowOutOfOrderExecution extraArg value to be true.
107108
bytes4 chainFamilySelector; // ──────────────╯ Selector that identifies the destination chain's family. Used to determine the correct validations to perform for the dest chain.
108109
}
@@ -202,8 +203,8 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
202203

203204
/// @dev Subset of tokens which prices tracked by this registry which are fee tokens.
204205
EnumerableSet.AddressSet private s_feeTokens;
205-
/// @dev The amount of time a gas price can be stale before it is considered invalid.
206-
uint32 private immutable i_stalenessThreshold;
206+
/// @dev The amount of time a token price can be stale before it is considered invalid.
207+
uint32 private immutable i_tokenPriceStalenessThreshold;
207208

208209
constructor(
209210
StaticConfig memory staticConfig,
@@ -216,14 +217,14 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
216217
) AuthorizedCallers(priceUpdaters) {
217218
if (
218219
staticConfig.linkToken == address(0) || staticConfig.maxFeeJuelsPerMsg == 0
219-
|| staticConfig.stalenessThreshold == 0
220+
|| staticConfig.tokenPriceStalenessThreshold == 0
220221
) {
221222
revert InvalidStaticConfig();
222223
}
223224

224225
i_linkToken = staticConfig.linkToken;
225226
i_maxFeeJuelsPerMsg = staticConfig.maxFeeJuelsPerMsg;
226-
i_stalenessThreshold = staticConfig.stalenessThreshold;
227+
i_tokenPriceStalenessThreshold = staticConfig.tokenPriceStalenessThreshold;
227228

228229
_applyFeeTokensUpdates(feeTokens, new address[](0));
229230
_updateTokenPriceFeeds(tokenPriceFeeds);
@@ -241,7 +242,7 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
241242
Internal.TimestampedPackedUint224 memory tokenPrice = s_usdPerToken[token];
242243

243244
// If the token price is not stale, return it
244-
if (block.timestamp - tokenPrice.timestamp < i_stalenessThreshold) {
245+
if (block.timestamp - tokenPrice.timestamp < i_tokenPriceStalenessThreshold) {
245246
return tokenPrice;
246247
}
247248

@@ -305,14 +306,12 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
305306
function getTokenAndGasPrices(
306307
address token,
307308
uint64 destChainSelector
308-
) public view returns (uint224 tokenPrice, uint224 gasPriceValue) {
309-
Internal.TimestampedPackedUint224 memory gasPrice = s_usdPerUnitGasByDestChainSelector[destChainSelector];
310-
// We do allow a gas price of 0, but no stale or unset gas prices
311-
if (gasPrice.timestamp == 0) revert ChainNotSupported(destChainSelector);
312-
uint256 timePassed = block.timestamp - gasPrice.timestamp;
313-
if (timePassed > i_stalenessThreshold) revert StaleGasPrice(destChainSelector, i_stalenessThreshold, timePassed);
314-
315-
return (_getValidatedTokenPrice(token), gasPrice.value);
309+
) external view returns (uint224 tokenPrice, uint224 gasPriceValue) {
310+
if (!s_destChainConfigs[destChainSelector].isEnabled) revert DestinationChainNotEnabled(destChainSelector);
311+
return (
312+
_getValidatedTokenPrice(token),
313+
_getValidatedGasPrice(destChainSelector, s_destChainConfigs[destChainSelector].gasPriceStalenessThreshold)
314+
);
316315
}
317316

318317
/// @notice Convert a given token amount to target token amount.
@@ -374,6 +373,27 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
374373
return Internal.TimestampedPackedUint224({value: rebasedValue, timestamp: uint32(block.timestamp)});
375374
}
376375

376+
/// @dev Gets the fee token price and the gas price, both denominated in dollars.
377+
/// @param destChainSelector The destination chain to get the gas price for.
378+
/// @param gasPriceStalenessThreshold The amount of time a gas price can be stale before it is considered invalid.
379+
/// @return gasPriceValue The price of gas in 1e18 dollars per base unit.
380+
function _getValidatedGasPrice(
381+
uint64 destChainSelector,
382+
uint32 gasPriceStalenessThreshold
383+
) private view returns (uint224 gasPriceValue) {
384+
Internal.TimestampedPackedUint224 memory gasPrice = s_usdPerUnitGasByDestChainSelector[destChainSelector];
385+
// If the staleness threshold is 0, we consider the gas price to be always valid
386+
if (gasPriceStalenessThreshold != 0) {
387+
// We do allow a gas price of 0, but no stale or unset gas prices
388+
uint256 timePassed = block.timestamp - gasPrice.timestamp;
389+
if (timePassed > gasPriceStalenessThreshold) {
390+
revert StaleGasPrice(destChainSelector, gasPriceStalenessThreshold, timePassed);
391+
}
392+
}
393+
394+
return gasPrice.value;
395+
}
396+
377397
// ================================================================
378398
// │ Fee tokens │
379399
// ================================================================
@@ -507,7 +527,8 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
507527
_validateMessage(destChainConfig, message.data.length, numberOfTokens, message.receiver);
508528

509529
// The below call asserts that feeToken is a supported token
510-
(uint224 feeTokenPrice, uint224 packedGasPrice) = getTokenAndGasPrices(message.feeToken, destChainSelector);
530+
uint224 feeTokenPrice = _getValidatedTokenPrice(message.feeToken);
531+
uint224 packedGasPrice = _getValidatedGasPrice(destChainSelector, destChainConfig.gasPriceStalenessThreshold);
511532

512533
// Calculate premiumFee in USD with 18 decimals precision first.
513534
// If message-only and no token transfers, a flat network fee is charged.
@@ -990,7 +1011,7 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
9901011
return StaticConfig({
9911012
maxFeeJuelsPerMsg: i_maxFeeJuelsPerMsg,
9921013
linkToken: i_linkToken,
993-
stalenessThreshold: i_stalenessThreshold
1014+
tokenPriceStalenessThreshold: i_tokenPriceStalenessThreshold
9941015
});
9951016
}
9961017
}

contracts/src/v0.8/ccip/test/feeQuoter/FeeQuoter.t.sol

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// SPDX-License-Identifier: BUSL-1.1
22
pragma solidity 0.8.24;
33

4-
import {IFeeQuoter} from "../../interfaces/IFeeQuoter.sol";
5-
64
import {KeystoneFeedsPermissionHandler} from "../../../keystone/KeystoneFeedsPermissionHandler.sol";
75
import {AuthorizedCallers} from "../../../shared/access/AuthorizedCallers.sol";
86
import {MockV3Aggregator} from "../../../tests/MockV3Aggregator.sol";
@@ -35,7 +33,7 @@ contract FeeQuoter_constructor is FeeQuoterSetup {
3533
FeeQuoter.StaticConfig memory staticConfig = FeeQuoter.StaticConfig({
3634
linkToken: s_sourceTokens[0],
3735
maxFeeJuelsPerMsg: MAX_MSG_FEES_JUELS,
38-
stalenessThreshold: uint32(TWELVE_HOURS)
36+
tokenPriceStalenessThreshold: uint32(TWELVE_HOURS)
3937
});
4038
s_feeQuoter = new FeeQuoterHelper(
4139
staticConfig,
@@ -93,7 +91,7 @@ contract FeeQuoter_constructor is FeeQuoterSetup {
9391
FeeQuoter.StaticConfig memory staticConfig = FeeQuoter.StaticConfig({
9492
linkToken: s_sourceTokens[0],
9593
maxFeeJuelsPerMsg: MAX_MSG_FEES_JUELS,
96-
stalenessThreshold: 0
94+
tokenPriceStalenessThreshold: 0
9795
});
9896

9997
vm.expectRevert(FeeQuoter.InvalidStaticConfig.selector);
@@ -113,7 +111,7 @@ contract FeeQuoter_constructor is FeeQuoterSetup {
113111
FeeQuoter.StaticConfig memory staticConfig = FeeQuoter.StaticConfig({
114112
linkToken: address(0),
115113
maxFeeJuelsPerMsg: MAX_MSG_FEES_JUELS,
116-
stalenessThreshold: uint32(TWELVE_HOURS)
114+
tokenPriceStalenessThreshold: uint32(TWELVE_HOURS)
117115
});
118116

119117
vm.expectRevert(FeeQuoter.InvalidStaticConfig.selector);
@@ -133,7 +131,7 @@ contract FeeQuoter_constructor is FeeQuoterSetup {
133131
FeeQuoter.StaticConfig memory staticConfig = FeeQuoter.StaticConfig({
134132
linkToken: s_sourceTokens[0],
135133
maxFeeJuelsPerMsg: 0,
136-
stalenessThreshold: uint32(TWELVE_HOURS)
134+
tokenPriceStalenessThreshold: uint32(TWELVE_HOURS)
137135
});
138136

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

175173
// Above staleness threshold
176-
vm.warp(originalTimestampValue + s_feeQuoter.getStaticConfig().stalenessThreshold + 1);
174+
vm.warp(originalTimestampValue + s_feeQuoter.getStaticConfig().tokenPriceStalenessThreshold + 1);
177175

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

597+
function test_StalenessCheckDisabled_Success() public {
598+
uint64 neverStaleChainSelector = 345678;
599+
FeeQuoter.DestChainConfigArgs[] memory destChainConfigArgs = _generateFeeQuoterDestChainConfigArgs();
600+
destChainConfigArgs[0].destChainSelector = neverStaleChainSelector;
601+
destChainConfigArgs[0].destChainConfig.gasPriceStalenessThreshold = 0; // disables the staleness check
602+
603+
s_feeQuoter.applyDestChainConfigUpdates(destChainConfigArgs);
604+
605+
Internal.GasPriceUpdate[] memory gasPriceUpdates = new Internal.GasPriceUpdate[](1);
606+
gasPriceUpdates[0] = Internal.GasPriceUpdate({destChainSelector: neverStaleChainSelector, usdPerUnitGas: 999});
607+
608+
Internal.PriceUpdates memory priceUpdates =
609+
Internal.PriceUpdates({tokenPriceUpdates: new Internal.TokenPriceUpdate[](0), gasPriceUpdates: gasPriceUpdates});
610+
s_feeQuoter.updatePrices(priceUpdates);
611+
612+
// this should have no affect! But we do it anyway to make sure the staleness check is disabled
613+
vm.warp(block.timestamp + 52_000_000 weeks); // 1M-ish years
614+
615+
(, uint224 gasPrice) = s_feeQuoter.getTokenAndGasPrices(s_sourceFeeToken, neverStaleChainSelector);
616+
617+
assertEq(gasPrice, 999);
618+
}
619+
599620
function test_ZeroGasPrice_Success() public {
600621
uint64 zeroGasDestChainSelector = 345678;
622+
FeeQuoter.DestChainConfigArgs[] memory destChainConfigArgs = _generateFeeQuoterDestChainConfigArgs();
623+
destChainConfigArgs[0].destChainSelector = zeroGasDestChainSelector;
624+
625+
s_feeQuoter.applyDestChainConfigUpdates(destChainConfigArgs);
601626
Internal.GasPriceUpdate[] memory gasPriceUpdates = new Internal.GasPriceUpdate[](1);
602627
gasPriceUpdates[0] = Internal.GasPriceUpdate({destChainSelector: zeroGasDestChainSelector, usdPerUnitGas: 0});
603628

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

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

610-
assertEq(gasPrice, priceUpdates.gasPriceUpdates[0].usdPerUnitGas);
635+
assertEq(gasPrice, 0);
611636
}
612637

613638
function test_UnsupportedChain_Revert() public {
614-
vm.expectRevert(abi.encodeWithSelector(FeeQuoter.ChainNotSupported.selector, DEST_CHAIN_SELECTOR + 1));
639+
vm.expectRevert(abi.encodeWithSelector(FeeQuoter.DestinationChainNotEnabled.selector, DEST_CHAIN_SELECTOR + 1));
615640
s_feeQuoter.getTokenAndGasPrices(s_sourceTokens[0], DEST_CHAIN_SELECTOR + 1);
616641
}
617642

contracts/src/v0.8/ccip/test/feeQuoter/FeeQuoterSetup.t.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ contract FeeQuoterSetup is TokenSetup {
162162
FeeQuoter.StaticConfig({
163163
linkToken: s_sourceTokens[0],
164164
maxFeeJuelsPerMsg: MAX_MSG_FEES_JUELS,
165-
stalenessThreshold: uint32(TWELVE_HOURS)
165+
tokenPriceStalenessThreshold: uint32(TWELVE_HOURS)
166166
}),
167167
priceUpdaters,
168168
feeTokens,
@@ -254,6 +254,7 @@ contract FeeQuoterSetup is TokenSetup {
254254
defaultTxGasLimit: GAS_LIMIT,
255255
gasMultiplierWeiPerEth: 5e17,
256256
networkFeeUSDCents: 1_00,
257+
gasPriceStalenessThreshold: uint32(TWELVE_HOURS),
257258
enforceOutOfOrder: false,
258259
chainFamilySelector: Internal.CHAIN_FAMILY_SELECTOR_EVM
259260
})

core/gethwrappers/ccip/deployment_test/deployment_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@ func TestDeployAllV1_6(t *testing.T) {
7373
owner,
7474
chain,
7575
fee_quoter.FeeQuoterStaticConfig{
76-
MaxFeeJuelsPerMsg: big.NewInt(1e18),
77-
LinkToken: common.HexToAddress("0x1"),
78-
StalenessThreshold: 10,
76+
MaxFeeJuelsPerMsg: big.NewInt(1e18),
77+
LinkToken: common.HexToAddress("0x1"),
78+
TokenPriceStalenessThreshold: 10,
7979
},
8080
[]common.Address{common.HexToAddress("0x1")},
8181
[]common.Address{common.HexToAddress("0x2")},

core/gethwrappers/ccip/generated/fee_quoter/fee_quoter.go

Lines changed: 8 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/gethwrappers/ccip/generation/generated-wrapper-dependency-versions-do-not-edit.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ commit_store_helper: ../../../contracts/solc/v0.8.24/CommitStoreHelper/CommitSto
1313
ether_sender_receiver: ../../../contracts/solc/v0.8.24/EtherSenderReceiver/EtherSenderReceiver.abi ../../../contracts/solc/v0.8.24/EtherSenderReceiver/EtherSenderReceiver.bin 09510a3f773f108a3c231e8d202835c845ded862d071ec54c4f89c12d868b8de
1414
evm_2_evm_offramp: ../../../contracts/solc/v0.8.24/EVM2EVMOffRamp/EVM2EVMOffRamp.abi ../../../contracts/solc/v0.8.24/EVM2EVMOffRamp/EVM2EVMOffRamp.bin b0d77babbe635cd6ba04c2af049badc9e9d28a4b6ed6bb75f830ad902a618beb
1515
evm_2_evm_onramp: ../../../contracts/solc/v0.8.24/EVM2EVMOnRamp/EVM2EVMOnRamp.abi ../../../contracts/solc/v0.8.24/EVM2EVMOnRamp/EVM2EVMOnRamp.bin 5c02c2b167946b3467636ff2bb58594cb4652fc63d8bdfee2488ed562e2a3e50
16-
fee_quoter: ../../../contracts/solc/v0.8.24/FeeQuoter/FeeQuoter.abi ../../../contracts/solc/v0.8.24/FeeQuoter/FeeQuoter.bin 6806f01f305df73a923361f128b8962e9a8d3e7338a9a5b5fbe0636e6c9fc35f
16+
fee_quoter: ../../../contracts/solc/v0.8.24/FeeQuoter/FeeQuoter.abi ../../../contracts/solc/v0.8.24/FeeQuoter/FeeQuoter.bin 503823a939ff99fe3bdaaef7a89cd4bbe475e260d3921335dbf9c80d4f584b76
1717
lock_release_token_pool: ../../../contracts/solc/v0.8.24/LockReleaseTokenPool/LockReleaseTokenPool.abi ../../../contracts/solc/v0.8.24/LockReleaseTokenPool/LockReleaseTokenPool.bin e6a8ec9e8faccb1da7d90e0f702ed72975964f97dc3222b54cfcca0a0ba3fea2
1818
lock_release_token_pool_and_proxy: ../../../contracts/solc/v0.8.24/LockReleaseTokenPoolAndProxy/LockReleaseTokenPoolAndProxy.abi ../../../contracts/solc/v0.8.24/LockReleaseTokenPoolAndProxy/LockReleaseTokenPoolAndProxy.bin e632b08be0fbd1d013e8b3a9d75293d0d532b83071c531ff2be1deec1fa48ec1
1919
maybe_revert_message_receiver: ../../../contracts/solc/v0.8.24/MaybeRevertMessageReceiver/MaybeRevertMessageReceiver.abi ../../../contracts/solc/v0.8.24/MaybeRevertMessageReceiver/MaybeRevertMessageReceiver.bin d73956c26232ebcc4a5444429fa99cbefed960e323be9b5a24925885c2e477d5

0 commit comments

Comments
 (0)