Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/ci.just
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ simulate-all-templates:
task_name=$(basename "$task")
task_path="$root_dir/test/tasks/example/$network/$task_name"

# Skip tasks that exceed the 15M gas limit
# 023-u13-to-u16a: OPCMUpgradeV220toV410 performs 4 sequential upgrades (U13 through U16a)
# consuming ~21.7M gas, which exceeds the 15M safety limit for Fusaka EIP-7825 compatibility.
# This template is not actively used and can be skipped in CI.
if [ "$task_name" = "023-u13-to-u16a" ]; then
Comment thread
mds1 marked this conversation as resolved.
echo "Skipping $task_name (exceeds 15M gas limit)"
continue
fi

# Launch each simulation in background.
"${root_dir}/src/script/simulate-task.sh" "$task_path" "$nested_safe_name_depth_1" "$nested_safe_name_depth_2" "$network" & pids+=( "$!" )
current_batch=$((current_batch + 1))
Expand All @@ -65,7 +74,7 @@ simulate-all-templates:
wait "$pid"
simulation_count=$((simulation_count + 1))
done

# Reset for next batch
pids=()
current_batch=0
Expand Down
21 changes: 21 additions & 0 deletions src/tasks/MultisigTask.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ abstract contract MultisigTask is Test, Script, StateOverrideManager, TaskManage
using AccountAccessParser for VmSafe.AccountAccess;
using StdStyle for string;

/// @notice Maximum gas limit for transactions, 15M gas is ~90% of the limit
Comment thread
mds1 marked this conversation as resolved.
uint256 internal constant MAX_GAS_LIMIT = 15_000_000;

/// @notice AddressesRegistry contract
AddressRegistry public addrRegistry;

Expand Down Expand Up @@ -519,6 +522,24 @@ abstract contract MultisigTask is Test, Script, StateOverrideManager, TaskManage

(bool success, bytes memory returnData) = multisig.call{gas: gas}(callData);

// Check that the transaction did not exceed the maximum gas limit.
// We must check gas consumed BEFORE refunds, because the EVM requires enough gas upfront,
// therefore we check gasTotalUsed + gasRefunded
// Note: gasRefunded is int64, but should always be >= 0 in practice, unsure why this is
// an int64 in forge.
VmSafe.Gas memory gasInfo = vm.lastCallGas();
require(gasInfo.gasRefunded >= 0, "MultisigTask: negative gas refund is invalid");
uint256 gasRefunded = uint256(uint64(gasInfo.gasRefunded));
uint256 gasConsumedBeforeRefund = uint256(gasInfo.gasTotalUsed) + gasRefunded;
if (gasConsumedBeforeRefund > MAX_GAS_LIMIT) {
console.log("Gas consumed before refund:", gasConsumedBeforeRefund);
console.log("Gas refunded:", gasRefunded);
console.log("Gas final used:", gasInfo.gasTotalUsed);
console.log("Gas limit:", MAX_GAS_LIMIT);
console.log("Fusaka EIP-7825 cap: 16,777,216 gas");
revert("MultisigTask: transaction exceeds 15M gas limit");
}

if (!success) {
MultisigTaskPrinter.printErrorExecutingMultisigTransaction(returnData);
revert("MultisigTask: execute failed");
Expand Down
17 changes: 16 additions & 1 deletion test/tasks/MultisigTask.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {VmSafe} from "forge-std/Vm.sol";
import {Test} from "forge-std/Test.sol";
import {stdStorage, StdStorage} from "forge-std/StdStorage.sol";
import {IGnosisSafe, Enum} from "@base-contracts/script/universal/IGnosisSafe.sol";
import {LibString} from "@solady/utils/LibString.sol";
import {Vm} from "forge-std/Vm.sol";
import {Solarray} from "lib/optimism/packages/contracts-bedrock/scripts/libraries/Solarray.sol";

Expand All @@ -15,6 +14,7 @@ import {SuperchainAddressRegistry} from "src/SuperchainAddressRegistry.sol";
import {Action, TaskPayload} from "src/libraries/MultisigTypes.sol";
import {MockMultisigTask} from "test/tasks/mock/MockMultisigTask.sol";
import {MockTarget} from "test/tasks/mock/MockTarget.sol";
import {HighGasMultisigTask} from "test/tasks/mock/HighGasMultisigTask.sol";

contract MultisigTaskUnitTest is Test {
using stdStorage for StdStorage;
Expand Down Expand Up @@ -408,6 +408,21 @@ contract MultisigTaskUnitTest is Test {
assertNestedCalldata(result[0], root, abi.encodeCall(IGnosisSafe(root).approveHash, (hash)));
}

/// @notice Tests that MultisigTask reverts when a transaction exceeds the 15M gas limit
function test_simulate_revertsOnHighGasUsage_fails() public {
string memory highGasToml =
"l2chains = [{name = \"OP Mainnet\", chainId = 10}]\n" "\n" "templateName = \"HighGasMultisigTask\"\n" "\n";

string memory taskConfigFilePath =
MultisigTaskTestHelper.createTempTomlFile(highGasToml, TESTING_DIRECTORY, "highgas");

HighGasMultisigTask highGasTask = new HighGasMultisigTask();

vm.expectRevert("MultisigTask: transaction exceeds 15M gas limit");
highGasTask.simulate(taskConfigFilePath);
MultisigTaskTestHelper.removeFile(taskConfigFilePath);
}

/// @notice Asserts that the root safe calldata is correct.
function assertRootCalldata(bytes memory data, address target, uint256 value, bytes memory callData)
internal
Expand Down
47 changes: 47 additions & 0 deletions test/tasks/mock/HighGasMultisigTask.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import {VmSafe} from "forge-std/Vm.sol";

import {L2TaskBase} from "src/tasks/types/L2TaskBase.sol";
import {Action} from "src/libraries/MultisigTypes.sol";
import {MockTarget} from "test/tasks/mock/MockTarget.sol";

/// @notice Mock task that consumes a lot of gas to test that MultisigTask correctly rejects
/// transactions that consume gas too close to the Fusaka EIP-7825 cap of 16,777,216 gas.
contract HighGasMultisigTask is L2TaskBase {
/// @notice reference to the mock target contract
MockTarget public mockTarget;

/// @notice Returns the safe address string identifier
/// @return The string "ProxyAdminOwner"
function safeAddressString() public pure override returns (string memory) {
return "ProxyAdminOwner";
}

/// @notice Returns the storage write permissions required for this task
/// @return Array of storage write permissions
function _taskStorageWrites() internal pure override returns (string[] memory) {
string[] memory storageWrites = new string[](1);
storageWrites[0] = "ProxyAdminOwner";
return storageWrites;
}

function _templateSetup(string memory, address rootSafe) internal override {
super._templateSetup("", rootSafe);
// Initialize mockTarget so it's available when _build() runs
mockTarget = new MockTarget();
}

/// @notice Build function that creates an action which will consume >15M gas on-chain
function _build(address) internal override {
// Call mockTarget.consumeGas() which will expand memory during execution to use a lot of gas
mockTarget.consumeGas();
}

/// @notice Validates that the task executed
function _validate(VmSafe.AccountAccess[] memory, Action[] memory, address) internal view override {}

/// @notice No code exceptions for this template.
function _getCodeExceptions() internal view virtual override returns (address[] memory) {}
}
27 changes: 25 additions & 2 deletions test/tasks/mock/MockTarget.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,30 @@ contract MockTarget is Test {
vm.store(task, startSnapshotSlot, bytes32(id));
}

function foobar() public {
// This function is when creating dummy/noop actions for testing.
/// @notice Function that consumes a lot of gas through memory expansion.
/// Used to test that MultisigTask correctly rejects transactions that consume gas
/// too close to the Fusaka EIP-7825 cap of 16,777,216 gas.
function consumeGas() public {
// Expand memory to consume ~17M gas. Memory expansion cost grows quadratically, so we
// need to allocate enough memory to exceed the 15M threshold in MultisigTask.sol.
bytes memory largeData1 = new bytes(200000); // 200KB
bytes memory largeData2 = new bytes(400000); // 400KB
bytes memory largeData3 = new bytes(800000); // 800KB
bytes memory largeData4 = new bytes(1600000); // 1.6MB

// Write to storage to ensure this call is captured as an action, if this function was
// view or pure it would get filtered out by MultisigTask.sol.
uint256 sum;
assembly {
// Touch each array to prevent these from being optimized away.
let val1 := mload(add(largeData1, 32))
let val2 := mload(add(largeData2, 32))
let val3 := mload(add(largeData3, 32))
let val4 := mload(add(largeData4, 32))
sum := add(add(add(val1, val2), val3), val4)
}

// Write to storage to make this a state-changing function
task = address(uint160(sum));
}
}