Commit 2f17e6b6 authored by smartcontracts's avatar smartcontracts Committed by GitHub

feat: introduce DeputyPauseModule (#13186)

Introduces the DeputyPauseModule which allows an account to act as
the Foundation Safe for the sake of triggering the Superchain-wide
pause function. Adding this module to the Foundation Safe would
remove the need for any pre-signed pause transactions and generally
simplifies the incident response process.
parent 4bf21aec
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import { GnosisSafe as Safe } from "safe-contracts/GnosisSafe.sol";
import { ISemver } from "interfaces/universal/ISemver.sol";
import { IDeputyGuardianModule } from "interfaces/safe/IDeputyGuardianModule.sol";
import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol";
interface IDeputyPauseModule is ISemver {
error DeputyPauseModule_InvalidDeputy();
error DeputyPauseModule_ExecutionFailed(string);
error DeputyPauseModule_SuperchainNotPaused();
error DeputyPauseModule_Unauthorized();
error DeputyPauseModule_NonceAlreadyUsed();
error DeputyPauseModule_NotFromSafe();
error ECDSAInvalidSignature();
error ECDSAInvalidSignatureLength(uint256 length);
error ECDSAInvalidSignatureS(bytes32 s);
error InvalidShortString();
error StringTooLong(string str);
struct PauseMessage {
bytes32 nonce;
}
struct DeputyAuthMessage {
address deputy;
}
event DeputySet(address indexed deputy);
event DeputyGuardianModuleSet(IDeputyGuardianModule indexed deputyGuardianModule);
event PauseTriggered(address indexed deputy, bytes32 nonce);
event EIP712DomainChanged();
function version() external view returns (string memory);
function __constructor__(
Safe _foundationSafe,
IDeputyGuardianModule _deputyGuardianModule,
ISuperchainConfig _superchainConfig,
address _deputy,
bytes memory _deputySignature
)
external;
function foundationSafe() external view returns (Safe foundationSafe_);
function deputyGuardianModule() external view returns (IDeputyGuardianModule);
function superchainConfig() external view returns (ISuperchainConfig superchainConfig_);
function deputy() external view returns (address);
function pauseMessageTypehash() external pure returns (bytes32 pauseMessageTypehash_);
function deputyAuthMessageTypehash() external pure returns (bytes32 deputyAuthMessageTypehash_);
function usedNonces(bytes32) external view returns (bool);
function pause(bytes32 _nonce, bytes memory _signature) external;
function setDeputy(address _deputy, bytes memory _deputySignature) external;
function setDeputyGuardianModule(IDeputyGuardianModule _deputyGuardianModule) external;
function eip712Domain()
external
view
returns (
bytes1 fields,
string memory name,
string memory version,
uint256 chainId,
address verifyingContract,
bytes32 salt,
uint256[] memory extensions
);
}
...@@ -16,10 +16,10 @@ import ( ...@@ -16,10 +16,10 @@ import (
var excludeContracts = []string{ var excludeContracts = []string{
// External dependencies // External dependencies
"IERC20", "IERC721", "IERC721Enumerable", "IERC721Upgradeable", "IERC721Metadata", "IERC20", "IERC721", "IERC5267", "IERC721Enumerable", "IERC721Upgradeable", "IERC721Metadata",
"IERC165", "IERC165Upgradeable", "ERC721TokenReceiver", "ERC1155TokenReceiver", "IERC165", "IERC165Upgradeable", "ERC721TokenReceiver", "ERC1155TokenReceiver",
"ERC777TokensRecipient", "Guard", "IProxy", "Vm", "VmSafe", "IMulticall3", "ERC777TokensRecipient", "Guard", "IProxy", "Vm", "VmSafe", "IMulticall3",
"IERC721TokenReceiver", "IProxyCreationCallback", "IBeacon", "IERC721TokenReceiver", "IProxyCreationCallback", "IBeacon", "IEIP712",
// EAS // EAS
"IEAS", "ISchemaResolver", "ISchemaRegistry", "IEAS", "ISchemaResolver", "ISchemaRegistry",
......
[
{
"inputs": [
{
"internalType": "contract GnosisSafe",
"name": "_foundationSafe",
"type": "address"
},
{
"internalType": "contract IDeputyGuardianModule",
"name": "_deputyGuardianModule",
"type": "address"
},
{
"internalType": "contract ISuperchainConfig",
"name": "_superchainConfig",
"type": "address"
},
{
"internalType": "address",
"name": "_deputy",
"type": "address"
},
{
"internalType": "bytes",
"name": "_deputySignature",
"type": "bytes"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "deputy",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "deputyAuthMessageTypehash",
"outputs": [
{
"internalType": "bytes32",
"name": "deputyAuthMessageTypehash_",
"type": "bytes32"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [],
"name": "deputyGuardianModule",
"outputs": [
{
"internalType": "contract IDeputyGuardianModule",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "eip712Domain",
"outputs": [
{
"internalType": "bytes1",
"name": "fields",
"type": "bytes1"
},
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "version",
"type": "string"
},
{
"internalType": "uint256",
"name": "chainId",
"type": "uint256"
},
{
"internalType": "address",
"name": "verifyingContract",
"type": "address"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
},
{
"internalType": "uint256[]",
"name": "extensions",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "foundationSafe",
"outputs": [
{
"internalType": "contract GnosisSafe",
"name": "foundationSafe_",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "_nonce",
"type": "bytes32"
},
{
"internalType": "bytes",
"name": "_signature",
"type": "bytes"
}
],
"name": "pause",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "pauseMessageTypehash",
"outputs": [
{
"internalType": "bytes32",
"name": "pauseMessageTypehash_",
"type": "bytes32"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_deputy",
"type": "address"
},
{
"internalType": "bytes",
"name": "_deputySignature",
"type": "bytes"
}
],
"name": "setDeputy",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract IDeputyGuardianModule",
"name": "_deputyGuardianModule",
"type": "address"
}
],
"name": "setDeputyGuardianModule",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "superchainConfig",
"outputs": [
{
"internalType": "contract ISuperchainConfig",
"name": "superchainConfig_",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"name": "usedNonces",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "version",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "contract IDeputyGuardianModule",
"name": "deputyGuardianModule",
"type": "address"
}
],
"name": "DeputyGuardianModuleSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "deputy",
"type": "address"
}
],
"name": "DeputySet",
"type": "event"
},
{
"anonymous": false,
"inputs": [],
"name": "EIP712DomainChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "deputy",
"type": "address"
},
{
"indexed": false,
"internalType": "bytes32",
"name": "nonce",
"type": "bytes32"
}
],
"name": "PauseTriggered",
"type": "event"
},
{
"inputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"name": "DeputyPauseModule_ExecutionFailed",
"type": "error"
},
{
"inputs": [],
"name": "DeputyPauseModule_InvalidDeputy",
"type": "error"
},
{
"inputs": [],
"name": "DeputyPauseModule_NonceAlreadyUsed",
"type": "error"
},
{
"inputs": [],
"name": "DeputyPauseModule_NotFromSafe",
"type": "error"
},
{
"inputs": [],
"name": "DeputyPauseModule_SuperchainNotPaused",
"type": "error"
},
{
"inputs": [],
"name": "DeputyPauseModule_Unauthorized",
"type": "error"
},
{
"inputs": [],
"name": "ECDSAInvalidSignature",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "length",
"type": "uint256"
}
],
"name": "ECDSAInvalidSignatureLength",
"type": "error"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "s",
"type": "bytes32"
}
],
"name": "ECDSAInvalidSignatureS",
"type": "error"
},
{
"inputs": [],
"name": "InvalidShortString",
"type": "error"
},
{
"inputs": [
{
"internalType": "string",
"name": "str",
"type": "string"
}
],
"name": "StringTooLong",
"type": "error"
}
]
\ No newline at end of file
...@@ -183,6 +183,10 @@ ...@@ -183,6 +183,10 @@
"initCodeHash": "0x5eaf823d81995ce1f703f26e31049c54c1d4902dd9873a0b4645d470f2f459a2", "initCodeHash": "0x5eaf823d81995ce1f703f26e31049c54c1d4902dd9873a0b4645d470f2f459a2",
"sourceCodeHash": "0x17236a91c4171ae9525eae0e59fa65bb2dc320d62677cfc7d7eb942f182619fb" "sourceCodeHash": "0x17236a91c4171ae9525eae0e59fa65bb2dc320d62677cfc7d7eb942f182619fb"
}, },
"src/safe/DeputyPauseModule.sol": {
"initCodeHash": "0x55f0b6c1a102114b8c30157e7295da7058ba8d1b93f7d4070028286968611279",
"sourceCodeHash": "0xec92b5ccbd3ee337897464546ccb8306fca245bbb6ce4b0941d86bc82e0f4cfe"
},
"src/safe/LivenessGuard.sol": { "src/safe/LivenessGuard.sol": {
"initCodeHash": "0xc8e29e8b12f423c8cd229a38bc731240dd815d96f1b0ab96c71494dde63f6a81", "initCodeHash": "0xc8e29e8b12f423c8cd229a38bc731240dd815d96f1b0ab96c71494dde63f6a81",
"sourceCodeHash": "0x72b8d8d855e7af8beee29330f6cb9b9069acb32e23ce940002ec9a41aa012a16" "sourceCodeHash": "0x72b8d8d855e7af8beee29330f6cb9b9069acb32e23ce940002ec9a41aa012a16"
......
[
{
"bytes": "32",
"label": "_nameFallback",
"offset": 0,
"slot": "0",
"type": "string"
},
{
"bytes": "32",
"label": "_versionFallback",
"offset": 0,
"slot": "1",
"type": "string"
},
{
"bytes": "20",
"label": "deputy",
"offset": 0,
"slot": "2",
"type": "address"
},
{
"bytes": "20",
"label": "deputyGuardianModule",
"offset": 0,
"slot": "3",
"type": "contract IDeputyGuardianModule"
},
{
"bytes": "32",
"label": "usedNonces",
"offset": 0,
"slot": "4",
"type": "mapping(bytes32 => bool)"
}
]
\ No newline at end of file
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
// Safe
import { GnosisSafe as Safe } from "safe-contracts/GnosisSafe.sol";
import { Enum } from "safe-contracts/common/Enum.sol";
// Contracts
import { EIP712 } from "@openzeppelin/contracts-v5/utils/cryptography/EIP712.sol";
// Libraries
import { ECDSA } from "@openzeppelin/contracts-v5/utils/cryptography/ECDSA.sol";
// Interfaces
import { ISemver } from "interfaces/universal/ISemver.sol";
import { IDeputyGuardianModule } from "interfaces/safe/IDeputyGuardianModule.sol";
import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol";
/// @title DeputyPauseModule
/// @notice Safe Module designed to be installed in the Foundation Safe which allows a specific
/// deputy address to act as the Foundation Safe for the sake of triggering the
/// Superchain-wide pause functionality. Significantly simplifies the process of triggering
/// a Superchain-wide pause without changing the existing security model.
contract DeputyPauseModule is ISemver, EIP712 {
/// @notice Error message for invalid deputy.
error DeputyPauseModule_InvalidDeputy();
/// @notice Error message for unauthorized calls.
error DeputyPauseModule_Unauthorized();
/// @notice Error message for nonce reuse.
error DeputyPauseModule_NonceAlreadyUsed();
/// @notice Error message for failed transaction execution.
error DeputyPauseModule_ExecutionFailed(string);
/// @notice Error message for the SuperchainConfig not being paused.
error DeputyPauseModule_SuperchainNotPaused();
/// @notice Error message for the call not being from the Foundation Safe.
error DeputyPauseModule_NotFromSafe();
/// @notice Struct for the Pause action.
/// @custom:field nonce Signature nonce.
struct PauseMessage {
bytes32 nonce;
}
/// @notice Struct for the DeputyAuth action.
/// @custom:field deputy Address of the deputy account.
struct DeputyAuthMessage {
address deputy;
}
/// @notice Event emitted when the deputy address is set.
event DeputySet(address indexed deputy);
/// @notice Event emitted when the DeputyGuardianModule is set.
event DeputyGuardianModuleSet(IDeputyGuardianModule indexed deputyGuardianModule);
/// @notice Event emitted when the pause is triggered.
event PauseTriggered(address indexed deputy, bytes32 nonce);
/// @notice Foundation Safe.
Safe internal immutable FOUNDATION_SAFE;
/// @notice SuperchainConfig contract.
ISuperchainConfig internal immutable SUPERCHAIN_CONFIG;
/// @notice Typehash for the Pause action.
bytes32 internal constant PAUSE_MESSAGE_TYPEHASH = keccak256("PauseMessage(bytes32 nonce)");
/// @notice Typehash for the DeputyAuth message.
bytes32 internal constant DEPUTY_AUTH_MESSAGE_TYPEHASH = keccak256("DeputyAuthMessage(address deputy)");
/// @notice Address of the Deputy account.
address public deputy;
/// @notice Address of the DeputyGuardianModule used by the SC Safe.
IDeputyGuardianModule public deputyGuardianModule;
/// @notice Used nonces.
mapping(bytes32 => bool) public usedNonces;
/// @notice Semantic version.
/// @custom:semver 1.0.0-beta.1
string public constant version = "1.0.0-beta.1";
/// @param _foundationSafe Address of the Foundation Safe.
/// @param _deputyGuardianModule Address of the DeputyGuardianModule used by the SC Safe.
/// @param _superchainConfig Address of the SuperchainConfig contract.
/// @param _deputy Address of the deputy account.
/// @param _deputySignature Signature from the deputy verifying that the account is an EOA.
constructor(
Safe _foundationSafe,
IDeputyGuardianModule _deputyGuardianModule,
ISuperchainConfig _superchainConfig,
address _deputy,
bytes memory _deputySignature
)
EIP712("DeputyPauseModule", "1")
{
_setDeputy(_deputy, _deputySignature);
deputyGuardianModule = _deputyGuardianModule;
FOUNDATION_SAFE = _foundationSafe;
SUPERCHAIN_CONFIG = _superchainConfig;
}
/// @notice Getter function for the Foundation Safe address.
/// @return foundationSafe_ Foundation Safe address.
function foundationSafe() public view returns (Safe foundationSafe_) {
foundationSafe_ = FOUNDATION_SAFE;
}
/// @notice Getter function for the SuperchainConfig address.
/// @return superchainConfig_ SuperchainConfig address.
function superchainConfig() public view returns (ISuperchainConfig superchainConfig_) {
superchainConfig_ = SUPERCHAIN_CONFIG;
}
/// @notice Getter function for the Pause message typehash.
/// @return pauseMessageTypehash_ Pause message typehash.
function pauseMessageTypehash() public pure returns (bytes32 pauseMessageTypehash_) {
pauseMessageTypehash_ = PAUSE_MESSAGE_TYPEHASH;
}
/// @notice Getter function for the DeputyAuth message typehash.
/// @return deputyAuthMessageTypehash_ DeputyAuth message typehash.
function deputyAuthMessageTypehash() public pure returns (bytes32 deputyAuthMessageTypehash_) {
deputyAuthMessageTypehash_ = DEPUTY_AUTH_MESSAGE_TYPEHASH;
}
/// @notice Sets the deputy address.
/// @param _deputy Deputy address.
/// @param _deputySignature Deputy signature.
function setDeputy(address _deputy, bytes memory _deputySignature) external {
// Can only be called by the Foundation Safe itself.
if (msg.sender != address(FOUNDATION_SAFE)) {
revert DeputyPauseModule_NotFromSafe();
}
// Set the deputy address.
_setDeputy(_deputy, _deputySignature);
}
/// @notice Sets the DeputyGuardianModule.
/// @param _deputyGuardianModule DeputyGuardianModule address.
function setDeputyGuardianModule(IDeputyGuardianModule _deputyGuardianModule) external {
if (msg.sender != address(FOUNDATION_SAFE)) {
revert DeputyPauseModule_NotFromSafe();
}
deputyGuardianModule = _deputyGuardianModule;
emit DeputyGuardianModuleSet(_deputyGuardianModule);
}
/// @notice Calls the Foundation Safe's `execTransactionFromModuleReturnData()` function with
/// the arguments necessary to call `pause()` on the Security Council Safe, which will
/// then cause the Security Council Safe to trigger SuperchainConfig pause.
/// Front-running this function is completely safe, it'll pause either way.
/// @param _nonce Signature nonce.
/// @param _signature ECDSA signature.
function pause(bytes32 _nonce, bytes memory _signature) external {
// Verify the signature.
bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(PAUSE_MESSAGE_TYPEHASH, PauseMessage(_nonce))));
if (ECDSA.recover(digest, _signature) != deputy) {
revert DeputyPauseModule_Unauthorized();
}
// Make sure the nonce hasn't been used yet.
if (usedNonces[_nonce]) {
revert DeputyPauseModule_NonceAlreadyUsed();
}
// Mark the nonce as used.
usedNonces[_nonce] = true;
// Attempt to trigger the call.
(bool success, bytes memory returnData) = FOUNDATION_SAFE.execTransactionFromModuleReturnData(
address(deputyGuardianModule), 0, abi.encodeCall(IDeputyGuardianModule.pause, ()), Enum.Operation.Call
);
// If the call fails, revert.
if (!success) {
revert DeputyPauseModule_ExecutionFailed(string(returnData));
}
// Verify that the SuperchainConfig is now paused.
if (!SUPERCHAIN_CONFIG.paused()) {
revert DeputyPauseModule_SuperchainNotPaused();
}
// Emit that the pause was triggered.
emit PauseTriggered(deputy, _nonce);
}
/// @notice Internal function to set the deputy address.
/// @param _deputy Deputy address.
/// @param _deputySignature Deputy signature.
function _setDeputy(address _deputy, bytes memory _deputySignature) internal {
// Check that the deputy signature is valid.
bytes32 digest =
_hashTypedDataV4(keccak256(abi.encode(DEPUTY_AUTH_MESSAGE_TYPEHASH, DeputyAuthMessage(_deputy))));
if (ECDSA.recover(digest, _deputySignature) != _deputy) {
revert DeputyPauseModule_InvalidDeputy();
}
// Set the deputy address.
deputy = _deputy;
// Emit the DeputySet event.
emit DeputySet(_deputy);
}
}
This diff is collapsed.
...@@ -34,6 +34,7 @@ contract Specification_Test is CommonTest { ...@@ -34,6 +34,7 @@ contract Specification_Test is CommonTest {
SYSTEMCONFIGOWNER, SYSTEMCONFIGOWNER,
GUARDIAN, GUARDIAN,
DEPUTYGUARDIAN, DEPUTYGUARDIAN,
PAUSEDEPUTY,
MESSENGER, MESSENGER,
L1PROXYADMINOWNER, L1PROXYADMINOWNER,
GOVERNANCETOKENOWNER, GOVERNANCETOKENOWNER,
...@@ -878,6 +879,24 @@ contract Specification_Test is CommonTest { ...@@ -878,6 +879,24 @@ contract Specification_Test is CommonTest {
_addSpec({ _name: "DeputyGuardianModule", _sel: _getSel("superchainConfig()") }); _addSpec({ _name: "DeputyGuardianModule", _sel: _getSel("superchainConfig()") });
_addSpec({ _name: "DeputyGuardianModule", _sel: _getSel("version()") }); _addSpec({ _name: "DeputyGuardianModule", _sel: _getSel("version()") });
// DeputyPauseModule
_addSpec({ _name: "DeputyPauseModule", _sel: _getSel("version()") });
_addSpec({ _name: "DeputyPauseModule", _sel: _getSel("foundationSafe()") });
_addSpec({ _name: "DeputyPauseModule", _sel: _getSel("deputyGuardianModule()") });
_addSpec({ _name: "DeputyPauseModule", _sel: _getSel("superchainConfig()") });
_addSpec({ _name: "DeputyPauseModule", _sel: _getSel("deputy()") });
_addSpec({ _name: "DeputyPauseModule", _sel: _getSel("usedNonces(bytes32)") });
_addSpec({ _name: "DeputyPauseModule", _sel: _getSel("pauseMessageTypehash()") });
_addSpec({ _name: "DeputyPauseModule", _sel: _getSel("deputyAuthMessageTypehash()") });
_addSpec({ _name: "DeputyPauseModule", _sel: _getSel("setDeputy(address,bytes)"), _auth: Role.DEPUTYGUARDIAN });
_addSpec({
_name: "DeputyPauseModule",
_sel: _getSel("setDeputyGuardianModule(address)"),
_auth: Role.DEPUTYGUARDIAN
});
_addSpec({ _name: "DeputyPauseModule", _sel: _getSel("eip712Domain()") });
_addSpec({ _name: "DeputyPauseModule", _sel: _getSel("pause(bytes32,bytes)"), _auth: Role.PAUSEDEPUTY });
// LivenessGuard // LivenessGuard
_addSpec({ _name: "LivenessGuard", _sel: _getSel("checkAfterExecution(bytes32,bool)"), _auth: Role.COUNCILSAFE }); _addSpec({ _name: "LivenessGuard", _sel: _getSel("checkAfterExecution(bytes32,bool)"), _auth: Role.COUNCILSAFE });
_addSpec({ _addSpec({
...@@ -987,8 +1006,9 @@ contract Specification_Test is CommonTest { ...@@ -987,8 +1006,9 @@ contract Specification_Test is CommonTest {
/// @notice Ensures that the DeputyGuardian is authorized to take all Guardian actions. /// @notice Ensures that the DeputyGuardian is authorized to take all Guardian actions.
function test_deputyGuardianAuth_works() public view { function test_deputyGuardianAuth_works() public view {
assertEq(specsByRole[Role.DEPUTYGUARDIAN].length, specsByRole[Role.GUARDIAN].length); // Additional 2 roles for the DeputyPauseModule.
assertEq(specsByRole[Role.DEPUTYGUARDIAN].length, 5); assertEq(specsByRole[Role.GUARDIAN].length, 5);
assertEq(specsByRole[Role.DEPUTYGUARDIAN].length, specsByRole[Role.GUARDIAN].length + 2);
mapping(bytes4 => Spec) storage dgmFuncSpecs = specs["DeputyGuardianModule"]; mapping(bytes4 => Spec) storage dgmFuncSpecs = specs["DeputyGuardianModule"];
mapping(bytes4 => Spec) storage superchainConfigFuncSpecs = specs["SuperchainConfig"]; mapping(bytes4 => Spec) storage superchainConfigFuncSpecs = specs["SuperchainConfig"];
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment