Commit 843a6600 authored by Maurelian's avatar Maurelian Committed by GitHub

ctb: Add DeputyGuardianModule (#10201)

* ctb: Add DeputyGuardianModule

* ctb: Add Guardian actions on the Portal to deputy guardian module

* ctb: Bubble up returndata from module on errors

* ctb: move deputy guardian check to internal function

* ctb: Add snapshots

* ctb: Use encodeCall

* ctb: Fix natspec typos

* ctb: Add events to DeputyGuardianModule

* ctb: use custom errors in DeputyGuardianModule

* ctb: Update snapshots

* Reuse events from OptimismPortal

* Add test_noPortalCollisions_succeeds

* ctb: Refactor to getContractFunctionAbis with path and excludes arguments

* ctb: Import PortalErrors.sol for Unauthorized

* ctb: Fix function name and whitespace

* ctb: Fix test visibility
parent a4c47fe1
......@@ -144,13 +144,23 @@ library ForgeArtifacts {
}
/// @notice Returns the function ABIs of all L1 contracts.
function getL1ContractFunctionAbis() internal returns (Abi[] memory abis_) {
function getContractFunctionAbis(
string memory path,
string memory excludes
)
internal
returns (Abi[] memory abis_)
{
string[] memory command = new string[](3);
command[0] = Executables.bash;
command[1] = "-c";
command[2] = string.concat(
Executables.find,
" src/{L1,governance,universal/ProxyAdmin.sol} -type f -exec basename {} \\;",
" ",
path,
" -type f ",
bytes(excludes).length > 0 ? string.concat(" ! -name ", excludes, " ") : "",
"-exec basename {} \\;",
" | ",
Executables.sed,
" 's/\\.[^.]*$//'",
......@@ -164,7 +174,7 @@ library ForgeArtifacts {
for (uint256 i; i < contractNames.length; i++) {
string memory contractName = contractNames[i];
string[] memory methodIdentifiers = ForgeArtifacts.getMethodIdentifiers(contractName);
string[] memory methodIdentifiers = getMethodIdentifiers(contractName);
abis_[i].contractName = contractName;
abis_[i].entries = new AbiEntry[](methodIdentifiers.length);
for (uint256 j; j < methodIdentifiers.length; j++) {
......
......@@ -87,6 +87,10 @@
"initCodeHash": "0xd62e193d89b1661d34031227a45ce1eade9c2a89b0bd7f362f511d03cceef294",
"sourceCodeHash": "0xa304b4b556162323d69662b4dd9a1d073d55ec661494465489bb67f1e465e7b3"
},
"src/Safe/DeputyGuardianModule.sol": {
"initCodeHash": "0x8f6adc162587ac7150045c0cf4671f23e0453417a4b7006e39eb8cb58052dc58",
"sourceCodeHash": "0x8ebf09555561d475ec51c681033b8c567281f40f310ad47312b00710f6394d34"
},
"src/Safe/LivenessGuard.sol": {
"initCodeHash": "0x16ec47f0888391638814047a1735dbac849b48e256b2e20182bbb3186d950a3c",
"sourceCodeHash": "0x9633cea9b66077e222f470439fe3e9a31f3e33b4f7a5618374c44310fd234b24"
......
[
{
"inputs": [
{
"internalType": "contract Safe",
"name": "_safe",
"type": "address"
},
{
"internalType": "contract SuperchainConfig",
"name": "_superchainConfig",
"type": "address"
},
{
"internalType": "address",
"name": "_deputyGuardian",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "contract OptimismPortal2",
"name": "_portal",
"type": "address"
},
{
"internalType": "contract IDisputeGame",
"name": "_game",
"type": "address"
}
],
"name": "blacklistDisputeGame",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "deputyGuardian",
"outputs": [
{
"internalType": "address",
"name": "deputyGuardian_",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pause",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "safe",
"outputs": [
{
"internalType": "contract Safe",
"name": "safe_",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract OptimismPortal2",
"name": "_portal",
"type": "address"
},
{
"internalType": "GameType",
"name": "_gameType",
"type": "uint32"
}
],
"name": "setRespectedGameType",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "superchainConfig",
"outputs": [
{
"internalType": "contract SuperchainConfig",
"name": "superchainConfig_",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "unpause",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "version",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "contract IDisputeGame",
"name": "game",
"type": "address"
}
],
"name": "DisputeGameBlacklisted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "string",
"name": "identifier",
"type": "string"
}
],
"name": "Paused",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "GameType",
"name": "gameType",
"type": "uint32"
}
],
"name": "RespectedGameTypeSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [],
"name": "Unpaused",
"type": "event"
},
{
"inputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"name": "ExecutionFailed",
"type": "error"
},
{
"inputs": [],
"name": "Unauthorized",
"type": "error"
}
]
\ No newline at end of file
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Safe } from "safe-contracts/Safe.sol";
import { Enum } from "safe-contracts/common/Enum.sol";
import { SuperchainConfig } from "src/L1/SuperchainConfig.sol";
import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol";
import { IDisputeGame } from "src/dispute/interfaces/IDisputeGame.sol";
import { ISemver } from "src/universal/ISemver.sol";
import { Unauthorized } from "src/libraries/PortalErrors.sol";
import "src/libraries/DisputeTypes.sol";
/// @title DeputyGuardianModule
/// @notice This module is intended to be enabled on the Security Council Safe, which will own the Guardian role in the
/// SuperchainConfig contract. The DeputyGuardianModule should allow a Deputy Guardian to administer any of the
/// actions that the Guardian is authorized to take. The security council can revoke the Deputy Guardian's
/// authorization at any time by disabling this module.
contract DeputyGuardianModule is ISemver {
/// @notice Error message for failed transaction execution
error ExecutionFailed(string);
/// @notice Emitted when the SuperchainConfig is paused
event Paused(string identifier);
/// @notice Emitted when the SuperchainConfig is unpaused
event Unpaused();
/// @notice Emitted when a DisputeGame is blacklisted
event DisputeGameBlacklisted(IDisputeGame game);
/// @notice Emitted when the respected game type is set
event RespectedGameTypeSet(GameType gameType);
/// @notice The Safe contract instance
Safe internal immutable SAFE;
/// @notice The SuperchainConfig's address
SuperchainConfig internal immutable SUPERCHAIN_CONFIG;
/// @notice The deputy guardian's address
address internal immutable DEPUTY_GUARDIAN;
/// @notice Semantic version.
/// @custom:semver 1.0.0
string public constant version = "1.0.0";
// Constructor to initialize the Safe and baseModule instances
constructor(Safe _safe, SuperchainConfig _superchainConfig, address _deputyGuardian) {
SAFE = _safe;
SUPERCHAIN_CONFIG = _superchainConfig;
DEPUTY_GUARDIAN = _deputyGuardian;
}
/// @notice Getter function for the Safe contract instance
/// @return safe_ The Safe contract instance
function safe() public view returns (Safe safe_) {
safe_ = SAFE;
}
/// @notice Getter function for the SuperchainConfig's address
/// @return superchainConfig_ The SuperchainConfig's address
function superchainConfig() public view returns (SuperchainConfig superchainConfig_) {
superchainConfig_ = SUPERCHAIN_CONFIG;
}
/// @notice Getter function for the deputy guardian's address
/// @return deputyGuardian_ The deputy guardian's address
function deputyGuardian() public view returns (address deputyGuardian_) {
deputyGuardian_ = DEPUTY_GUARDIAN;
}
/// @notice Internal function to ensure that only the deputy guardian can call certain functions.
function _onlyDeputyGuardian() internal view {
if (msg.sender != DEPUTY_GUARDIAN) {
revert Unauthorized();
}
}
/// @notice Calls the Security Council Safe's `execTransactionFromModuleReturnData()`, with the arguments
/// necessary to call `pause()` on the `SuperchainConfig` contract.
/// Only the deputy guardian can call this function.
function pause() external {
_onlyDeputyGuardian();
bytes memory data = abi.encodeCall(SUPERCHAIN_CONFIG.pause, ("Deputy Guardian"));
(bool success, bytes memory returnData) =
SAFE.execTransactionFromModuleReturnData(address(SUPERCHAIN_CONFIG), 0, data, Enum.Operation.Call);
if (!success) {
revert ExecutionFailed(string(returnData));
}
emit Paused("Deputy Guardian");
}
/// @notice Calls the Security Council Safe's `execTransactionFromModuleReturnData()`, with the arguments
/// necessary to call `unpause()` on the `SuperchainConfig` contract.
/// Only the deputy guardian can call this function.
function unpause() external {
_onlyDeputyGuardian();
bytes memory data = abi.encodeCall(SUPERCHAIN_CONFIG.unpause, ());
(bool success, bytes memory returnData) =
SAFE.execTransactionFromModuleReturnData(address(SUPERCHAIN_CONFIG), 0, data, Enum.Operation.Call);
if (!success) {
revert ExecutionFailed(string(returnData));
}
emit Unpaused();
}
/// @notice Calls the Security Council Safe's `execTransactionFromModuleReturnData()`, with the arguments
/// necessary to call `blacklistDisputeGame()` on the `OptimismPortal2` contract.
/// Only the deputy guardian can call this function.
/// @param _portal The `OptimismPortal2` contract instance.
/// @param _game The `IDisputeGame` contract instance.
function blacklistDisputeGame(OptimismPortal2 _portal, IDisputeGame _game) external {
_onlyDeputyGuardian();
bytes memory data = abi.encodeCall(OptimismPortal2.blacklistDisputeGame, (_game));
(bool success, bytes memory returnData) =
SAFE.execTransactionFromModuleReturnData(address(_portal), 0, data, Enum.Operation.Call);
if (!success) {
revert ExecutionFailed(string(returnData));
}
emit DisputeGameBlacklisted(_game);
}
/// @notice Calls the Security Council Safe's `execTransactionFromModuleReturnData()`, with the arguments
/// necessary to call `setRespectedGameType()` on the `OptimismPortal2` contract.
/// Only the deputy guardian can call this function.
/// @param _portal The `OptimismPortal2` contract instance.
/// @param _gameType The `GameType` to set as the respected game type.
function setRespectedGameType(OptimismPortal2 _portal, GameType _gameType) external {
_onlyDeputyGuardian();
bytes memory data = abi.encodeCall(OptimismPortal2.setRespectedGameType, (_gameType));
(bool success, bytes memory returnData) =
SAFE.execTransactionFromModuleReturnData(address(_portal), 0, data, Enum.Operation.Call);
if (!success) {
revert ExecutionFailed(string(returnData));
}
emit RespectedGameTypeSet(_gameType);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { CommonTest } from "test/setup/CommonTest.sol";
import { ForgeArtifacts, Abi } from "scripts/ForgeArtifacts.sol";
import { Safe } from "safe-contracts/Safe.sol";
import "test/safe-tools/SafeTestTools.sol";
import { IDisputeGame } from "src/dispute/interfaces/IDisputeGame.sol";
import { DeputyGuardianModule } from "src/Safe/DeputyGuardianModule.sol";
import { GameType } from "src/libraries/DisputeTypes.sol";
contract DeputyGuardianModule_TestInit is CommonTest, SafeTestTools {
using SafeTestLib for SafeInstance;
error Unauthorized();
error ExecutionFailed(string);
event DisputeGameBlacklisted(IDisputeGame);
event RespectedGameTypeSet(GameType);
event ExecutionFromModuleSuccess(address indexed);
DeputyGuardianModule deputyGuardianModule;
SafeInstance safeInstance;
address deputyGuardian;
/// @dev Sets up the test environment
function setUp() public virtual override {
super.enableFaultProofs();
super.setUp();
// Create a Safe with 10 owners
(, uint256[] memory keys) = SafeTestLib.makeAddrsAndKeys("moduleTest", 10);
safeInstance = _setupSafe(keys, 10);
// Set the Safe as the Guardian of the SuperchainConfig
vm.store(
address(superchainConfig),
superchainConfig.GUARDIAN_SLOT(),
bytes32(uint256(uint160(address(safeInstance.safe))))
);
deputyGuardian = makeAddr("deputyGuardian");
deputyGuardianModule = new DeputyGuardianModule({
_safe: safeInstance.safe,
_superchainConfig: superchainConfig,
_deputyGuardian: deputyGuardian
});
safeInstance.enableModule(address(deputyGuardianModule));
}
}
contract DeputyGuardianModule_Getters_Test is DeputyGuardianModule_TestInit {
/// @dev Tests that the constructor sets the correct values
function test_getters_works() external view {
assertEq(address(deputyGuardianModule.safe()), address(safeInstance.safe));
assertEq(address(deputyGuardianModule.deputyGuardian()), address(deputyGuardian));
assertEq(address(deputyGuardianModule.superchainConfig()), address(superchainConfig));
}
}
contract DeputyGuardianModule_Pause_Test is DeputyGuardianModule_TestInit {
/// @dev Tests that `pause` successfully pauses when called by the deputy guardian.
function test_pause_succeeds() external {
vm.expectEmit(address(superchainConfig));
emit Paused("Deputy Guardian");
vm.expectEmit(address(safeInstance.safe));
emit ExecutionFromModuleSuccess(address(deputyGuardianModule));
vm.expectEmit(address(deputyGuardianModule));
emit Paused("Deputy Guardian");
vm.prank(address(deputyGuardian));
deputyGuardianModule.pause();
assertEq(superchainConfig.paused(), true);
}
}
contract DeputyGuardianModule_Pause_TestFail is DeputyGuardianModule_TestInit {
/// @dev Tests that `pause` reverts when called by a non deputy guardian.
function test_pause_notDeputyGuardian_reverts() external {
vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector));
deputyGuardianModule.pause();
}
/// @dev Tests that when the call from the Safe reverts, the error message is returned.
function test_pause_targetReverts_reverts() external {
vm.mockCallRevert(
address(superchainConfig),
abi.encodeWithSelector(superchainConfig.pause.selector),
"SuperchainConfig: pause() reverted"
);
vm.prank(address(deputyGuardian));
vm.expectRevert(abi.encodeWithSelector(ExecutionFailed.selector, "SuperchainConfig: pause() reverted"));
deputyGuardianModule.pause();
}
}
contract DeputyGuardianModule_Unpause_Test is DeputyGuardianModule_TestInit {
/// @dev Sets up the test environment with the SuperchainConfig paused
function setUp() public override {
super.setUp();
vm.prank(address(deputyGuardian));
deputyGuardianModule.pause();
assertTrue(superchainConfig.paused());
}
/// @dev Tests that `unpause` successfully unpauses when called by the deputy guardian.
function test_unpause_succeeds() external {
vm.expectEmit(address(superchainConfig));
emit Unpaused();
vm.expectEmit(address(safeInstance.safe));
emit ExecutionFromModuleSuccess(address(deputyGuardianModule));
vm.expectEmit(address(deputyGuardianModule));
emit Unpaused();
vm.prank(address(deputyGuardian));
deputyGuardianModule.unpause();
assertFalse(superchainConfig.paused());
}
}
/// @dev Note that this contract inherits from DeputyGuardianModule_Unpause_Test to ensure that the SuperchainConfig is
/// paused before the tests are run.
contract DeputyGuardianModule_Unpause_TestFail is DeputyGuardianModule_Unpause_Test {
/// @dev Tests that `unpause` reverts when called by a non deputy guardian.
function test_unpause_notDeputyGuardian_reverts() external {
vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector));
deputyGuardianModule.unpause();
assertTrue(superchainConfig.paused());
}
/// @dev Tests that when the call from the Safe reverts, the error message is returned.
function test_unpause_targetReverts_reverts() external {
vm.mockCallRevert(
address(superchainConfig),
abi.encodeWithSelector(superchainConfig.unpause.selector),
"SuperchainConfig: unpause reverted"
);
vm.prank(address(deputyGuardian));
vm.expectRevert(abi.encodeWithSelector(ExecutionFailed.selector, "SuperchainConfig: unpause reverted"));
deputyGuardianModule.unpause();
}
}
contract DeputyGuardianModule_BlacklistDisputeGame_Test is DeputyGuardianModule_TestInit {
/// @dev Tests that `blacklistDisputeGame` successfully blacklists a dispute game when called by the deputy
/// guardian.
function test_blacklistDisputeGame_succeeds() external {
IDisputeGame game = IDisputeGame(makeAddr("game"));
vm.expectEmit(address(safeInstance.safe));
emit ExecutionFromModuleSuccess(address(deputyGuardianModule));
vm.expectEmit(address(deputyGuardianModule));
emit DisputeGameBlacklisted(game);
vm.prank(address(deputyGuardian));
deputyGuardianModule.blacklistDisputeGame(optimismPortal2, game);
assertTrue(optimismPortal2.disputeGameBlacklist(game));
}
}
contract DeputyGuardianModule_BlacklistDisputeGame_TestFail is DeputyGuardianModule_TestInit {
/// @dev Tests that `blacklistDisputeGame` reverts when called by a non deputy guardian.
function test_blacklistDisputeGame_notDeputyGuardian_reverts() external {
IDisputeGame game = IDisputeGame(makeAddr("game"));
vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector));
deputyGuardianModule.blacklistDisputeGame(optimismPortal2, game);
assertFalse(optimismPortal2.disputeGameBlacklist(game));
}
/// @dev Tests that when the call from the Safe reverts, the error message is returned.
function test_blacklistDisputeGame_targetReverts_reverts() external {
vm.mockCallRevert(
address(optimismPortal2),
abi.encodeWithSelector(optimismPortal2.blacklistDisputeGame.selector),
"OptimismPortal2: blacklistDisputeGame reverted"
);
IDisputeGame game = IDisputeGame(makeAddr("game"));
vm.prank(address(deputyGuardian));
vm.expectRevert(
abi.encodeWithSelector(ExecutionFailed.selector, "OptimismPortal2: blacklistDisputeGame reverted")
);
deputyGuardianModule.blacklistDisputeGame(optimismPortal2, game);
}
}
contract DeputyGuardianModule_setRespectedGameType_Test is DeputyGuardianModule_TestInit {
/// @dev Tests that `setRespectedGameType` successfully updates the respected game type when called by the deputy
/// guardian.
function testFuzz_setRespectedGameType_succeeds(GameType _gameType) external {
vm.expectEmit(address(safeInstance.safe));
emit ExecutionFromModuleSuccess(address(deputyGuardianModule));
vm.expectEmit(address(deputyGuardianModule));
emit RespectedGameTypeSet(_gameType);
vm.prank(address(deputyGuardian));
deputyGuardianModule.setRespectedGameType(optimismPortal2, _gameType);
assertEq(GameType.unwrap(optimismPortal2.respectedGameType()), GameType.unwrap(_gameType));
assertEq(optimismPortal2.respectedGameTypeUpdatedAt(), uint64(block.timestamp));
}
}
contract DeputyGuardianModule_setRespectedGameType_TestFail is DeputyGuardianModule_TestInit {
/// @dev Tests that `setRespectedGameType` when called by a non deputy guardian.
function testFuzz_setRespectedGameType_notDeputyGuardian_reverts(GameType _gameType) external {
vm.assume(GameType.unwrap(optimismPortal2.respectedGameType()) != GameType.unwrap(_gameType));
vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector));
deputyGuardianModule.setRespectedGameType(optimismPortal2, _gameType);
assertNotEq(GameType.unwrap(optimismPortal2.respectedGameType()), GameType.unwrap(_gameType));
}
/// @dev Tests that when the call from the Safe reverts, the error message is returned.
function test_setRespectedGameType_targetReverts_reverts() external {
vm.mockCallRevert(
address(optimismPortal2),
abi.encodeWithSelector(optimismPortal2.setRespectedGameType.selector),
"OptimismPortal2: setRespectedGameType reverted"
);
GameType gameType = GameType.wrap(1);
vm.prank(address(deputyGuardian));
vm.expectRevert(
abi.encodeWithSelector(ExecutionFailed.selector, "OptimismPortal2: setRespectedGameType reverted")
);
deputyGuardianModule.setRespectedGameType(optimismPortal2, gameType);
}
}
contract DeputyGuardianModule_NoPortalCollisions_Test is DeputyGuardianModule_TestInit {
/// @dev tests that no function selectors in the L1 contracts collide with the OptimismPortal2 functions called by
/// the DeputyGuardianModule.
function test_noPortalCollisions_succeeds() external {
Abi[] memory abis = ForgeArtifacts.getContractFunctionAbis("src/{L1,dispute,universal}/", "OptimismPortal2.sol");
for (uint256 i; i < abis.length; i++) {
for (uint256 j; j < abis[i].entries.length; j++) {
bytes4 sel = abis[i].entries[j].sel;
assertNotEq(sel, optimismPortal2.blacklistDisputeGame.selector);
assertNotEq(sel, optimismPortal2.setRespectedGameType.selector);
}
}
}
}
......@@ -2,7 +2,6 @@
pragma solidity ^0.8.15;
import { CommonTest } from "test/setup/CommonTest.sol";
import { Abi, AbiEntry } from "scripts/ForgeArtifacts.sol";
import { Executables } from "scripts/Executables.sol";
import { console2 as console } from "forge-std/console2.sol";
import { ProtocolVersions } from "src/L1/ProtocolVersions.sol";
......@@ -10,7 +9,7 @@ import { OptimismPortal } from "src/L1/OptimismPortal.sol";
import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol";
import { SystemConfig } from "src/L1/SystemConfig.sol";
import { DataAvailabilityChallenge } from "src/L1/DataAvailabilityChallenge.sol";
import { ForgeArtifacts } from "scripts/ForgeArtifacts.sol";
import { ForgeArtifacts, Abi, AbiEntry } from "scripts/ForgeArtifacts.sol";
/// @title Specification_Test
/// @dev Specifies common security properties of entrypoints to L1 contracts, including authorization and
......@@ -476,7 +475,7 @@ contract Specification_Test is CommonTest {
/// @notice Ensures that there's an auth spec for every L1 contract function.
function testContractAuth() public {
Abi[] memory abis = ForgeArtifacts.getL1ContractFunctionAbis();
Abi[] memory abis = ForgeArtifacts.getContractFunctionAbis("src/{L1,governance,universal/ProxyAdmin.sol}", "");
for (uint256 i = 0; i < abis.length; i++) {
string memory contractName = abis[i].contractName;
......
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