Skip to content

Commit

Permalink
Merge dd8ca34 into 498b0b8
Browse files Browse the repository at this point in the history
  • Loading branch information
jhweintraub authored Dec 27, 2024
2 parents 498b0b8 + dd8ca34 commit 227b5c3
Show file tree
Hide file tree
Showing 16 changed files with 618 additions and 142 deletions.
10 changes: 10 additions & 0 deletions contracts/.changeset/clean-horses-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@chainlink/contracts': minor
---

Update FeeQuoter to support Solana chain families #feature


PR issue: CCIP-4687

Solidity Review issue: CCIP-3966
199 changes: 102 additions & 97 deletions contracts/gas-snapshots/ccip.gas-snapshot

Large diffs are not rendered by default.

120 changes: 107 additions & 13 deletions contracts/src/v0.8/ccip/FeeQuoter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {EnumerableSet} from "../vendor/openzeppelin-solidity/v5.0.2/contracts/ut
/// The authorized callers in the contract represent the fee price updaters.
contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver, KeystoneFeedsPermissionHandler {
using EnumerableSet for EnumerableSet.AddressSet;
using EnumerableSet for EnumerableSet.Bytes32Set;
using USDPriceWith18Decimals for uint224;
using KeystoneFeedDefaultMetadataLib for bytes;

Expand All @@ -44,6 +45,8 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
error MessageTooLarge(uint256 maxSize, uint256 actualSize);
error UnsupportedNumberOfTokens(uint256 numberOfTokens, uint256 maxNumberOfTokensPerMsg);
error InvalidFeeRange(uint256 minFeeUSDCents, uint256 maxFeeUSDCents);
error FirstSolExtraArgsAddressCannotBeWritable();
error SolExtraArgsMustBeProvided();

event FeeTokenAdded(address indexed feeToken);
event FeeTokenRemoved(address indexed feeToken);
Expand All @@ -57,6 +60,7 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
event PremiumMultiplierWeiPerEthUpdated(address indexed token, uint64 premiumMultiplierWeiPerEth);
event DestChainConfigUpdated(uint64 indexed destChainSelector, DestChainConfig destChainConfig);
event DestChainAdded(uint64 indexed destChainSelector, DestChainConfig destChainConfig);
event ChainFamilySelectorModified(bytes4 chainFamilySelector, bool isAdded);

/// @dev Contains token price configuration used in both the keystone price updates and the price feed fallback logic.
struct TokenPriceFeedConfig {
Expand Down Expand Up @@ -602,7 +606,7 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
* (
destChainConfig.destGasOverhead
+ ((message.data.length + tokenTransferBytesOverhead) * destChainConfig.destGasPerPayloadByte) + tokenTransferGas
+ _parseEVMExtraArgsFromBytes(message.extraArgs, destChainConfig).gasLimit
+ _parseGasLimitFromExtraArgBytes(message.extraArgs, destChainConfig)
) * destChainConfig.gasMultiplierWeiPerEth;

// Calculate number of fee tokens to charge.
Expand Down Expand Up @@ -848,12 +852,73 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
/// @param chainFamilySelector Tag to identify the target family.
/// @param destAddress Dest address to validate.
/// @dev precondition - assumes the family tag is correct and validated.
/// @dev Since Solana addresses are parsed as bytes32, and no other form of validation occurs, no explicit
/// call is needed to a library function for address validation.
function _validateDestFamilyAddress(bytes4 chainFamilySelector, bytes memory destAddress) internal pure {
if (chainFamilySelector == Internal.CHAIN_FAMILY_SELECTOR_EVM) {
Internal._validateEVMAddress(destAddress);
}
}

function _parseGasLimitFromExtraArgBytes(
bytes calldata extraArgs,
DestChainConfig memory destChainConfig
) internal pure returns (uint256 gasLimit) {
if (destChainConfig.chainFamilySelector == Internal.CHAIN_FAMILY_SELECTOR_EVM) {
gasLimit = _parseEVMExtraArgsFromBytes(extraArgs, destChainConfig).gasLimit;
} else if (destChainConfig.chainFamilySelector == Internal.CHAIN_FAMILY_SELECTOR_SOL) {
// If extra args are empty, generate default values.
gasLimit = _parseSolExtraArgsFromBytes(extraArgs, destChainConfig, 0).computeUnits;
} else {
gasLimit = destChainConfig.defaultTxGasLimit;
}

return gasLimit;
}

/// @notice Parse and validate the Solana specific Extra Args Bytes.
/// @param messageDataLengthBytes The length of the arbitrary message data. If this is non-zero, then an extraArgs
/// MUST be provided otherwise an error will be returned, due to requirements in how Solana accounts operate.
function _parseSolExtraArgsFromBytes(
bytes calldata extraArgs,
DestChainConfig memory destChainConfig,
uint256 messageDataLengthBytes
) internal pure returns (Client.SolExtraArgsV1 memory) {
Client.SolExtraArgsV1 memory SolExtraArgs =
_parseUnvalidatedSolExtraArgsFromBytes(extraArgs, destChainConfig.defaultTxGasLimit);

// If the message data length is non-zero, then accounts extra args must be provided.
if (messageDataLengthBytes != 0 && SolExtraArgs.accounts.length == 0) revert SolExtraArgsMustBeProvided();

// The Program name being invoked, which is the first account provided, cannot be writable on Solana.
if (SolExtraArgs.accounts.length != 0 && SolExtraArgs.accounts[0].isWritable) {
revert FirstSolExtraArgsAddressCannotBeWritable();
}

// Check that compute units is within the allowed range.
if (SolExtraArgs.computeUnits > uint256(destChainConfig.maxPerMsgGasLimit)) revert MessageGasLimitTooHigh();

return SolExtraArgs;
}

function _parseUnvalidatedSolExtraArgsFromBytes(
bytes calldata extraArgs,
uint64 defaultTxGasLimit
) internal pure returns (Client.SolExtraArgsV1 memory) {
if (extraArgs.length == 0) {
return Client.SolExtraArgsV1({
computeUnits: uint32(defaultTxGasLimit), //TODO: Fix Potentially unsafe cast
accounts: new Client.SolanaAccountMeta[](0)
});
}

bytes memory argsData = extraArgs[4:];

Client.SolExtraArgsV1 memory solExtraArgs = abi.decode(argsData, (Client.SolExtraArgsV1));

return solExtraArgs;
}

/// @dev Convert the extra args bytes into a struct with validations against the dest chain config.
/// @param extraArgs The extra args bytes.
/// @param destChainConfig Dest chain config to validate against.
Expand Down Expand Up @@ -928,7 +993,10 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,

/// @inheritdoc IFeeQuoter
/// @dev precondition - onRampTokenTransfers and sourceTokenAmounts lengths must be equal.
/// @param messageData The arbitrary bytes to be processed. Solana address requirements require that if an arbitrary message
/// exists, then extraArgs must also be checked for a valid recipient account.
function processMessageArgs(
bytes calldata messageData,
uint64 destChainSelector,
address feeToken,
uint256 feeTokenAmount,
Expand All @@ -945,6 +1013,12 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
bytes[] memory destExecDataPerToken
)
{
// Prevents a stack too deep error on messageData
{
bytes memory data = messageData;
(convertedExtraArgs, isOutOfOrderExecution) = _processChainFamilySelector(destChainSelector, data, extraArgs);
}

// Convert feeToken to link if not already in link.
if (feeToken == i_linkToken) {
msgFeeJuels = feeTokenAmount;
Expand All @@ -954,16 +1028,38 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,

if (msgFeeJuels > i_maxFeeJuelsPerMsg) revert MessageFeeTooHigh(msgFeeJuels, i_maxFeeJuelsPerMsg);

uint64 defaultTxGasLimit = s_destChainConfigs[destChainSelector].defaultTxGasLimit;

// NOTE: Only EVM chains are supported for now, additional validation logic will be added when supporting other
// chain families to parse non-EVM args.
// Since the message is called after getFee, which will already validate the params, no validation is necessary.
Client.EVMExtraArgsV2 memory parsedExtraArgs = _parseUnvalidatedEVMExtraArgsFromBytes(extraArgs, defaultTxGasLimit);
isOutOfOrderExecution = parsedExtraArgs.allowOutOfOrderExecution;
destExecDataPerToken = _processPoolReturnData(destChainSelector, onRampTokenTransfers, sourceTokenAmounts);

return (msgFeeJuels, isOutOfOrderExecution, Client._argsToBytes(parsedExtraArgs), destExecDataPerToken);
return (msgFeeJuels, isOutOfOrderExecution, convertedExtraArgs, destExecDataPerToken);
}

/// @notice Parses the extra Args based on the chain family selector. Isolated into a separate function
/// as it was the only way to prevent a stack too deep error, and makes future chain family additions easier.
function _processChainFamilySelector(
uint64 destChainSelector,
bytes memory message,
bytes calldata extraArgs
) internal view returns (bytes memory convertedExtraArgs, bool isOutOfOrderExecution) {
DestChainConfig memory destChainConfig = s_destChainConfigs[destChainSelector];
if (destChainConfig.chainFamilySelector == Internal.CHAIN_FAMILY_SELECTOR_EVM) {
// Since the message is called after getFee, which will already validate the params, no validation is necessary.
Client.EVMExtraArgsV2 memory parsedExtraArgs =
_parseUnvalidatedEVMExtraArgsFromBytes(extraArgs, destChainConfig.defaultTxGasLimit);

convertedExtraArgs = Client._argsToBytes(parsedExtraArgs);

isOutOfOrderExecution = parsedExtraArgs.allowOutOfOrderExecution;
} else if (destChainConfig.chainFamilySelector == Internal.CHAIN_FAMILY_SELECTOR_SOL) {
Client.SolExtraArgsV1 memory parsedExtraArgs =
_parseSolExtraArgsFromBytes(extraArgs, destChainConfig, message.length);

convertedExtraArgs = Client._solArgsToBytes(parsedExtraArgs);

// On Solana OOO execution is enabled for all messages.
isOutOfOrderExecution = true;
}

return (convertedExtraArgs, isOutOfOrderExecution);
}

/// @notice Validates pool return data.
Expand Down Expand Up @@ -1038,12 +1134,10 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
uint64 destChainSelector = destChainConfigArgs[i].destChainSelector;
DestChainConfig memory destChainConfig = destChainConfigArg.destChainConfig;

// destChainSelector must be non-zero, defaultTxGasLimit must be set, and must be less than maxPerMsgGasLimit.
// Only EVM chains are supported for now, additional validation logic will be added when supporting other chain
// families
// destChainSelector must be non-zero, defaultTxGasLimit must be set, must be less than maxPerMsgGasLimit
// TODO: With the addition of Solana and other Non-evm Chains, family selector is not validated.
if (
destChainSelector == 0 || destChainConfig.defaultTxGasLimit == 0
|| destChainConfig.chainFamilySelector != Internal.CHAIN_FAMILY_SELECTOR_EVM
|| destChainConfig.defaultTxGasLimit > destChainConfig.maxPerMsgGasLimit
) {
revert InvalidDestChainConfig(destChainSelector);
Expand Down
2 changes: 2 additions & 0 deletions contracts/src/v0.8/ccip/interfaces/IFeeQuoter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface IFeeQuoter is IPriceRegistry {

/// @notice Converts the extraArgs to the latest version and returns the converted message fee in juels.
/// @notice Validates pool return data.
/// @param message The message to process, necessary when sending messages to Solana to check against extraArgs validity.
/// @param destChainSelector destination chain selector to process, must be a configured valid chain.
/// @param feeToken token address used to pay for message fees, must be a configured valid fee token.
/// @param feeTokenAmount Fee token amount.
Expand All @@ -28,6 +29,7 @@ interface IFeeQuoter is IPriceRegistry {
/// @return convertedExtraArgs extra args converted to the latest family-specific args version.
/// @return destExecDataPerToken Destination chain execution data.
function processMessageArgs(
bytes calldata message,
uint64 destChainSelector,
address feeToken,
uint256 feeTokenAmount,
Expand Down
19 changes: 19 additions & 0 deletions contracts/src/v0.8/ccip/libraries/Client.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ library Client {
// bytes4(keccak256("CCIP EVMExtraArgsV2"));
bytes4 public constant EVM_EXTRA_ARGS_V2_TAG = 0x181dcf10;

// bytes4(keccak256("CCIP SolExtraArgsV1"));
bytes4 public constant SOL_EXTRA_EXTRA_ARGS_V1_TAG = 0x3f2538fa;

/// @param gasLimit: gas limit for the callback on the destination chain.
/// @param allowOutOfOrderExecution: if true, it indicates that the message can be executed in any order relative to
/// other messages from the same sender. This value's default varies by chain. On some chains, a particular value is
Expand All @@ -51,9 +54,25 @@ library Client {
bool allowOutOfOrderExecution;
}

struct SolExtraArgsV1 {
uint32 computeUnits;
SolanaAccountMeta[] accounts;
}

struct SolanaAccountMeta {
bytes32 pubKey;
bool isWritable;
}

function _argsToBytes(
EVMExtraArgsV2 memory extraArgs
) internal pure returns (bytes memory bts) {
return abi.encodeWithSelector(EVM_EXTRA_ARGS_V2_TAG, extraArgs);
}

function _solArgsToBytes(
SolExtraArgsV1 memory extraArgs
) internal pure returns (bytes memory bts) {
return abi.encodeWithSelector(SOL_EXTRA_EXTRA_ARGS_V1_TAG, extraArgs);
}
}
11 changes: 11 additions & 0 deletions contracts/src/v0.8/ccip/libraries/Internal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {MerkleMultiProof} from "../libraries/MerkleMultiProof.sol";
/// expect to have migrated to a new version by then.
library Internal {
error InvalidEVMAddress(bytes encodedAddress);
error InvalidSolAddress(bytes SolAddress);

/// @dev We limit return data to a selector plus 4 words. This is to avoid malicious contracts from returning
/// large amounts of data and causing repeated out-of-gas scenarios.
Expand Down Expand Up @@ -173,6 +174,13 @@ library Internal {
return address(uint160(encodedAddressUint));
}

// TODO: Comments for why this single check is done here so that it is future thinking
function _validateSolAddress(
bytes memory solAddress
) internal pure {
if (solAddress.length != 32) revert InvalidSolAddress(solAddress);
}

/// @notice Enum listing the possible message execution states within the offRamp contract.
/// UNTOUCHED never executed.
/// IN_PROGRESS currently being executed, used a replay protection.
Expand Down Expand Up @@ -262,6 +270,9 @@ library Internal {
// bytes4(keccak256("CCIP ChainFamilySelector EVM"));
bytes4 public constant CHAIN_FAMILY_SELECTOR_EVM = 0x2812d52c;

// bytes4(keccak256("CCIP ChainFamilySelector SOL"));
bytes4 public constant CHAIN_FAMILY_SELECTOR_SOL = 0x2d4dfc1c;

/// @dev Holds a merkle root and interval for a source chain so that an array of these can be passed in the CommitReport.
/// @dev RMN depends on this struct, if changing, please notify the RMN maintainers.
/// @dev inefficient struct packing intentionally chosen to maintain order of specificity. Not a storage struct so impact is minimal.
Expand Down
9 changes: 8 additions & 1 deletion contracts/src/v0.8/ccip/onRamp/OnRamp.sol
Original file line number Diff line number Diff line change
Expand Up @@ -221,13 +221,20 @@ contract OnRamp is IEVM2AnyOnRampClient, ITypeAndVersion, Ownable2StepMsgSender

// Convert message fee to juels and retrieve converted args.
// Validate pool return data after it is populated (view function - no state changes).
// Using newMessage.data prevents a stack too deep error.
bool isOutOfOrderExecution;
bytes memory convertedExtraArgs;
bytes[] memory destExecDataPerToken;
(newMessage.feeValueJuels, isOutOfOrderExecution, convertedExtraArgs, destExecDataPerToken) = IFeeQuoter(
s_dynamicConfig.feeQuoter
).processMessageArgs(
destChainSelector, message.feeToken, feeTokenAmount, message.extraArgs, newMessage.tokenAmounts, tokenAmounts
newMessage.data,
destChainSelector,
message.feeToken,
feeTokenAmount,
message.extraArgs,
newMessage.tokenAmounts,
tokenAmounts
);

newMessage.header.nonce = isOutOfOrderExecution
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,4 @@ contract FeeQuoter_applyDestChainConfigUpdates is FeeQuoterSetup {
);
s_feeQuoter.applyDestChainConfigUpdates(destChainConfigArgs);
}

function test_RevertWhen_InvalidChainFamilySelector() public {
FeeQuoter.DestChainConfigArgs[] memory destChainConfigArgs = _generateFeeQuoterDestChainConfigArgs();
FeeQuoter.DestChainConfigArgs memory destChainConfigArg = destChainConfigArgs[0];

destChainConfigArg.destChainConfig.chainFamilySelector = bytes4(uint32(1));

vm.expectRevert(
abi.encodeWithSelector(FeeQuoter.InvalidDestChainConfig.selector, destChainConfigArg.destChainSelector)
);
s_feeQuoter.applyDestChainConfigUpdates(destChainConfigArgs);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,22 @@ contract FeeQuoter_getValidatedFee is FeeQuoterFeeSetup {
s_feeQuoter.getValidatedFee(DEST_CHAIN_SELECTOR, message);
}

function test_SolChainFamilySelector() public {
// Update config to enforce allowOutOfOrderExecution = true.
vm.stopPrank();
vm.startPrank(OWNER);

FeeQuoter.DestChainConfigArgs[] memory destChainConfigArgs = _generateFeeQuoterDestChainConfigArgs();
destChainConfigArgs[0].destChainConfig.chainFamilySelector = Internal.CHAIN_FAMILY_SELECTOR_SOL;

s_feeQuoter.applyDestChainConfigUpdates(destChainConfigArgs);
vm.stopPrank();

Client.EVM2AnyMessage memory message = _generateEmptyMessage2Sol();

s_feeQuoter.getValidatedFee(DEST_CHAIN_SELECTOR, message);
}

// Reverts

function test_RevertWhen_DestinationChainNotEnabled() public {
Expand Down
Loading

0 comments on commit 227b5c3

Please sign in to comment.