Skip to content

Commit

Permalink
Manual-execute-gas-overrides (#1256)
Browse files Browse the repository at this point in the history
## Motivation
currently, gas amounts are placed in the SourceTokenData. However, for
the multi-ramps, the token data should be chain-family agnostic, which
will likely require the destGasAmount lift to another field



## Solution

- add a new Struct which holds the receiverExecutionGasLimit and
transferGasAmounts

```js
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

---------

Signed-off-by: 0xsuryansh <suryansh.shrivastava@smartcontract.com>
Co-authored-by: app-token-issuer-infra-releng[bot] <120227048+app-token-issuer-infra-releng[bot]@users.noreply.github.com>
Co-authored-by: 0xsuryansh <suryansh.shrivastava@smartcontract.com>
Co-authored-by: Rens Rooimans <github@rensrooimans.nl>
  • Loading branch information
4 people authored Aug 9, 2024
1 parent 0ae527c commit 1cfd5e7
Show file tree
Hide file tree
Showing 16 changed files with 619 additions and 298 deletions.
186 changes: 96 additions & 90 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 @@ -8,7 +8,7 @@ echo " └───────────────────────

SOLC_VERSION="0.8.24"
OPTIMIZE_RUNS=26000
OPTIMIZE_RUNS_OFFRAMP=22000
OPTIMIZE_RUNS_OFFRAMP=18000
OPTIMIZE_RUNS_ONRAMP=4100
OPTIMIZE_RUNS_MULTI_OFFRAMP=2000

Expand Down
95 changes: 77 additions & 18 deletions contracts/src/v0.8/ccip/offRamp/EVM2EVMOffRamp.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio
error UnsupportedNumberOfTokens(uint64 sequenceNumber);
error ManualExecutionNotYetEnabled();
error ManualExecutionGasLimitMismatch();
error InvalidManualExecutionGasLimit(uint256 index, uint256 newLimit);
error DestinationGasAmountCountMismatch(bytes32 messageId, uint64 sequenceNumber);
error InvalidManualExecutionGasLimit(bytes32 messageId, uint256 oldLimit, uint256 newLimit);
error InvalidTokenGasOverride(bytes32 messageId, uint256 tokenIndex, uint256 oldLimit, uint256 tokenGasOverride);
error RootNotCommitted();
error CanOnlySelfCall();
error ReceiverError(bytes err);
Expand Down Expand Up @@ -100,6 +102,15 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio
address destToken;
}

/// @notice Gas overrides for manual exec, the number of token overrides must match the number of tokens in the msg.
struct GasLimitOverride {
/// @notice Overrides EVM2EVMMessage.gasLimit. A value of zero indicates no override and is valid.
uint256 receiverExecutionGasLimit;
/// @notice Overrides EVM2EVMMessage.sourceTokenData.destGasAmount. Must be same length as tokenAmounts. A value
/// of zero indicates no override and is valid.
uint32[] tokenGasOverrides;
}

// STATIC CONFIG
string public constant override typeAndVersion = "EVM2EVMOffRamp 1.5.0-dev";

Expand Down Expand Up @@ -218,18 +229,44 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio
/// @param gasLimitOverrides New gasLimit for each message in the report.
/// @dev We permit gas limit overrides so that users may manually execute messages which failed due to
/// insufficient gas provided.
function manuallyExecute(Internal.ExecutionReport memory report, uint256[] memory gasLimitOverrides) external {
function manuallyExecute(
Internal.ExecutionReport memory report,
GasLimitOverride[] memory gasLimitOverrides
) external {
// We do this here because the other _execute path is already covered OCR2BaseXXX.
_checkChainForked();

uint256 numMsgs = report.messages.length;
if (numMsgs != gasLimitOverrides.length) revert ManualExecutionGasLimitMismatch();
for (uint256 i = 0; i < numMsgs; ++i) {
uint256 newLimit = gasLimitOverrides[i];
Internal.EVM2EVMMessage memory message = report.messages[i];
GasLimitOverride memory gasLimitOverride = gasLimitOverrides[i];

uint256 newLimit = gasLimitOverride.receiverExecutionGasLimit;
// Checks to ensure message cannot be executed with less gas than specified.
if (newLimit != 0) {
if (newLimit < report.messages[i].gasLimit) {
revert InvalidManualExecutionGasLimit(i, newLimit);
if (newLimit < message.gasLimit) {
revert InvalidManualExecutionGasLimit(message.messageId, message.gasLimit, newLimit);
}
}

if (message.tokenAmounts.length != gasLimitOverride.tokenGasOverrides.length) {
revert DestinationGasAmountCountMismatch(message.messageId, message.sequenceNumber);
}

bytes[] memory encodedSourceTokenData = message.sourceTokenData;

for (uint256 j = 0; j < message.tokenAmounts.length; ++j) {
Internal.SourceTokenData memory sourceTokenData =
abi.decode(encodedSourceTokenData[i], (Internal.SourceTokenData));
uint256 tokenGasOverride = gasLimitOverride.tokenGasOverrides[j];

// 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.
if (tokenGasOverride != 0 && tokenGasOverride < sourceTokenData.destGasAmount) {
revert InvalidTokenGasOverride(message.messageId, j, sourceTokenData.destGasAmount, tokenGasOverride);
}
}
}
Expand All @@ -239,16 +276,17 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio

/// @notice Entrypoint for execution, called by the OCR network
/// @dev Expects an encoded ExecutionReport
/// @dev Supplies no GasLimitOverrides as the DON will only execute with the original gas limits.
function _report(bytes calldata report) internal override {
_execute(abi.decode(report, (Internal.ExecutionReport)), new uint256[](0));
_execute(abi.decode(report, (Internal.ExecutionReport)), new GasLimitOverride[](0));
}

/// @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 manualExecGasOverrides 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 _execute(Internal.ExecutionReport memory report, uint256[] memory manualExecGasLimits) internal {
function _execute(Internal.ExecutionReport memory report, GasLimitOverride[] memory manualExecGasOverrides) internal {
if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(i_sourceChainSelector)))) revert CursedByRMN();

uint256 numMsgs = report.messages.length;
Expand All @@ -267,13 +305,13 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio
// a message with an unexpected hash.
if (hashedLeaves[i] != message.messageId) revert InvalidMessageId();
}
bool manualExecution = manualExecGasOverrides.length != 0;

// SECURITY CRITICAL CHECK
uint256 timestampCommitted = ICommitStore(i_commitStore).verify(hashedLeaves, report.proofs, report.proofFlagBits);
if (timestampCommitted == 0) revert RootNotCommitted();

// Execute messages
bool manualExecution = manualExecGasLimits.length != 0;
for (uint256 i = 0; i < numMsgs; ++i) {
Internal.EVM2EVMMessage memory message = report.messages[i];
Internal.MessageExecutionState originalState = getExecutionState(message.sequenceNumber);
Expand All @@ -292,8 +330,10 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio
emit SkippedAlreadyExecutedMessage(message.sequenceNumber);
continue;
}
uint32[] memory tokenGasOverrides;

if (manualExecution) {
tokenGasOverrides = manualExecGasOverrides[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 @@ -303,8 +343,8 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio
}

// 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 (manualExecGasOverrides[i].receiverExecutionGasLimit != 0) {
message.gasLimit = manualExecGasOverrides[i].receiverExecutionGasLimit;
}
} else {
// DON can only execute a message once
Expand Down Expand Up @@ -361,7 +401,8 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio
);

_setExecutionState(message.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(message.sequenceNumber, newState);

// Since it's hard to estimate whether manual execution will succeed, we
Expand Down Expand Up @@ -432,9 +473,10 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio
/// @return revert data in bytes if CCIP receiver reverted during execution.
function _trialExecute(
Internal.EVM2EVMMessage memory message,
bytes[] memory offchainTokenData
bytes[] memory offchainTokenData,
uint32[] memory tokenGasOverrides
) internal returns (Internal.MessageExecutionState, 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 @@ -451,12 +493,21 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio
/// its execution and enforce atomicity among successful message processing and token transfer.
/// @dev We use ERC-165 to check for the ccipReceive interface to permit sending tokens to contracts
/// (for example smart contract wallets) without an associated message.
function executeSingleMessage(Internal.EVM2EVMMessage calldata message, bytes[] calldata offchainTokenData) external {
function executeSingleMessage(
Internal.EVM2EVMMessage calldata message,
bytes[] calldata offchainTokenData,
uint32[] memory 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, abi.encode(message.sender), message.receiver, message.sourceTokenData, offchainTokenData
message.tokenAmounts,
abi.encode(message.sender),
message.receiver,
message.sourceTokenData,
offchainTokenData,
tokenGasOverrides
);
}
// There are three cases in which we skip calling the receiver:
Expand Down Expand Up @@ -701,19 +752,27 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio
bytes memory originalSender,
address receiver,
bytes[] calldata encodedSourceTokenData,
bytes[] calldata offchainTokenData
bytes[] calldata offchainTokenData,
uint32[] memory tokenGasOverrides
) internal returns (Client.EVMTokenAmount[] memory destTokenAmounts) {
// Creating a copy is more gas efficient than initializing a new array.
destTokenAmounts = sourceTokenAmounts;
uint256 value = 0;
for (uint256 i = 0; i < sourceTokenAmounts.length; ++i) {
Internal.SourceTokenData memory sourceTokenData =
abi.decode(encodedSourceTokenData[i], (Internal.SourceTokenData));
if (tokenGasOverrides.length != 0) {
if (tokenGasOverrides[i] != 0) {
sourceTokenData.destGasAmount = tokenGasOverrides[i];
}
}
destTokenAmounts[i] = _releaseOrMintToken(
sourceTokenAmounts[i].amount,
originalSender,
receiver,
// This should never revert as the onRamp encodes the sourceTokenData struct. Only the inner components from
// this struct come from untrusted sources.
abi.decode(encodedSourceTokenData[i], (Internal.SourceTokenData)),
sourceTokenData,
offchainTokenData[i]
);

Expand Down
24 changes: 18 additions & 6 deletions contracts/src/v0.8/ccip/test/NonceManager.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,9 @@ contract NonceManager_OffRampUpgrade is EVM2EVMMultiOffRampSetup {
_generateSingleLaneSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, SINGLE_LANE_ON_RAMP_ADDRESS_1);
uint64 startNonceChain3 =
s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_3, abi.encode(messages[0].sender));
s_prevOffRamp.execute(_generateSingleLaneRampReportFromMessages(messages), new uint256[](0));
s_prevOffRamp.execute(
_generateSingleLaneRampReportFromMessages(messages), new EVM2EVMOffRampHelper.GasLimitOverride[](0)
);

// Nonce unchanged for chain 3
assertEq(
Expand Down Expand Up @@ -446,7 +448,9 @@ contract NonceManager_OffRampUpgrade is EVM2EVMMultiOffRampSetup {
uint64 startNonce = s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, abi.encode(messages[0].sender));

for (uint64 i = 1; i < 4; ++i) {
s_prevOffRamp.execute(_generateSingleLaneRampReportFromMessages(messages), new uint256[](0));
s_prevOffRamp.execute(
_generateSingleLaneRampReportFromMessages(messages), new EVM2EVMOffRampHelper.GasLimitOverride[](0)
);

// messages contains a single message - update for the next execution
messages[0].nonce++;
Expand All @@ -465,7 +469,9 @@ contract NonceManager_OffRampUpgrade is EVM2EVMMultiOffRampSetup {
uint64 startNonce = s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_2, abi.encode(messages[0].sender));

for (uint64 i = 1; i < 4; ++i) {
s_nestedPrevOffRamps[0].execute(_generateSingleLaneRampReportFromMessages(messages), new uint256[](0));
s_nestedPrevOffRamps[0].execute(
_generateSingleLaneRampReportFromMessages(messages), new EVM2EVMOffRampHelper.GasLimitOverride[](0)
);

// messages contains a single message - update for the next execution
messages[0].nonce++;
Expand All @@ -484,7 +490,9 @@ contract NonceManager_OffRampUpgrade is EVM2EVMMultiOffRampSetup {
_generateSingleLaneSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, SINGLE_LANE_ON_RAMP_ADDRESS_1);

uint64 startNonce = s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, abi.encode(messages[0].sender));
s_prevOffRamp.execute(_generateSingleLaneRampReportFromMessages(messages), new uint256[](0));
s_prevOffRamp.execute(
_generateSingleLaneRampReportFromMessages(messages), new EVM2EVMOffRampHelper.GasLimitOverride[](0)
);

assertEq(
startNonce + 1, s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, abi.encode(messages[0].sender))
Expand Down Expand Up @@ -537,7 +545,9 @@ contract NonceManager_OffRampUpgrade is EVM2EVMMultiOffRampSetup {
Internal.EVM2EVMMessage[] memory messages =
_generateSingleLaneSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, SINGLE_LANE_ON_RAMP_ADDRESS_1);

s_prevOffRamp.execute(_generateSingleLaneRampReportFromMessages(messages), new uint256[](0));
s_prevOffRamp.execute(
_generateSingleLaneRampReportFromMessages(messages), new EVM2EVMOffRampHelper.GasLimitOverride[](0)
);

Internal.Any2EVMRampMessage[] memory messagesMultiRamp =
_generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1);
Expand Down Expand Up @@ -589,7 +599,9 @@ contract NonceManager_OffRampUpgrade is EVM2EVMMultiOffRampSetup {
messagesSingleLane[0].messageId = Internal._hash(messagesSingleLane[0], s_prevOffRamp.metadataHash());

// previous offramp executes msg and increases nonce
s_prevOffRamp.execute(_generateSingleLaneRampReportFromMessages(messagesSingleLane), new uint256[](0));
s_prevOffRamp.execute(
_generateSingleLaneRampReportFromMessages(messagesSingleLane), new EVM2EVMOffRampHelper.GasLimitOverride[](0)
);
assertEq(
startNonce + 1,
s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, abi.encode(messagesSingleLane[0].sender))
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/v0.8/ccip/test/e2e/End2End.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ contract E2E is EVM2EVMOnRampSetup, CommitStoreSetup, EVM2EVMOffRampSetup {

Internal.ExecutionReport memory execReport = _generateReportFromMessages(messages);
vm.resumeGasMetering();
s_offRamp.execute(execReport, new uint256[](0));
s_offRamp.execute(execReport, new EVM2EVMOffRamp.GasLimitOverride[](0));
}

function sendRequest(uint64 expectedSeqNum) public returns (Internal.EVM2EVMMessage memory) {
Expand Down
16 changes: 10 additions & 6 deletions contracts/src/v0.8/ccip/test/helpers/EVM2EVMOffRampHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,28 @@ contract EVM2EVMOffRampHelper is EVM2EVMOffRamp, IgnoreContractSize {
bytes calldata originalSender,
address receiver,
bytes[] calldata sourceTokenData,
bytes[] calldata offchainTokenData
bytes[] calldata offchainTokenData,
uint32[] memory tokenGasOverrides
) external returns (Client.EVMTokenAmount[] memory) {
return _releaseOrMintTokens(sourceTokenAmounts, originalSender, receiver, sourceTokenData, offchainTokenData);
return _releaseOrMintTokens(
sourceTokenAmounts, originalSender, receiver, sourceTokenData, offchainTokenData, tokenGasOverrides
);
}

function trialExecute(
Internal.EVM2EVMMessage 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 report(bytes calldata executableMessages) external {
_report(executableMessages);
}

function execute(Internal.ExecutionReport memory rep, uint256[] memory manualExecGasLimits) external {
_execute(rep, manualExecGasLimits);
function execute(Internal.ExecutionReport memory rep, GasLimitOverride[] memory gasLimitOverrides) external {
_execute(rep, gasLimitOverrides);
}

function metadataHash() external view returns (bytes32) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {EVM2EVMOffRamp} from "../../../offRamp/EVM2EVMOffRamp.sol";
contract ReentrancyAbuser is CCIPReceiver {
event ReentrancySucceeded();

uint32 internal constant DEFAULT_TOKEN_DEST_GAS_OVERHEAD = 144_000;

bool internal s_ReentrancyDone = false;
Internal.ExecutionReport internal s_payload;
EVM2EVMOffRamp internal s_offRamp;
Expand All @@ -23,11 +25,7 @@ contract ReentrancyAbuser 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[](numMsgs);
for (uint256 i = 0; i < numMsgs; ++i) {
gasOverrides[i] = 0;
}
EVM2EVMOffRamp.GasLimitOverride[] memory gasOverrides = _getGasLimitsFromMessages(s_payload.messages);

if (!s_ReentrancyDone) {
// Could do more rounds but a PoC one is enough
Expand All @@ -37,4 +35,22 @@ contract ReentrancyAbuser is CCIPReceiver {
emit ReentrancySucceeded();
}
}

function _getGasLimitsFromMessages(Internal.EVM2EVMMessage[] memory messages)
internal
view
returns (EVM2EVMOffRamp.GasLimitOverride[] memory)
{
EVM2EVMOffRamp.GasLimitOverride[] memory gasLimitOverrides = new EVM2EVMOffRamp.GasLimitOverride[](messages.length);
for (uint256 i = 0; i < messages.length; ++i) {
gasLimitOverrides[i].receiverExecutionGasLimit = messages[i].gasLimit;
gasLimitOverrides[i].tokenGasOverrides = new uint32[](messages[i].tokenAmounts.length);

for (uint256 j = 0; j < messages[i].tokenAmounts.length; ++j) {
gasLimitOverrides[i].tokenGasOverrides[j] = DEFAULT_TOKEN_DEST_GAS_OVERHEAD + 1;
}
}

return gasLimitOverrides;
}
}
Loading

0 comments on commit 1cfd5e7

Please sign in to comment.