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);
}
}
This diff is collapsed.
......@@ -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