Skip to content

Commit

Permalink
GasLimitOverride for manuallyExecute (#1375)
Browse files Browse the repository at this point in the history
## Motivation
1. OffRamp should now use destExecData to extract the gas used for 
2. Manual execution should enable user to override the gas amount for
releaseOrMint per token.

## Solution
1. Removed int32 `maxTokenTransferGas` & `maxPoolReleaseOrMintGas` and
used the gas encoded in `RampTokenAmount.destExecData`
2. add a new Struct which holds the receiverExecutionGasLimit and
transferGasAmounts
```
struct GasLimitOverride {
    uint256 receiverExecutionGasLimit;
    uint256[] tokenGasOverrides;
}
```
tokenGasOverrides is an array of GasLimits to be used during the
relaseOrMint call for the specific tokenPool associated with token

Note: 
The gas limit can not be lowered as that could cause the message to
fail. If manual execution is done from an UNTOUCHED state and we would
allow lower gas limit, anyone could grief by executing the message with
lower gas limit than the DON would have used. This results in the
message being marked FAILURE and the DON would not attempt it with the
correct gas limit.

for the above we have kept a check that `tokenGasOverrides` < gas
encoded in `RampTokenAmount.destExecData`

---------

Signed-off-by: 0xsuryansh <[email protected]>
Co-authored-by: app-token-issuer-infra-releng[bot] <120227048+app-token-issuer-infra-releng[bot]@users.noreply.github.com>
  • Loading branch information
1 parent fd212e6 commit 49fde28
Show file tree
Hide file tree
Showing 12 changed files with 517 additions and 326 deletions.
287 changes: 141 additions & 146 deletions contracts/gas-snapshots/ccip.gas-snapshot

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion contracts/scripts/native_solc_compile_all_ccip
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ SOLC_VERSION="0.8.24"
OPTIMIZE_RUNS=26000
OPTIMIZE_RUNS_OFFRAMP=18000
OPTIMIZE_RUNS_ONRAMP=4100
OPTIMIZE_RUNS_MULTI_OFFRAMP=1925
OPTIMIZE_RUNS_MULTI_OFFRAMP=800


SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
Expand Down
98 changes: 71 additions & 27 deletions contracts/src/v0.8/ccip/offRamp/OffRamp.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ contract OffRamp is ITypeAndVersion, MultiOCR3Base {
error UnexpectedTokenData();
error ManualExecutionNotYetEnabled(uint64 sourceChainSelector);
error ManualExecutionGasLimitMismatch();
error InvalidManualExecutionGasLimit(uint64 sourceChainSelector, uint256 index, uint256 newLimit);
error InvalidManualExecutionGasLimit(uint64 sourceChainSelector, bytes32 messageId, uint256 newLimit);
error InvalidManualExecutionTokenGasOverride(
bytes32 messageId, uint256 tokenIndex, uint256 oldLimit, uint256 tokenGasOverride
);
error ManualExecutionGasAmountCountMismatch(bytes32 messageId, uint64 sequenceNumber);
error RootNotCommitted(uint64 sourceChainSelector);
error RootAlreadyCommitted(uint64 sourceChainSelector, bytes32 merkleRoot);
error InvalidRoot();
Expand Down Expand Up @@ -116,9 +120,7 @@ contract OffRamp is ITypeAndVersion, MultiOCR3Base {
/// @dev Since DynamicConfig is part of DynamicConfigSet event, if changing it, we should update the ABI on Atlas
struct DynamicConfig {
address feeQuoter; // ──────────────────────────────╮ FeeQuoter address on the local chain
uint32 permissionLessExecutionThresholdSeconds; // │ Waiting time before manual execution is enabled
uint32 maxTokenTransferGas; // │ Maximum amount of gas passed on to token `transfer` call
uint32 maxPoolReleaseOrMintGas; // ─────────────────╯ Maximum amount of gas passed on to token pool when calling releaseOrMint
uint32 permissionLessExecutionThresholdSeconds; //──╯ Waiting time before manual execution is enabled
address messageValidator; // Optional message validator to validate incoming messages (zero address = no validator)
}

Expand All @@ -130,6 +132,12 @@ contract OffRamp is ITypeAndVersion, MultiOCR3Base {
IRMNV2.Signature[] rmnSignatures; // RMN signatures on the merkle roots
}

struct GasLimitOverride {
// A value of zero in both fields signifies no override and allows the corresponding field to be overridden as valid
uint256 receiverExecutionGasLimit; // Overrides EVM2EVMMessage.gasLimit.
uint32[] tokenGasOverrides; // Overrides EVM2EVMMessage.sourceTokenData.destGasAmount, length must be same as tokenAmounts.
}

// STATIC CONFIG
string public constant override typeAndVersion = "OffRamp 1.6.0-dev";
/// @dev ChainSelector of this chain
Expand Down Expand Up @@ -257,7 +265,7 @@ contract OffRamp is ITypeAndVersion, MultiOCR3Base {
/// The reports do not have to contain all the messages (they can be omitted). Multiple reports can be passed in simultaneously.
function manuallyExecute(
Internal.ExecutionReportSingleChain[] memory reports,
uint256[][] memory gasLimitOverrides
GasLimitOverride[][] memory gasLimitOverrides
) external {
// We do this here because the other _execute path is already covered by MultiOCR3Base.
_whenChainNotForked();
Expand All @@ -269,15 +277,35 @@ contract OffRamp is ITypeAndVersion, MultiOCR3Base {
Internal.ExecutionReportSingleChain memory report = reports[reportIndex];

uint256 numMsgs = report.messages.length;
uint256[] memory msgGasLimitOverrides = gasLimitOverrides[reportIndex];
GasLimitOverride[] memory msgGasLimitOverrides = gasLimitOverrides[reportIndex];
if (numMsgs != msgGasLimitOverrides.length) revert ManualExecutionGasLimitMismatch();

for (uint256 msgIndex = 0; msgIndex < numMsgs; ++msgIndex) {
uint256 newLimit = msgGasLimitOverrides[msgIndex];
uint256 newLimit = msgGasLimitOverrides[msgIndex].receiverExecutionGasLimit;
// Checks to ensure message cannot be executed with less gas than specified.
Internal.Any2EVMRampMessage memory message = report.messages[msgIndex];
if (newLimit != 0) {
if (newLimit < report.messages[msgIndex].gasLimit) {
revert InvalidManualExecutionGasLimit(report.sourceChainSelector, msgIndex, newLimit);
if (newLimit < message.gasLimit) {
revert InvalidManualExecutionGasLimit(report.sourceChainSelector, message.header.messageId, newLimit);
}
}
if (message.tokenAmounts.length != msgGasLimitOverrides[msgIndex].tokenGasOverrides.length) {
revert ManualExecutionGasAmountCountMismatch(message.header.messageId, message.header.sequenceNumber);
}

// The gas limit can not be lowered as that could cause the message to fail. If manual execution is done
// from an UNTOUCHED state and we would allow lower gas limit, anyone could grief by executing the message with
// lower gas limit than the DON would have used. This results in the message being marked FAILURE and the DON
// would not attempt it with the correct gas limit.
for (uint256 tokenIndex = 0; tokenIndex < message.tokenAmounts.length; ++tokenIndex) {
uint256 tokenGasOverride = msgGasLimitOverrides[msgIndex].tokenGasOverrides[tokenIndex];
if (tokenGasOverride != 0) {
uint32 destGasAmount = abi.decode(message.tokenAmounts[tokenIndex].destExecData, (uint32));
if (tokenGasOverride < destGasAmount) {
revert InvalidManualExecutionTokenGasOverride(
message.header.messageId, tokenIndex, destGasAmount, tokenGasOverride
);
}
}
}
}
Expand All @@ -290,7 +318,7 @@ contract OffRamp is ITypeAndVersion, MultiOCR3Base {
/// and expects the exec plugin type to be configured with no signatures.
/// @param report serialized execution report
function execute(bytes32[3] calldata reportContext, bytes calldata report) external {
_batchExecute(abi.decode(report, (Internal.ExecutionReportSingleChain[])), new uint256[][](0));
_batchExecute(abi.decode(report, (Internal.ExecutionReportSingleChain[])), new GasLimitOverride[][](0));

bytes32[] memory emptySigs = new bytes32[](0);
_transmit(uint8(Internal.OCRPluginType.Execution), reportContext, report, emptySigs, emptySigs, bytes32(""));
Expand All @@ -305,30 +333,30 @@ contract OffRamp is ITypeAndVersion, MultiOCR3Base {
/// @dev If called from manual execution, each inner array's length has to match the number of messages.
function _batchExecute(
Internal.ExecutionReportSingleChain[] memory reports,
uint256[][] memory manualExecGasLimits
GasLimitOverride[][] memory manualExecGasOverrides
) internal {
if (reports.length == 0) revert EmptyReport();

bool areManualGasLimitsEmpty = manualExecGasLimits.length == 0;
bool areManualGasLimitsEmpty = manualExecGasOverrides.length == 0;
// Cache array for gas savings in the loop's condition
uint256[] memory emptyGasLimits = new uint256[](0);
GasLimitOverride[] memory emptyGasLimits = new GasLimitOverride[](0);

for (uint256 i = 0; i < reports.length; ++i) {
_executeSingleReport(reports[i], areManualGasLimitsEmpty ? emptyGasLimits : manualExecGasLimits[i]);
_executeSingleReport(reports[i], areManualGasLimitsEmpty ? emptyGasLimits : manualExecGasOverrides[i]);
}
}

/// @notice Executes a report, executing each message in order.
/// @param report The execution report containing the messages and proofs.
/// @param manualExecGasLimits An array of gas limits to use for manual execution.
/// @param manualExecGasExecOverrides An array of gas limits to use for manual execution.
/// @dev If called from the DON, this array is always empty.
/// @dev If called from manual execution, this array is always same length as messages.
function _executeSingleReport(
Internal.ExecutionReportSingleChain memory report,
uint256[] memory manualExecGasLimits
GasLimitOverride[] memory manualExecGasExecOverrides
) internal {
uint64 sourceChainSelector = report.sourceChainSelector;
bool manualExecution = manualExecGasLimits.length != 0;
bool manualExecution = manualExecGasExecOverrides.length != 0;
if (i_rmn.isCursed(bytes16(uint128(sourceChainSelector)))) {
if (manualExecution) {
// For manual execution we don't want to silently fail so we revert
Expand Down Expand Up @@ -396,8 +424,9 @@ contract OffRamp is ITypeAndVersion, MultiOCR3Base {
emit SkippedAlreadyExecutedMessage(sourceChainSelector, message.header.sequenceNumber);
continue;
}

uint32[] memory tokenGasOverrides;
if (manualExecution) {
tokenGasOverrides = manualExecGasExecOverrides[i].tokenGasOverrides;
bool isOldCommitReport =
(block.timestamp - timestampCommitted) > s_dynamicConfig.permissionLessExecutionThresholdSeconds;
// Manually execution is fine if we previously failed or if the commit report is just too old
Expand All @@ -407,8 +436,8 @@ contract OffRamp is ITypeAndVersion, MultiOCR3Base {
}

// Manual execution gas limit can override gas limit specified in the message. Value of 0 indicates no override.
if (manualExecGasLimits[i] != 0) {
message.gasLimit = manualExecGasLimits[i];
if (manualExecGasExecOverrides[i].receiverExecutionGasLimit != 0) {
message.gasLimit = manualExecGasExecOverrides[i].receiverExecutionGasLimit;
}
} else {
// DON can only execute a message once
Expand Down Expand Up @@ -445,7 +474,8 @@ contract OffRamp is ITypeAndVersion, MultiOCR3Base {
}

_setExecutionState(sourceChainSelector, message.header.sequenceNumber, Internal.MessageExecutionState.IN_PROGRESS);
(Internal.MessageExecutionState newState, bytes memory returnData) = _trialExecute(message, offchainTokenData);
(Internal.MessageExecutionState newState, bytes memory returnData) =
_trialExecute(message, offchainTokenData, tokenGasOverrides);
_setExecutionState(sourceChainSelector, message.header.sequenceNumber, newState);

// Since it's hard to estimate whether manual execution will succeed, we
Expand Down Expand Up @@ -490,9 +520,10 @@ contract OffRamp is ITypeAndVersion, MultiOCR3Base {
/// @return errData Revert data in bytes if CCIP receiver reverted during execution.
function _trialExecute(
Internal.Any2EVMRampMessage memory message,
bytes[] memory offchainTokenData
bytes[] memory offchainTokenData,
uint32[] memory tokenGasOverrides
) internal returns (Internal.MessageExecutionState executionState, bytes memory) {
try this.executeSingleMessage(message, offchainTokenData) {}
try this.executeSingleMessage(message, offchainTokenData, tokenGasOverrides) {}
catch (bytes memory err) {
// return the message execution state as FAILURE and the revert data
// Max length of revert data is Router.MAX_RET_BYTES, max length of err is 4 + Router.MAX_RET_BYTES
Expand All @@ -511,13 +542,19 @@ contract OffRamp is ITypeAndVersion, MultiOCR3Base {
/// (for example smart contract wallets) without an associated message.
function executeSingleMessage(
Internal.Any2EVMRampMessage memory message,
bytes[] calldata offchainTokenData
bytes[] calldata offchainTokenData,
uint32[] calldata tokenGasOverrides
) external {
if (msg.sender != address(this)) revert CanOnlySelfCall();
Client.EVMTokenAmount[] memory destTokenAmounts = new Client.EVMTokenAmount[](0);
if (message.tokenAmounts.length > 0) {
destTokenAmounts = _releaseOrMintTokens(
message.tokenAmounts, message.sender, message.receiver, message.header.sourceChainSelector, offchainTokenData
message.tokenAmounts,
message.sender,
message.receiver,
message.header.sourceChainSelector,
offchainTokenData,
tokenGasOverrides
);
}

Expand Down Expand Up @@ -827,7 +864,7 @@ contract OffRamp is ITypeAndVersion, MultiOCR3Base {

// We retrieve the local token balance of the receiver before the pool call.
(uint256 balancePre, uint256 gasLeft) =
_getBalanceOfReceiver(receiver, localToken, s_dynamicConfig.maxPoolReleaseOrMintGas);
_getBalanceOfReceiver(receiver, localToken, abi.decode(sourceTokenAmount.destExecData, (uint32)));

// We determined that the pool address is a valid EVM address, but that does not mean the code at this
// address is a (compatible) pool contract. _callWithExactGasSafeReturnData will check if the location
Expand Down Expand Up @@ -923,10 +960,17 @@ contract OffRamp is ITypeAndVersion, MultiOCR3Base {
bytes memory originalSender,
address receiver,
uint64 sourceChainSelector,
bytes[] calldata offchainTokenData
bytes[] calldata offchainTokenData,
uint32[] calldata tokenGasOverrides
) internal returns (Client.EVMTokenAmount[] memory destTokenAmounts) {
destTokenAmounts = new Client.EVMTokenAmount[](sourceTokenAmounts.length);
bool isTokenGasOverridesEmpty = tokenGasOverrides.length == 0;
for (uint256 i = 0; i < sourceTokenAmounts.length; ++i) {
if (!isTokenGasOverridesEmpty) {
if (tokenGasOverrides[i] != 0) {
sourceTokenAmounts[i].destExecData = abi.encode(tokenGasOverrides[i]);
}
}
destTokenAmounts[i] = _releaseOrMintSingleToken(
sourceTokenAmounts[i], originalSender, receiver, sourceChainSelector, offchainTokenData[i]
);
Expand Down
20 changes: 13 additions & 7 deletions contracts/src/v0.8/ccip/test/NonceManager.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,9 @@ contract NonceManager_OffRampUpgrade is OffRampSetup {
_generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1);

vm.recordLogs();
s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0));
s_offRamp.executeSingleReport(
_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new OffRamp.GasLimitOverride[](0)
);
assertExecutionStateChangedEventLogs(
SOURCE_CHAIN_SELECTOR_1,
messages[0].header.sequenceNumber,
Expand Down Expand Up @@ -421,7 +423,7 @@ contract NonceManager_OffRampUpgrade is OffRampSetup {
vm.recordLogs();

s_offRamp.executeSingleReport(
_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_3, messagesChain3), new uint256[](0)
_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_3, messagesChain3), new OffRamp.GasLimitOverride[](0)
);
assertExecutionStateChangedEventLogs(
SOURCE_CHAIN_SELECTOR_3,
Expand Down Expand Up @@ -502,7 +504,7 @@ contract NonceManager_OffRampUpgrade is OffRampSetup {
vm.recordLogs();

s_offRamp.executeSingleReport(
_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messagesMultiRamp), new uint256[](0)
_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messagesMultiRamp), new OffRamp.GasLimitOverride[](0)
);

assertExecutionStateChangedEventLogs(
Expand All @@ -524,7 +526,7 @@ contract NonceManager_OffRampUpgrade is OffRampSetup {

vm.recordLogs();
s_offRamp.executeSingleReport(
_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messagesMultiRamp), new uint256[](0)
_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messagesMultiRamp), new OffRamp.GasLimitOverride[](0)
);
assertExecutionStateChangedEventLogs(
SOURCE_CHAIN_SELECTOR_1,
Expand Down Expand Up @@ -559,7 +561,7 @@ contract NonceManager_OffRampUpgrade is OffRampSetup {
assertEq(s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, newSender), 0);
vm.recordLogs();
s_offRamp.executeSingleReport(
_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messagesMultiRamp), new uint256[](0)
_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messagesMultiRamp), new OffRamp.GasLimitOverride[](0)
);
assertExecutionStateChangedEventLogs(
SOURCE_CHAIN_SELECTOR_1,
Expand Down Expand Up @@ -587,7 +589,9 @@ contract NonceManager_OffRampUpgrade is OffRampSetup {
// it waits for previous offramp to execute
vm.expectEmit();
emit NonceManager.SkippedIncorrectNonce(SOURCE_CHAIN_SELECTOR_1, messages[0].header.nonce, messages[0].sender);
s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0));
s_offRamp.executeSingleReport(
_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new OffRamp.GasLimitOverride[](0)
);
assertEq(startNonce, s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messages[0].sender));

Internal.EVM2EVMMessage[] memory messagesSingleLane =
Expand All @@ -611,7 +615,9 @@ contract NonceManager_OffRampUpgrade is OffRampSetup {

// new offramp is able to execute
vm.recordLogs();
s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0));
s_offRamp.executeSingleReport(
_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new OffRamp.GasLimitOverride[](0)
);
assertExecutionStateChangedEventLogs(
SOURCE_CHAIN_SELECTOR_1,
messages[0].header.sequenceNumber,
Expand Down
4 changes: 4 additions & 0 deletions contracts/src/v0.8/ccip/test/e2e/MultiRampsEnd2End.sol
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,10 @@ contract MultiRampsE2E is OnRampSetup, OffRampSetup {
router.ccipSend(DEST_CHAIN_SELECTOR, message);
vm.pauseGasMetering();

uint256 gasLimit = s_feeQuoter.parseEVMExtraArgsFromBytes(msgEvent.extraArgs, DEST_CHAIN_SELECTOR).gasLimit;
for (uint256 i = 0; i < msgEvent.tokenAmounts.length; ++i) {
msgEvent.tokenAmounts[i].destExecData = abi.encode(MAX_TOKEN_POOL_RELEASE_OR_MINT_GAS);
}
return Internal.Any2EVMRampMessage({
header: Internal.RampMessageHeader({
messageId: msgEvent.header.messageId,
Expand Down
18 changes: 11 additions & 7 deletions contracts/src/v0.8/ccip/test/helpers/OffRampHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,28 +43,32 @@ contract OffRampHelper is OffRamp, IgnoreContractSize {
bytes calldata originalSender,
address receiver,
uint64 sourceChainSelector,
bytes[] calldata offchainTokenData
bytes[] calldata offchainTokenData,
uint32[] calldata tokenGasOverrides
) external returns (Client.EVMTokenAmount[] memory) {
return _releaseOrMintTokens(sourceTokenAmounts, originalSender, receiver, sourceChainSelector, offchainTokenData);
return _releaseOrMintTokens(
sourceTokenAmounts, originalSender, receiver, sourceChainSelector, offchainTokenData, tokenGasOverrides
);
}

function trialExecute(
Internal.Any2EVMRampMessage memory message,
bytes[] memory offchainTokenData
bytes[] memory offchainTokenData,
uint32[] memory tokenGasOverrides
) external returns (Internal.MessageExecutionState, bytes memory) {
return _trialExecute(message, offchainTokenData);
return _trialExecute(message, offchainTokenData, tokenGasOverrides);
}

function executeSingleReport(
Internal.ExecutionReportSingleChain memory rep,
uint256[] memory manualExecGasLimits
GasLimitOverride[] memory manualExecGasExecOverrides
) external {
_executeSingleReport(rep, manualExecGasLimits);
_executeSingleReport(rep, manualExecGasExecOverrides);
}

function batchExecute(
Internal.ExecutionReportSingleChain[] memory reports,
uint256[][] memory manualExecGasLimits
GasLimitOverride[][] memory manualExecGasLimits
) external {
_batchExecute(reports, manualExecGasLimits);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ contract ReentrancyAbuserMultiRamp is CCIPReceiver {
function _ccipReceive(Client.Any2EVMMessage memory) internal override {
// Use original message gas limits in manual execution
uint256 numMsgs = s_payload.messages.length;
uint256[][] memory gasOverrides = new uint256[][](1);
gasOverrides[0] = new uint256[](numMsgs);
OffRamp.GasLimitOverride[][] memory gasOverrides = new OffRamp.GasLimitOverride[][](1);
gasOverrides[0] = new OffRamp.GasLimitOverride[](numMsgs);
for (uint256 i = 0; i < numMsgs; ++i) {
gasOverrides[0][i] = 0;
gasOverrides[0][i].receiverExecutionGasLimit = 0;
gasOverrides[0][i].tokenGasOverrides = new uint32[](s_payload.messages[i].tokenAmounts.length);
}

Internal.ExecutionReportSingleChain[] memory batchPayload = new Internal.ExecutionReportSingleChain[](1);
Expand Down
Loading

0 comments on commit 49fde28

Please sign in to comment.