Skip to content

Commit

Permalink
Finish provisional tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jhweintraub committed Dec 27, 2024
1 parent 06c66ae commit f119246
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 206 deletions.
5 changes: 5 additions & 0 deletions contracts/.changeset/tame-worms-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/contracts': minor
---

Add support for Solana in the Fee Quoter with new extra args tag and chain family selector #feature
199 changes: 102 additions & 97 deletions contracts/gas-snapshots/ccip.gas-snapshot

Large diffs are not rendered by default.

101 changes: 39 additions & 62 deletions contracts/src/v0.8/ccip/FeeQuoter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +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 SolAddressCannotBeWritable(bytes32 SolAddress);
error CannotUpdateChainFamilySelector(bytes4 chainFamilySelector);
error InvalidChainFamilySelector(bytes4 chainFamilySelector);
error SolExtraArgsAccountsCannotBeZero();
error FirstSolExtraArgsAddressCannotBeWritable();
error SolExtraArgsMustBeProvided();

event FeeTokenAdded(address indexed feeToken);
event FeeTokenRemoved(address indexed feeToken);
Expand Down Expand Up @@ -876,36 +874,39 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
DestChainConfig memory destChainConfig
) internal pure returns (uint256 gasLimit) {
if (destChainConfig.chainFamilySelector == Internal.CHAIN_FAMILY_SELECTOR_EVM) {
return _parseEVMExtraArgsFromBytes(extraArgs, destChainConfig).gasLimit;
gasLimit = _parseEVMExtraArgsFromBytes(extraArgs, destChainConfig).gasLimit;
} else if (destChainConfig.chainFamilySelector == Internal.CHAIN_FAMILY_SELECTOR_SOL) {
// If extra args are empty, generate default values.
return _parseSolExtraArgsFromBytes(extraArgs, destChainConfig, 0).computeUnits;
gasLimit = _parseSolExtraArgsFromBytes(extraArgs, destChainConfig, 0).computeUnits;
} else {
gasLimit = destChainConfig.defaultTxGasLimit;
}

return gasLimit;
}

/// @notice Parse and validate the Solana specific Extra Args Bytes.
/// TODO: Finish
/// @param messageLengthBytes The length of the arbitrary message data. If this is non-zero, then an extraArgs
/// @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 messageLengthBytes
uint256 messageDataLengthBytes
) internal pure returns (Client.SolExtraArgsV1 memory) {
Client.SolExtraArgsV1 memory SolExtraArgs =
_parseUnvalidatedSolExtraArgsFromBytes(extraArgs, destChainConfig.defaultTxGasLimit);

if (messageLengthBytes != 0 && SolExtraArgs.accounts.length == 0) revert SolExtraArgsAccountsCannotBeZero();

// Check that compute units is within the allowed range.
if (SolExtraArgs.computeUnits > uint256(destChainConfig.maxPerMsgGasLimit)) revert MessageGasLimitTooHigh();
// 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 cannot be writable. If no accounts are provided as extra args, the account
// length check is skipped to prevent an index-out-of-bounds error.
// 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 SolAddressCannotBeWritable(SolExtraArgs.accounts[0].pubKey);
revert FirstSolExtraArgsAddressCannotBeWritable();
}

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

return SolExtraArgs;
}

Expand Down Expand Up @@ -1001,10 +1002,10 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,

/// @inheritdoc IFeeQuoter
/// @dev precondition - onRampTokenTransfers and sourceTokenAmounts lengths must be equal.
/// @param message The arbitrary bytes to be processed. Solana address requirements require that if an arbitrary message
/// @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 message,
bytes calldata messageData,
uint64 destChainSelector,
address feeToken,
uint256 feeTokenAmount,
Expand All @@ -1021,11 +1022,10 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
bytes[] memory destExecDataPerToken
)
{

// Prevents a stack too deep error on messageData
{
bytes memory messageData = message;
(convertedExtraArgs, isOutOfOrderExecution) = _processChainFamilySelector(destChainSelector, messageData, extraArgs);
bytes memory data = messageData;
(convertedExtraArgs, isOutOfOrderExecution) = _processChainFamilySelector(destChainSelector, data, extraArgs);
}

// Convert feeToken to link if not already in link.
Expand All @@ -1048,30 +1048,27 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
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);
) 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);
convertedExtraArgs = Client._argsToBytes(parsedExtraArgs);

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

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

convertedExtraArgs = Client._solArgsToBytes(parsedExtraArgs);
// On Solana OOO execution is enabled for all messages.
isOutOfOrderExecution = true;
}

// On Solana OOO execution is enabled for all messages.
isOutOfOrderExecution = true;
} else {
revert InvalidChainFamilySelector(destChainConfig.chainFamilySelector);
}
return (convertedExtraArgs, isOutOfOrderExecution);
}

/// @notice Validates pool return data.
Expand Down Expand Up @@ -1146,11 +1143,11 @@ 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, must be less than maxPerMsgGasLimit, and the chain family selector must be valid.
// destChainSelector must be non-zero, defaultTxGasLimit must be set, must be less than maxPerMsgGasLimit
// Note: With the addition of Solana and other Non-evm Chains, family selector is not validated.
if (
destChainSelector == 0 || destChainConfig.defaultTxGasLimit == 0
|| destChainConfig.defaultTxGasLimit > destChainConfig.maxPerMsgGasLimit
|| !s_validchainFamilySelectors.contains(bytes32(destChainConfig.chainFamilySelector))
) {
revert InvalidDestChainConfig(destChainSelector);
}
Expand All @@ -1167,26 +1164,6 @@ contract FeeQuoter is AuthorizedCallers, IFeeQuoter, ITypeAndVersion, IReceiver,
}
}

/// @notice Updates the system to accept a new chain family selector.
/// @dev The validity of a chain family selector affects the ability to add new chains in the applyDestChainConfigUpdates function.
/// @param removes Chain family selectors to remove.
/// @param adds Chain family selectors to add.
function updateChainFamilySelectors(bytes4[] memory removes, bytes4[] memory adds) external onlyOwner {
for (uint256 i = 0; i < removes.length; ++i) {
if (!s_validchainFamilySelectors.remove(bytes32(removes[i]))) {
revert CannotUpdateChainFamilySelector(removes[i]);
}
emit ChainFamilySelectorModified(removes[i], false);
}

for (uint256 i = 0; i < adds.length; ++i) {
if (!s_validchainFamilySelectors.add(bytes32(adds[i]))) {
revert CannotUpdateChainFamilySelector(adds[i]);
}
emit ChainFamilySelectorModified(adds[i], true);
}
}

/// @notice Returns the static FeeQuoter config.
/// @dev RMN depends on this function, if updated, please notify the RMN maintainers.
/// @return staticConfig The static configuration.
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,23 +201,21 @@ contract FeeQuoter_getValidatedFee is FeeQuoterFeeSetup {
s_feeQuoter.getValidatedFee(DEST_CHAIN_SELECTOR, message);
}

// TODO: Finish
// function test_SolChainFamilySelector() public {
// // Update config to enforce allowOutOfOrderExecution = true.
// vm.stopPrank();
// vm.startPrank(OWNER);
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;
FeeQuoter.DestChainConfigArgs[] memory destChainConfigArgs = _generateFeeQuoterDestChainConfigArgs();
destChainConfigArgs[0].destChainConfig.chainFamilySelector = Internal.CHAIN_FAMILY_SELECTOR_SOL;

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

// Client.EVM2AnyMessage memory message = _generateEmptyMessage();
// // Empty extraArgs to should revert since it enforceOutOfOrder is true.
Client.EVM2AnyMessage memory message = _generateEmptyMessage2Sol();

// s_feeQuoter.getValidatedFee(DEST_CHAIN_SELECTOR, message);
// }
s_feeQuoter.getValidatedFee(DEST_CHAIN_SELECTOR, message);
}

// Reverts

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@ contract FeeQuoter_parseSolExtraArgsFromBytes is FeeQuoterSetup {

/// @dev a Valid pubkey is one that is 32 bytes long, and that's it since no other validation can be performed
/// within the constraints of the EVM.
bytes32 internal constant VALID_SOL_PUBKEY = keccak256("valid_sol_pubkey");
bytes32 internal constant VALID_SOL_PUBKEY = keccak256("SOL_PUBKEY");

function setUp() public virtual override {
super.setUp();
s_destChainConfig = _generateFeeQuoterDestChainConfigArgs()[0].destChainConfig;
s_destChainConfig.chainFamilySelector = Internal.CHAIN_FAMILY_SELECTOR_SOL;

FeeQuoter.DestChainConfigArgs[] memory destChainConfigs = new FeeQuoter.DestChainConfigArgs[](1);
destChainConfigs[0] =
FeeQuoter.DestChainConfigArgs({destChainSelector: DEST_CHAIN_SELECTOR, destChainConfig: s_destChainConfig});
s_feeQuoter.applyDestChainConfigUpdates(destChainConfigs);
}

function test_SolExtraArgsV1() public view {
Expand Down Expand Up @@ -47,13 +52,48 @@ contract FeeQuoter_parseSolExtraArgsFromBytes is FeeQuoterSetup {
});

vm.assertEq(
abi.encode(s_feeQuoter.parseSOLExtraArgsFromBytes("", s_destChainConfig, messageDataLengthBytes)), abi.encode(expectedOutputArgs)
abi.encode(s_feeQuoter.parseSOLExtraArgsFromBytes("", s_destChainConfig, messageDataLengthBytes)),
abi.encode(expectedOutputArgs)
);
}

function test_parseGasLimitFromExtraArgBytes_defaultTxGasLimit() public {
// Need to apply a chain family selector that does not have an explicit extraArgs parser available
FeeQuoter.DestChainConfigArgs[] memory destChainConfigArgs = new FeeQuoter.DestChainConfigArgs[](1);
destChainConfigArgs[0] = _generateFeeQuoterDestChainConfigArgs()[0];
destChainConfigArgs[0].destChainConfig.isEnabled = false;
destChainConfigArgs[0].destChainConfig.chainFamilySelector = bytes4(0xdeadbeef);

s_feeQuoter.applyDestChainConfigUpdates(destChainConfigArgs);

uint256 defaultTxGasLimit = s_destChainConfig.defaultTxGasLimit;
uint256 gasLimit = s_feeQuoter.parseGasLimitFromExtraArgBytes("", s_destChainConfig);
vm.assertEq(gasLimit, defaultTxGasLimit);
}

// Reverts

function test_SolExtraArgsV1_RevertWhen_SolAddressCannotBeWritable() public {}
function test_SolExtraArgsV1_RevertWhen_SolExtraArgsAccountsCannotBeZero() public {
bytes memory extraArgs = Client._solArgsToBytes(
Client.SolExtraArgsV1({computeUnits: GAS_LIMIT, accounts: new Client.SolanaAccountMeta[](0)})
);

vm.expectRevert(FeeQuoter.SolExtraArgsMustBeProvided.selector);

s_feeQuoter.parseSOLExtraArgsFromBytes(extraArgs, s_destChainConfig, 1);
}

function test_SolExtraArgsV1_RevertWhen_SolAddressCannotBeWritable() public {
Client.SolanaAccountMeta[] memory solAccounts = new Client.SolanaAccountMeta[](1);
solAccounts[0] = Client.SolanaAccountMeta({pubKey: VALID_SOL_PUBKEY, isWritable: true});

bytes memory extraArgs =
Client._solArgsToBytes(Client.SolExtraArgsV1({computeUnits: GAS_LIMIT, accounts: solAccounts}));

vm.expectRevert(FeeQuoter.FirstSolExtraArgsAddressCannotBeWritable.selector);

s_feeQuoter.parseSOLExtraArgsFromBytes(extraArgs, s_destChainConfig, 1);
}

function test_SolExtraArgsV1_RevertWhen_MessageGasLimitTooHigh() public {
Client.SolExtraArgsV1 memory inputArgs = Client.SolExtraArgsV1({
Expand All @@ -70,23 +110,4 @@ contract FeeQuoter_parseSolExtraArgsFromBytes is FeeQuoterSetup {
vm.expectRevert(FeeQuoter.MessageGasLimitTooHigh.selector);
s_feeQuoter.parseSOLExtraArgsFromBytes(inputExtraArgs, s_destChainConfig, messageDataLengthBytes);
}

// function test_RevertWhen_SolExtraArgsEnforceOutOfOrder() public {
// Client.EVMExtraArgsV2 memory inputArgs =
// Client.EVMExtraArgsV2({gasLimit: GAS_LIMIT, allowOutOfOrderExecution: false});
// bytes memory inputExtraArgs = Client._argsToBytes(inputArgs);
// s_destChainConfig.enforceOutOfOrder = true;

// vm.expectRevert(FeeQuoter.ExtraArgOutOfOrderExecutionMustBeTrue.selector);
// s_feeQuoter.parseEVMExtraArgsFromBytes(inputExtraArgs, s_destChainConfig);
// }

// function test_RevertWhen_SolExtraArgsGasLimitTooHigh() public {
// Client.EVMExtraArgsV2 memory inputArgs =
// Client.EVMExtraArgsV2({gasLimit: s_destChainConfig.maxPerMsgGasLimit + 1, allowOutOfOrderExecution: true});
// bytes memory inputExtraArgs = Client._argsToBytes(inputArgs);

// vm.expectRevert(FeeQuoter.MessageGasLimitTooHigh.selector);
// s_feeQuoter.parseEVMExtraArgsFromBytes(inputExtraArgs, s_destChainConfig);
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,42 @@ contract FeeQuoter_processMessageArgs is FeeQuoterFeeSetup {
);
}

function test_processMessageArgs_WithSolExtraArgsV1() public {
// Apply the chain update to set the chain family selector to SOL
FeeQuoter.DestChainConfig memory s_destChainConfig = _generateFeeQuoterDestChainConfigArgs()[0].destChainConfig;
s_destChainConfig.chainFamilySelector = Internal.CHAIN_FAMILY_SELECTOR_SOL;

FeeQuoter.DestChainConfigArgs[] memory destChainConfigs = new FeeQuoter.DestChainConfigArgs[](1);
destChainConfigs[0] =
FeeQuoter.DestChainConfigArgs({destChainSelector: DEST_CHAIN_SELECTOR, destChainConfig: s_destChainConfig});
s_feeQuoter.applyDestChainConfigUpdates(destChainConfigs);

bytes memory extraArgs =
Client._solArgsToBytes(Client.SolExtraArgsV1({computeUnits: 0, accounts: new Client.SolanaAccountMeta[](0)}));

(
/* uint256 msgFeeJuels */
,
bool isOutOfOrderExecution,
bytes memory convertedExtraArgs,
/* destExecDataPerToken */
) = s_feeQuoter.processMessageArgs(
"",
DEST_CHAIN_SELECTOR,
s_sourceTokens[0],
0,
extraArgs,
new Internal.EVM2AnyTokenTransfer[](0),
new Client.EVMTokenAmount[](0)
);

assertTrue(isOutOfOrderExecution);
assertEq(
convertedExtraArgs,
Client._solArgsToBytes(s_feeQuoter.parseSOLExtraArgsFromBytes(extraArgs, s_destChainConfig, 0))
);
}

// Reverts

function test_RevertWhen_processMessageArgs_MessageFeeTooHigh() public {
Expand Down
13 changes: 13 additions & 0 deletions contracts/src/v0.8/ccip/test/feeQuoter/FeeQuoterSetup.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,19 @@ contract FeeQuoterFeeSetup is FeeQuoterSetup {
});
}

// Used to generate a message with a specific extraArgs tag for Solana
function _generateEmptyMessage2Sol() public view returns (Client.EVM2AnyMessage memory) {
return Client.EVM2AnyMessage({
receiver: abi.encode(OWNER),
data: "",
tokenAmounts: new Client.EVMTokenAmount[](0),
feeToken: s_sourceFeeToken,
extraArgs: Client._solArgsToBytes(
Client.SolExtraArgsV1({computeUnits: GAS_LIMIT, accounts: new Client.SolanaAccountMeta[](0)})
)
});
}

function _generateSingleTokenMessage(
address token,
uint256 amount
Expand Down
Loading

0 comments on commit f119246

Please sign in to comment.