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 (
var excludeContracts = []string{
// External dependencies
"IERC20", "IERC721", "IERC721Enumerable", "IERC721Upgradeable", "IERC721Metadata",
"IERC20", "IERC721", "IERC5267", "IERC721Enumerable", "IERC721Upgradeable", "IERC721Metadata",
"IERC165", "IERC165Upgradeable", "ERC721TokenReceiver", "ERC1155TokenReceiver",
"ERC777TokensRecipient", "Guard", "IProxy", "Vm", "VmSafe", "IMulticall3",
"IERC721TokenReceiver", "IProxyCreationCallback", "IBeacon",
"IERC721TokenReceiver", "IProxyCreationCallback", "IBeacon", "IEIP712",
// EAS
"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 @@
"initCodeHash": "0x5eaf823d81995ce1f703f26e31049c54c1d4902dd9873a0b4645d470f2f459a2",
"sourceCodeHash": "0x17236a91c4171ae9525eae0e59fa65bb2dc320d62677cfc7d7eb942f182619fb"
},
"src/safe/DeputyPauseModule.sol": {
"initCodeHash": "0x55f0b6c1a102114b8c30157e7295da7058ba8d1b93f7d4070028286968611279",
"sourceCodeHash": "0xec92b5ccbd3ee337897464546ccb8306fca245bbb6ce4b0941d86bc82e0f4cfe"
},
"src/safe/LivenessGuard.sol": {
"initCodeHash": "0xc8e29e8b12f423c8cd229a38bc731240dd815d96f1b0ab96c71494dde63f6a81",
"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);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
// Testing
import { CommonTest } from "test/setup/CommonTest.sol";
import { GnosisSafe as Safe } from "safe-contracts/GnosisSafe.sol";
import "test/safe-tools/SafeTestTools.sol";
// Scripts
import { DeployUtils } from "scripts/libraries/DeployUtils.sol";
// Interfaces
import { IDeputyGuardianModule } from "interfaces/safe/IDeputyGuardianModule.sol";
import { IDeputyPauseModule } from "interfaces/safe/IDeputyPauseModule.sol";
/// @title DeputyPauseModule_TestInit
/// @notice Base test setup for the DeputyPauseModule.
contract DeputyPauseModule_TestInit is CommonTest, SafeTestTools {
using SafeTestLib for SafeInstance;
event ExecutionFromModuleSuccess(address indexed);
event DeputySet(address indexed);
event DeputyGuardianModuleSet(IDeputyGuardianModule indexed);
event PauseTriggered(address indexed deputy, bytes32 nonce);
IDeputyPauseModule deputyPauseModule;
IDeputyGuardianModule deputyGuardianModule;
SafeInstance securityCouncilSafeInstance;
SafeInstance foundationSafeInstance;
address deputy;
uint256 deputyKey;
bytes deputyAuthSignature;
bytes32 constant SOME_VALID_NONCE = keccak256("some valid nonce");
bytes32 constant PAUSE_MESSAGE_TYPEHASH = keccak256("PauseMessage(bytes32 nonce)");
bytes32 constant DEPUTY_AUTH_MESSAGE_TYPEHASH = keccak256("DeputyAuthMessage(address deputy)");
/// @notice Sets up the test environment.
function setUp() public virtual override {
super.setUp();
// Set up 20 keys.
(, uint256[] memory keys) = SafeTestLib.makeAddrsAndKeys("DeputyPauseModule_test_", 20);
// Split into two sets of 10 keys.
uint256[] memory keys1 = new uint256[](10);
uint256[] memory keys2 = new uint256[](10);
for (uint256 i; i < 10; i++) {
keys1[i] = keys[i];
keys2[i] = keys[i + 10];
}
// Create a Security Council Safe with 10 owners.
securityCouncilSafeInstance = _setupSafe(keys1, 10);
// Create a Foundation Safe with 10 different owners.
foundationSafeInstance = _setupSafe(keys2, 10);
// Set the Security Council Safe as the Guardian of the SuperchainConfig.
vm.store(
address(superchainConfig),
superchainConfig.GUARDIAN_SLOT(),
bytes32(uint256(uint160(address(securityCouncilSafeInstance.safe))))
);
// Create a DeputyGuardianModule and set the Foundation Safe as the Deputy Guardian.
deputyGuardianModule = IDeputyGuardianModule(
DeployUtils.create1({
_name: "DeputyGuardianModule",
_args: DeployUtils.encodeConstructor(
abi.encodeCall(
IDeputyGuardianModule.__constructor__,
(securityCouncilSafeInstance.safe, superchainConfig, address(foundationSafeInstance.safe))
)
)
})
);
// Enable the DeputyGuardianModule on the Security Council Safe.
securityCouncilSafeInstance.enableModule(address(deputyGuardianModule));
// Create the deputy for the DeputyPauseModule.
(deputy, deputyKey) = makeAddrAndKey("deputy");
// Create the deputy auth signature.
deputyAuthSignature = makeAuthSignature(getNextContract(), deputyKey, deputy);
// Create the DeputyPauseModule.
deputyPauseModule = IDeputyPauseModule(
DeployUtils.create1({
_name: "DeputyPauseModule",
_args: DeployUtils.encodeConstructor(
abi.encodeCall(
IDeputyPauseModule.__constructor__,
(foundationSafeInstance.safe, deputyGuardianModule, superchainConfig, deputy, deputyAuthSignature)
)
)
})
);
// Enable the DeputyPauseModule on the Foundation Safe.
foundationSafeInstance.enableModule(address(deputyPauseModule));
}
/// @notice Generates a signature to authenticate as the deputy.
/// @param _verifyingContract The verifying contract.
/// @param _privateKey The private key to use to sign the message.
/// @param _deputy The deputy to authenticate as.
/// @return Generated signature.
function makeAuthSignature(
address _verifyingContract,
uint256 _privateKey,
address _deputy
)
internal
view
returns (bytes memory)
{
return makeAuthSignature(block.chainid, _verifyingContract, _privateKey, _deputy);
}
/// @notice Generates a signature to authenticate as the deputy.
/// @param _chainId Chain ID to use for the domain separator.
/// @param _verifyingContract The verifying contract.
/// @param _privateKey The private key to use to sign the message.
/// @param _deputy The deputy to authenticate as.
/// @return Generated signature.
function makeAuthSignature(
uint256 _chainId,
address _verifyingContract,
uint256 _privateKey,
address _deputy
)
internal
pure
returns (bytes memory)
{
bytes32 structHash = keccak256(abi.encode(DEPUTY_AUTH_MESSAGE_TYPEHASH, _deputy));
bytes32 digest = hashTypedData(_verifyingContract, _chainId, structHash);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, digest);
return abi.encodePacked(r, s, v);
}
/// @notice Generates a signature to trigger a pause.
/// @param _verifyingContract The verifying contract.
/// @param _nonce Signature nonce.
/// @param _privateKey The private key to use to sign the message.
/// @return Generated signature.
function makePauseSignature(
address _verifyingContract,
bytes32 _nonce,
uint256 _privateKey
)
internal
view
returns (bytes memory)
{
return makePauseSignature(block.chainid, _verifyingContract, _nonce, _privateKey);
}
/// @notice Generates a signature to trigger a pause.
/// @param _chainId Chain ID to use for the domain separator.
/// @param _verifyingContract The verifying contract.
/// @param _nonce Signature nonce.
/// @param _privateKey The private key to use to sign the message.
/// @return Generated signature.
function makePauseSignature(
uint256 _chainId,
address _verifyingContract,
bytes32 _nonce,
uint256 _privateKey
)
internal
pure
returns (bytes memory)
{
bytes32 structHash = keccak256(abi.encode(PAUSE_MESSAGE_TYPEHASH, _nonce));
bytes32 digest = hashTypedData(_verifyingContract, _chainId, structHash);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, digest);
return abi.encodePacked(r, s, v);
}
/// @notice Helper function to compute EIP-712 typed data hash
/// @param _verifyingContract The verifying contract.
/// @param _chainId Chain ID to use for the domain separator.
/// @param _structHash The struct hash.
/// @return The EIP-712 typed data hash.
function hashTypedData(
address _verifyingContract,
uint256 _chainId,
bytes32 _structHash
)
internal
pure
returns (bytes32)
{
bytes32 DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256("DeputyPauseModule"),
keccak256("1"),
_chainId,
_verifyingContract
)
);
return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, _structHash));
}
/// @notice Gets the next contract that will be created by this test contract.
/// @return Address of the next contract to be created.
function getNextContract() internal view returns (address) {
return vm.computeCreateAddress(address(this), vm.getNonce(address(this)));
}
}
/// @title DeputyPauseModule_Constructor_Test
/// @notice Tests that the constructor works.
contract DeputyPauseModule_Constructor_Test is DeputyPauseModule_TestInit {
/// @notice Tests that the constructor works.
function test_constructor_validParameters_succeeds() external {
// Create the signature.
address nextContract = getNextContract();
bytes memory signature = makeAuthSignature(nextContract, deputyKey, deputy);
// Deploy the module.
vm.expectEmit(address(nextContract));
emit DeputySet(deputy);
deputyPauseModule = IDeputyPauseModule(
DeployUtils.create1({
_name: "DeputyPauseModule",
_args: DeployUtils.encodeConstructor(
abi.encodeCall(
IDeputyPauseModule.__constructor__,
(foundationSafeInstance.safe, deputyGuardianModule, superchainConfig, deputy, signature)
)
)
})
);
}
}
/// @title DeputyPauseModule_Constructor_TestFail
/// @notice Tests that the constructor fails when it should.
contract DeputyPauseModule_Constructor_TestFail is DeputyPauseModule_TestInit {
/// @notice Tests that the constructor reverts when the signature is not the deputy auth message.
function testFuzz_constructor_signatureNotNextContract_reverts(address _nextContract) external {
// Make sure that the next contract is not correct.
vm.assume(_nextContract != getNextContract());
// Create the signature.
bytes memory signature = makeAuthSignature(_nextContract, deputyKey, deputy);
// Expect a revert.
vm.expectRevert(abi.encodeWithSelector(IDeputyPauseModule.DeputyPauseModule_InvalidDeputy.selector));
IDeputyPauseModule(
DeployUtils.create1({
_name: "DeputyPauseModule",
_args: DeployUtils.encodeConstructor(
abi.encodeCall(
IDeputyPauseModule.__constructor__,
(foundationSafeInstance.safe, deputyGuardianModule, superchainConfig, deputy, signature)
)
)
})
);
}
/// @notice Tests that the constructor reverts when the signature is not the deputy auth message.
function testFuzz_constructor_signatureNotOverDeputy_reverts(address _deputy) external {
// Make sure that the deputy is not correct.
vm.assume(_deputy != deputy);
// Create the signature.
bytes memory signature = makeAuthSignature(getNextContract(), deputyKey, _deputy);
// Expect a revert.
vm.expectRevert(abi.encodeWithSelector(IDeputyPauseModule.DeputyPauseModule_InvalidDeputy.selector));
IDeputyPauseModule(
DeployUtils.create1({
_name: "DeputyPauseModule",
_args: DeployUtils.encodeConstructor(
abi.encodeCall(
IDeputyPauseModule.__constructor__,
(foundationSafeInstance.safe, deputyGuardianModule, superchainConfig, deputy, signature)
)
)
})
);
}
/// @notice Tests that the constructor reverts when the signature is not from the deputy.
function testFuzz_constructor_signatureNotFromDeputy_reverts(uint256 _privateKey) external {
// Make sure that the private key is not the deputy's private key.
vm.assume(_privateKey != deputyKey);
// Make sure that the private key is in the range of a valid secp256k1 private key.
_privateKey = bound(_privateKey, 1, SECP256K1_ORDER - 1);
// Create the signature.
bytes memory signature = makeAuthSignature(getNextContract(), _privateKey, deputy);
// Expect a revert.
vm.expectRevert(abi.encodeWithSelector(IDeputyPauseModule.DeputyPauseModule_InvalidDeputy.selector));
IDeputyPauseModule(
DeployUtils.create1({
_name: "DeputyPauseModule",
_args: DeployUtils.encodeConstructor(
abi.encodeCall(
IDeputyPauseModule.__constructor__,
(foundationSafeInstance.safe, deputyGuardianModule, superchainConfig, deputy, signature)
)
)
})
);
}
/// @notice Tests that the constructor reverts when the signature uses the wrong chain ID.
function testFuzz_constructor_wrongChainId_reverts(uint256 _chainId) external {
// Make sure that the chain ID is not the current chain ID.
vm.assume(_chainId != block.chainid);
// Create the signature.
bytes memory signature = makeAuthSignature(_chainId, getNextContract(), deputyKey, deputy);
// Expect a revert.
vm.expectRevert(abi.encodeWithSelector(IDeputyPauseModule.DeputyPauseModule_InvalidDeputy.selector));
IDeputyPauseModule(
DeployUtils.create1({
_name: "DeputyPauseModule",
_args: DeployUtils.encodeConstructor(
abi.encodeCall(
IDeputyPauseModule.__constructor__,
(foundationSafeInstance.safe, deputyGuardianModule, superchainConfig, deputy, signature)
)
)
})
);
}
/// @notice Tests that the constructor reverts when the signature is not the deputy auth message.
function test_constructor_signatureNotAuthMessage_reverts() external {
// Create the signature.
bytes memory signature = makePauseSignature(getNextContract(), bytes32(0), deputyKey);
// Expect a revert.
vm.expectRevert(abi.encodeWithSelector(IDeputyPauseModule.DeputyPauseModule_InvalidDeputy.selector));
IDeputyPauseModule(
DeployUtils.create1({
_name: "DeputyPauseModule",
_args: DeployUtils.encodeConstructor(
abi.encodeCall(
IDeputyPauseModule.__constructor__,
(foundationSafeInstance.safe, deputyGuardianModule, superchainConfig, deputy, signature)
)
)
})
);
}
}
/// @title DeputyPauseModule_Getters_Test
/// @notice Tests that the getters work.
contract DeputyPauseModule_Getters_Test is DeputyPauseModule_TestInit {
/// @notice Tests that the getters work.
function test_getters_works() external view {
assertEq(address(deputyPauseModule.foundationSafe()), address(foundationSafeInstance.safe));
assertEq(address(deputyPauseModule.deputyGuardianModule()), address(deputyGuardianModule));
assertEq(address(deputyPauseModule.superchainConfig()), address(superchainConfig));
assertEq(deputyPauseModule.deputy(), deputy);
assertEq(deputyPauseModule.pauseMessageTypehash(), PAUSE_MESSAGE_TYPEHASH);
assertEq(deputyPauseModule.deputyAuthMessageTypehash(), DEPUTY_AUTH_MESSAGE_TYPEHASH);
}
}
/// @title DeputyPauseModule_Pause_Test
/// @notice Tests that the pause() function works.
contract DeputyPauseModule_Pause_Test is DeputyPauseModule_TestInit {
/// @notice Tests that pause() successfully pauses when called by the deputy.
/// @param _nonce Signature nonce.
function testFuzz_pause_validParameters_succeeds(bytes32 _nonce) external {
vm.expectEmit(address(superchainConfig));
emit Paused("Deputy Guardian");
vm.expectEmit(address(securityCouncilSafeInstance.safe));
emit ExecutionFromModuleSuccess(address(deputyGuardianModule));
vm.expectEmit(address(deputyGuardianModule));
emit Paused("Deputy Guardian");
vm.expectEmit(address(foundationSafeInstance.safe));
emit ExecutionFromModuleSuccess(address(deputyPauseModule));
vm.expectEmit(address(deputyPauseModule));
emit PauseTriggered(deputy, _nonce);
// State assertions before the pause.
assertEq(deputyPauseModule.usedNonces(_nonce), false);
assertEq(superchainConfig.paused(), false);
// Trigger the pause.
bytes memory signature = makePauseSignature(address(deputyPauseModule), _nonce, deputyKey);
deputyPauseModule.pause(_nonce, signature);
// State assertions after the pause.
assertEq(deputyPauseModule.usedNonces(_nonce), true);
assertEq(superchainConfig.paused(), true);
}
/// @notice Tests that pause() succeeds when called with two different nonces if the
/// SuperchainConfig contract is not paused between calls.
/// @param _nonce1 First nonce.
/// @param _nonce2 Second nonce.
function testFuzz_pause_differentNonces_succeeds(bytes32 _nonce1, bytes32 _nonce2) external {
// Make sure that the nonces are different.
vm.assume(_nonce1 != _nonce2);
// Pause once.
bytes memory sig1 = makePauseSignature(address(deputyPauseModule), _nonce1, deputyKey);
deputyPauseModule.pause(_nonce1, sig1);
// Unpause.
vm.prank(address(securityCouncilSafeInstance.safe));
superchainConfig.unpause();
// Pause again with a different nonce.
bytes memory sig2 = makePauseSignature(address(deputyPauseModule), _nonce2, deputyKey);
deputyPauseModule.pause(_nonce2, sig2);
}
/// @notice Tests that pause() succeeds when called with two different nonces after the
/// superchain has already been paused between calls.
/// @param _nonce1 First nonce.
/// @param _nonce2 Second nonce.
function testFuzz_pause_differentNoncesAlreadyPaused_succeeds(bytes32 _nonce1, bytes32 _nonce2) external {
// Make sure that the nonces are different.
vm.assume(_nonce1 != _nonce2);
// Pause once.
bytes memory sig1 = makePauseSignature(address(deputyPauseModule), _nonce1, deputyKey);
deputyPauseModule.pause(_nonce1, sig1);
// Pause again with a different nonce.
bytes memory sig2 = makePauseSignature(address(deputyPauseModule), _nonce2, deputyKey);
deputyPauseModule.pause(_nonce2, sig2);
}
/// @notice Tests that pause() succeeds within 1 million gas.
function test_pause_withinMillionGas_succeeds() external {
bytes memory signature = makePauseSignature(address(deputyPauseModule), SOME_VALID_NONCE, deputyKey);
uint256 gasBefore = gasleft();
deputyPauseModule.pause(SOME_VALID_NONCE, signature);
uint256 gasUsed = gasBefore - gasleft();
// Ensure gas usage is within expected bounds.
// 1m is a conservative limit that means we can trigger the pause in most blocks. It would
// be prohibitively expensive to fill up blocks to prevent the pause from being triggered
// even at 1m gas for any prolonged duration. Means that we can always trigger the pause
// within a short period of time.
assertLt(gasUsed, 1000000);
}
}
/// @title DeputyPauseModule_Pause_TestFail
/// @notice Tests that the pause() function reverts when it should.
contract DeputyPauseModule_Pause_TestFail is DeputyPauseModule_TestInit {
/// @notice Tests that pause() reverts when called by an address other than the deputy.
/// @param _privateKey The private key to use to sign the message.
function testFuzz_pause_notDeputy_reverts(uint256 _privateKey) external {
// Make sure that the private key is in the range of a valid secp256k1 private key.
_privateKey = bound(_privateKey, 1, SECP256K1_ORDER - 1);
// Make sure that the private key is not the deputy's private key.
vm.assume(_privateKey != deputyKey);
// Expect a revert.
vm.expectRevert(abi.encodeWithSelector(IDeputyPauseModule.DeputyPauseModule_Unauthorized.selector));
bytes memory signature = makePauseSignature(address(deputyPauseModule), SOME_VALID_NONCE, _privateKey);
deputyPauseModule.pause(SOME_VALID_NONCE, signature);
}
/// @notice Tests that pause() reverts when the nonce has already been used.
/// @param _nonce Signature nonce.
function testFuzz_pause_nonceAlreadyUsed_reverts(bytes32 _nonce) external {
// Pause once.
bytes memory signature = makePauseSignature(address(deputyPauseModule), _nonce, deputyKey);
deputyPauseModule.pause(_nonce, signature);
// Unpause.
vm.prank(address(securityCouncilSafeInstance.safe));
superchainConfig.unpause();
// Expect that the nonce is now used.
assertEq(deputyPauseModule.usedNonces(_nonce), true);
// Pause again.
vm.expectRevert(abi.encodeWithSelector(IDeputyPauseModule.DeputyPauseModule_NonceAlreadyUsed.selector));
deputyPauseModule.pause(_nonce, signature);
}
/// @notice Tests that pause() reverts when the signature is longer than 65 bytes.
/// @param _length The length of the malformed signature.
function testFuzz_pause_signatureTooLong_reverts(uint256 _length) external {
// Make sure signature is longer than 65 bytes.
_length = bound(_length, 66, 1000);
// Create the malformed signature.
bytes memory signature = new bytes(_length);
// Expect a revert.
vm.expectRevert(abi.encodeWithSelector(IDeputyPauseModule.ECDSAInvalidSignatureLength.selector, _length));
deputyPauseModule.pause(SOME_VALID_NONCE, signature);
}
/// @notice Tests that pause() reverts when the signature is shorter than 65 bytes.
/// @param _length The length of the malformed signature.
function testFuzz_pause_signatureTooShort_reverts(uint256 _length) external {
// Make sure signature is shorter than 65 bytes.
_length = bound(_length, 0, 64);
// Create the malformed signature.
bytes memory signature = new bytes(_length);
// Expect a revert.
vm.expectRevert(abi.encodeWithSelector(IDeputyPauseModule.ECDSAInvalidSignatureLength.selector, _length));
deputyPauseModule.pause(SOME_VALID_NONCE, signature);
}
/// @notice Tests that pause() reverts when the chain ID is not the same as the chain ID that
/// the signature was created for.
/// @param _chainId Chain ID to use for the signature.
function testFuzz_pause_wrongChainId_reverts(uint256 _chainId) external {
// Make sure that the chain ID is not the current chain ID.
vm.assume(_chainId != block.chainid);
// Signature with the wrong chain ID.
bytes memory signature = makePauseSignature(_chainId, address(deputyPauseModule), SOME_VALID_NONCE, deputyKey);
vm.expectRevert(abi.encodeWithSelector(IDeputyPauseModule.DeputyPauseModule_Unauthorized.selector));
deputyPauseModule.pause(SOME_VALID_NONCE, signature);
}
/// @notice Tests that pause() reverts when the verifying contract is not the deputy pause module.
/// @param _verifyingContract The verifying contract.
function testFuzz_pause_wrongVerifyingContract_reverts(address _verifyingContract) external {
// Make sure that the verifying contract is not the deputy pause module.
vm.assume(_verifyingContract != address(deputyPauseModule));
// Expect a revert.
vm.expectRevert(abi.encodeWithSelector(IDeputyPauseModule.DeputyPauseModule_Unauthorized.selector));
bytes memory signature = makePauseSignature(_verifyingContract, SOME_VALID_NONCE, deputyKey);
deputyPauseModule.pause(SOME_VALID_NONCE, signature);
}
/// @notice Tests that the error message is returned when the call to the safe reverts.
function test_pause_targetReverts_reverts() external {
// Make sure that the SuperchainConfig pause() reverts.
vm.mockCallRevert(
address(superchainConfig),
abi.encodePacked(superchainConfig.pause.selector),
"SuperchainConfig: pause() reverted"
);
// Note that the error here will be somewhat awkwardly double-encoded because the
// DeputyGuardianModule will encode the revert message as an ExecutionFailed error and then
// the DeputyPauseModule will re-encode it as another ExecutionFailed error.
vm.expectRevert(
abi.encodeWithSelector(
IDeputyPauseModule.DeputyPauseModule_ExecutionFailed.selector,
string(
abi.encodeWithSelector(
IDeputyGuardianModule.ExecutionFailed.selector, "SuperchainConfig: pause() reverted"
)
)
)
);
bytes memory signature = makePauseSignature(address(deputyPauseModule), SOME_VALID_NONCE, deputyKey);
deputyPauseModule.pause(SOME_VALID_NONCE, signature);
}
/// @notice Tests that pause() reverts when the superchain is not in a paused state after the
/// transaction is sent.
function test_pause_superchainPauseFails_reverts() external {
// Make sure that the SuperchainConfig paused() returns false.
vm.mockCall(address(superchainConfig), abi.encodePacked(superchainConfig.paused.selector), abi.encode(false));
// Expect a revert.
vm.expectRevert(IDeputyPauseModule.DeputyPauseModule_SuperchainNotPaused.selector);
deputyPauseModule.pause(
SOME_VALID_NONCE, makePauseSignature(address(deputyPauseModule), SOME_VALID_NONCE, deputyKey)
);
}
}
/// @title DeputyPauseModule_SetDeputy_Test
/// @notice Tests that the setDeputy() function works.
contract DeputyPauseModule_SetDeputy_Test is DeputyPauseModule_TestInit {
/// @notice Tests that setDeputy() succeeds when called from the safe.
/// @param _seed Seed used to generate a private key.
function testFuzz_setDeputy_fromSafe_succeeds(bytes32 _seed) external {
(address newDeputy, uint256 newDeputyKey) = makeAddrAndKey(string(abi.encodePacked(_seed)));
// Make sure the private key is not the existing deputy's private key.
vm.assume(newDeputyKey != deputyKey);
// Sign the message.
bytes memory signature = makeAuthSignature(address(deputyPauseModule), newDeputyKey, newDeputy);
// Set the deputy address.
vm.expectEmit(address(deputyPauseModule));
emit DeputySet(newDeputy);
vm.prank(address(foundationSafeInstance.safe));
deputyPauseModule.setDeputy(newDeputy, signature);
// Assert that the deputy address has been set.
assertEq(deputyPauseModule.deputy(), newDeputy);
}
}
/// @title DeputyPauseModule_SetDeputy_TestFail
/// @notice Tests that the setDeputy() function reverts when it should.
contract DeputyPauseModule_SetDeputy_TestFail is DeputyPauseModule_TestInit {
/// @notice Tests that setDeputy() reverts when called by an address other than the safe.
function testFuzz_setDeputy_notSafe_reverts(address _sender) external {
// Make sure that the sender is not the safe.
vm.assume(_sender != address(foundationSafeInstance.safe));
// Create the key.
(address newDeputy, uint256 newDeputyKey) = makeAddrAndKey("whatever");
// Sign the message.
bytes memory signature = makeAuthSignature(address(deputyPauseModule), newDeputyKey, newDeputy);
// Expect a revert.
vm.expectRevert(abi.encodeWithSelector(IDeputyPauseModule.DeputyPauseModule_NotFromSafe.selector));
deputyPauseModule.setDeputy(newDeputy, signature);
// Make sure deputy has not changed.
assertEq(deputyPauseModule.deputy(), deputy);
}
}
/// @title DeputyPauseModule_SetDeputyGuardianModule_Test
/// @notice Tests that the setDeputyGuardianModule() function works.
contract DeputyPauseModule_SetDeputyGuardianModule_Test is DeputyPauseModule_TestInit {
/// @notice Tests that setDeputyGuardianModule() succeeds when called from the safe.
function testFuzz_setDeputyGuardianModule_fromSafe_succeeds(address _newModule) external {
vm.assume(_newModule != address(0));
vm.assume(_newModule != address(deputyGuardianModule));
// Set the new DeputyGuardianModule
vm.expectEmit(address(deputyPauseModule));
emit DeputyGuardianModuleSet(IDeputyGuardianModule(_newModule));
vm.prank(address(foundationSafeInstance.safe));
deputyPauseModule.setDeputyGuardianModule(IDeputyGuardianModule(_newModule));
// Assert that the DeputyGuardianModule has been set
assertEq(address(deputyPauseModule.deputyGuardianModule()), _newModule);
}
}
/// @title DeputyPauseModule_SetDeputyGuardianModule_TestFail
/// @notice Tests that the setDeputyGuardianModule() function reverts when it should.
contract DeputyPauseModule_SetDeputyGuardianModule_TestFail is DeputyPauseModule_TestInit {
/// @notice Tests that setDeputyGuardianModule() reverts when called by an address other than the safe.
function testFuzz_setDeputyGuardianModule_notSafe_reverts(address _sender, address _newModule) external {
vm.assume(_sender != address(foundationSafeInstance.safe));
vm.assume(_newModule != address(0));
// Expect a revert when called from non-safe address
vm.prank(_sender);
vm.expectRevert(abi.encodeWithSelector(IDeputyPauseModule.DeputyPauseModule_NotFromSafe.selector));
deputyPauseModule.setDeputyGuardianModule(IDeputyGuardianModule(_newModule));
// Make sure DeputyGuardianModule has not changed
assertEq(address(deputyPauseModule.deputyGuardianModule()), address(deputyGuardianModule));
}
}
......@@ -34,6 +34,7 @@ contract Specification_Test is CommonTest {
SYSTEMCONFIGOWNER,
GUARDIAN,
DEPUTYGUARDIAN,
PAUSEDEPUTY,
MESSENGER,
L1PROXYADMINOWNER,
GOVERNANCETOKENOWNER,
......@@ -878,6 +879,24 @@ contract Specification_Test is CommonTest {
_addSpec({ _name: "DeputyGuardianModule", _sel: _getSel("superchainConfig()") });
_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
_addSpec({ _name: "LivenessGuard", _sel: _getSel("checkAfterExecution(bytes32,bool)"), _auth: Role.COUNCILSAFE });
_addSpec({
......@@ -987,8 +1006,9 @@ contract Specification_Test is CommonTest {
/// @notice Ensures that the DeputyGuardian is authorized to take all Guardian actions.
function test_deputyGuardianAuth_works() public view {
assertEq(specsByRole[Role.DEPUTYGUARDIAN].length, specsByRole[Role.GUARDIAN].length);
assertEq(specsByRole[Role.DEPUTYGUARDIAN].length, 5);
// Additional 2 roles for the DeputyPauseModule.
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 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