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
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
7 changes: 4 additions & 3 deletions src/doc/simulate-l2-ownership-transfer.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Simulating an L2 Deposit Transaction Before Execution

The following steps describe how to simulate an L2 deposit transaction prior to L1 task execution. The [TransferL2PAOFromL1.sol](../template/TransferL2PAOFromL1.sol) template executes an L1 transaction, which is later forwarded to the L2 by the op-node. To gain additional confidence that the L2 deposit transaction works as expected, we manually simulate it and record the results in the task’s VALIDATION.md file.
The following steps describe how to simulate an L2 deposit transaction prior to L1 task execution. The [TransferL2PAOFromL1.sol](../template/TransferL2PAOFromL1.sol) transfers the ownership of a L2PAO from a current aliased L1 address to a new aliased L1 address.
This template executes an L1 transaction, which is later forwarded to the L2 by the op-node. To gain additional confidence that the L2 deposit transaction works as expected, we manually simulate it and record the results in the task’s VALIDATION.md file.

## Steps

Expand Down Expand Up @@ -32,7 +33,7 @@ The following steps describe how to simulate an L2 deposit transaction prior to
```
We must put `0xf2fde38b0000000000000000000000006b1bae59d09fccbddb6c6cceb07b7279367c4e3b` in the `Enter raw input data` field.
![Enter Raw Input Data](./images/tenderly-raw-input-data.png)
8. Double-check that the address returned from the `cast calldata-decode` step matches the aliased L1 ProxyAdmin owner (L1PAO) for the target chain. In this case, the L1PAO is `0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A`. Confirm this by manually unaliasing the address using [chisel](https://book.getfoundry.sh/chisel/).
8. Double-check that the address returned from the `cast calldata-decode` step matches the aliased new L1 ProxyAdmin owner (L1PAO) for the target chain. In this case, the L1PAO is `0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A`. Confirm this by manually unaliasing the address using [chisel](https://book.getfoundry.sh/chisel/).
```bash
> uint160 constant offset = uint160(0x1111000000000000000000000000000000001111)
> function undoL1ToL2Alias(address l2Address) internal pure returns (address l1Address) {
Expand All @@ -43,7 +44,7 @@ The following steps describe how to simulate an L2 deposit transaction prior to
> undoL1ToL2Alias(0x6B1BAE59D09fCcbdDB6C6cceb07B7279367C4E3b)
# returns: 0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A
```
9. Next we need to fill out the `Transaction Parameters` section on the right of the UI. Specifically, fill out the `From` address and `Gas` fields. The `From` address should be the aliased L1PAO address obtained in the previous step (i.e. `0x6B1BAE59D09fCcbdDB6C6cceb07B7279367C4E3b`). The `Gas` field should be set to `200000`. You can get this number by further parsing the opaque data and extracting the gas limit.
9. Next we need to fill out the `Transaction Parameters` section on the right of the UI. Specifically, fill out the `From` address and `Gas` fields. The `From` address should be the aliased old L1PAO address obtained as the `from` field in the `TransactionDeposited` event (i.e. `0x6B1BAE59D09fCcbdDB6C6cceb07B7279367C4E3b`). The `Gas` field should be set to `200000`. You can get this number by further parsing the opaque data and extracting the gas limit.
```bash
cast --to-dec 0x30d40
# returns: 200000
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
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
2 changes: 1 addition & 1 deletion src/tasks/sep/049-rev-share-betanet/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 049-rev-share-betanet: RevShare Upgrade and Setup for Betanet

Status: [DRAFT, NOT READY TO SIGN]()
Status: [READY TO SIGN]

## Objective

Expand Down
2 changes: 1 addition & 1 deletion src/tasks/sep/050-rev-share-ink-soneium/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 050-rev-share-ink-soneium: RevShare Upgrade and Setup for Ink Sepolia and Soneium Minato

Status: [DRAFT, NOT READY TO SIGN]()
Status: [READY TO SIGN]

## Objective

Expand Down
223 changes: 223 additions & 0 deletions src/template/OPCMUpgradeV600.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import {Claim} from "@eth-optimism-bedrock/src/dispute/lib/Types.sol";
import {VmSafe} from "forge-std/Vm.sol";
import {stdToml} from "forge-std/StdToml.sol";
import {LibString} from "solady/utils/LibString.sol";

import {OPCMTaskBase} from "src/tasks/types/OPCMTaskBase.sol";
import {SuperchainAddressRegistry} from "src/SuperchainAddressRegistry.sol";
import {Action} from "src/libraries/MultisigTypes.sol";

/// @notice A template contract for configuring OPCMTaskBase templates.
/// Supports: op-contracts/v6.0.0
contract OPCMUpgradeV600 is OPCMTaskBase {
using stdToml for string;
using LibString for string;

/// @notice Struct to store inputs data for each L2 chain.
struct OPCMUpgrade {
Claim cannonPrestate;
Claim cannonKonaPrestate;
uint256 chainId;
string expectedValidationErrors;
}

/// @notice Mapping of L2 chain IDs to their respective OPCMUpgrade structs.
mapping(uint256 => OPCMUpgrade) public upgrades;

/// @notice The Standard Validator returned by OPCM
IOPContractsManagerStandardValidator public STANDARD_VALIDATOR;

/// @notice OPCM we delegatecall into (must be v6.0.0).
address public OPCM;

/// @notice Names in the SuperchainAddressRegistry that are expected to be written during this task.
function _taskStorageWrites() internal pure virtual override returns (string[] memory) {
string[] memory storageWrites = new string[](9);
storageWrites[0] = "DisputeGameFactoryProxy";
storageWrites[1] = "SystemConfigProxy";
storageWrites[2] = "OptimismPortalProxy";
storageWrites[3] = "OptimismMintableERC20FactoryProxy";
storageWrites[4] = "AddressManager";
storageWrites[5] = "ProxyAdminOwner";
storageWrites[6] = "AnchorStateRegistryProxy";
storageWrites[7] = "L1StandardBridgeProxy";
storageWrites[8] = "L1ERC721BridgeProxy";
return storageWrites;
}

/// @notice Returns an array of strings that refer to contract names in the address registry.
/// Contracts with these names are expected to have their balance changes during the task.
/// By default returns an empty array. Override this function if your task expects balance changes.
function _taskBalanceChanges() internal view virtual override returns (string[] memory) {}

/// @notice Sets up the template with implementation configurations from a TOML file.
function _templateSetup(string memory taskConfigFilePath, address rootSafe) internal override {
super._templateSetup(taskConfigFilePath, rootSafe);
string memory tomlContent = vm.readFile(taskConfigFilePath);

// Load upgrades from TOML
OPCMUpgrade[] memory _upgrades = abi.decode(tomlContent.parseRaw(".opcmUpgrades"), (OPCMUpgrade[]));
for (uint256 i = 0; i < _upgrades.length; i++) {
upgrades[_upgrades[i].chainId] = _upgrades[i];
}

// OPCM from TOML; must be v6.0.0
OPCM = tomlContent.readAddress(".addresses.OPCM");
OPCM_TARGETS.push(OPCM);
require(IOPContractsManagerV600(OPCM).version().eq("6.0.0"), "Incorrect OPCM");
vm.label(OPCM, "OPCM");

// Fetch the validator directly from OPCM so it doesn't need to be configured in TOML
address validatorAddr = address(IOPCM(OPCM).opcmStandardValidator());
require(validatorAddr != address(0), "OPCM returned zero validator");
require(validatorAddr.code.length > 0, "Validator has no code");
STANDARD_VALIDATOR = IOPContractsManagerStandardValidator(validatorAddr);
vm.label(address(STANDARD_VALIDATOR), "OPCMStandardValidator");
}

/// @notice Builds the actions for executing the operations.
function _build(address) internal override {
SuperchainAddressRegistry.ChainInfo[] memory chains = superchainAddrRegistry.getChains();
IOPContractsManagerV600.OpChainConfig[] memory opChainConfigs =
new IOPContractsManagerV600.OpChainConfig[](chains.length);

for (uint256 i = 0; i < chains.length; i++) {
uint256 chainId = chains[i].chainId;
require(upgrades[chainId].chainId != 0, "OPCMUpgradeV600: Config not found for chain");

require(
Claim.unwrap(upgrades[chainId].cannonPrestate) != bytes32(0), "OPCMUpgradeV600: cannonPrestate is zero"
);
require(
Claim.unwrap(upgrades[chainId].cannonKonaPrestate) != bytes32(0),
"OPCMUpgradeV600: cannonKonaPrestate is zero"
);

opChainConfigs[i] = IOPContractsManagerV600.OpChainConfig({
systemConfigProxy: ISystemConfig(superchainAddrRegistry.getAddress("SystemConfigProxy", chainId)),
cannonPrestate: upgrades[chainId].cannonPrestate,
cannonKonaPrestate: upgrades[chainId].cannonKonaPrestate
});
}

// Delegatecall the OPCM.upgrade() function
(bool ok,) = OPCM_TARGETS[0].delegatecall(
abi.encodeWithSelector(IOPContractsManagerV600.upgrade.selector, opChainConfigs)
);
require(ok, "OPCMUpgradeV600: Delegatecall failed in _build.");
}

/// @notice This method performs all validations and assertions that verify the calls executed as expected.
function _validate(VmSafe.AccountAccess[] memory, Action[] memory, address) internal view override {
SuperchainAddressRegistry.ChainInfo[] memory chains = superchainAddrRegistry.getChains();

// Cache standard validator's expected values (same for all chains)
address standardL1PAO = STANDARD_VALIDATOR.l1PAOMultisig();
address standardChallenger = STANDARD_VALIDATOR.challenger();

for (uint256 i = 0; i < chains.length; i++) {
uint256 chainId = chains[i].chainId;

IOPContractsManagerStandardValidator.ValidationInputDev memory input = IOPContractsManagerStandardValidator
.ValidationInputDev({
sysCfg: ISystemConfig(superchainAddrRegistry.getAddress("SystemConfigProxy", chainId)),
cannonPrestate: Claim.unwrap(upgrades[chainId].cannonPrestate),
cannonKonaPrestate: Claim.unwrap(upgrades[chainId].cannonKonaPrestate),
l2ChainID: chainId,
proposer: superchainAddrRegistry.getAddress("Proposer", chainId)
});

// Compute overrides: non-zero only if chain differs from standard
address l1PAOOverride = superchainAddrRegistry.getAddress("ProxyAdminOwner", chainId);
address challengerOverride = superchainAddrRegistry.getAddress("Challenger", chainId);

l1PAOOverride = l1PAOOverride != standardL1PAO ? l1PAOOverride : address(0);
challengerOverride = challengerOverride != standardChallenger ? challengerOverride : address(0);

string memory errors;
if (l1PAOOverride != address(0) || challengerOverride != address(0)) {
errors = STANDARD_VALIDATOR.validateWithOverrides({
_input: input,
_allowFailure: true,
_overrides: IOPContractsManagerStandardValidator.ValidationOverrides({
l1PAOMultisig: l1PAOOverride,
challenger: challengerOverride
})
});
} else {
errors = STANDARD_VALIDATOR.validate({_input: input, _allowFailure: true});
}

string memory expErrors = upgrades[chainId].expectedValidationErrors;
require(errors.eq(expErrors), string.concat("Unexpected errors: ", errors, "; expected: ", expErrors));
}
}

/// @notice Override to return a list of addresses that should not be checked for code length.
function _getCodeExceptions() internal view virtual override returns (address[] memory) {}
}

/* ---------- Interfaces ---------- */
/// @notice OPCM Interface.
interface IOPContractsManagerV600 {
struct OpChainConfig {
ISystemConfig systemConfigProxy;
Claim cannonPrestate;
Claim cannonKonaPrestate;
}

function version() external view returns (string memory);

function upgrade(OpChainConfig[] memory _opChainConfigs) external;

function opcmStandardValidator() external view returns (IOPContractsManagerStandardValidator);
}

/// @notice Interface to retrieve the standard validator from OPCM.
interface IOPCM {
function opcmStandardValidator() external view returns (IOPContractsManagerStandardValidator);
}

/// @notice Validator interface for validateWithOverrides usage.
interface IOPContractsManagerStandardValidator {
struct ValidationInputDev {
ISystemConfig sysCfg;
bytes32 cannonPrestate;
bytes32 cannonKonaPrestate;
uint256 l2ChainID;
address proposer;
}

struct ValidationOverrides {
address l1PAOMultisig;
address challenger;
}

function validate(ValidationInputDev memory _input, bool _allowFailure) external view returns (string memory);
function l1PAOMultisig() external view returns (address);
function challenger() external view returns (address);
function validateWithOverrides(
ValidationInputDev memory _input,
bool _allowFailure,
ValidationOverrides memory _overrides
) external view returns (string memory);

function version() external view returns (string memory);
}

interface ISystemConfig {
struct Addresses {
address l1CrossDomainMessenger;
address l1ERC721Bridge;
address l1StandardBridge;
address optimismPortal;
address optimismMintableERC20Factory;
address delayedWETH;
address opcm;
}

function getAddresses() external view returns (Addresses memory);
}
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
Loading