From 135c74aa3e3f173d4c446a5178a3490f21970536 Mon Sep 17 00:00:00 2001 From: kingster-will <83567446+kingster-will@users.noreply.github.com> Date: Fri, 26 Jan 2024 21:21:12 -0800 Subject: [PATCH] Introducing Governance support into Protocol (#35) --- contracts/AccessController.sol | 30 +- contracts/governance/Governable.sol | 56 +++ contracts/governance/Governance.sol | 43 +++ .../interfaces/governance/IGovernable.sol | 18 + .../interfaces/governance/IGovernance.sol | 31 ++ contracts/lib/Errors.sol | 10 + contracts/lib/GovernanceLib.sol | 18 + contracts/registries/ModuleRegistry.sol | 9 +- script/foundry/deployment/Main.s.sol | 12 +- test/foundry/AccessController.t.sol | 12 +- test/foundry/IPAccount.t.sol | 6 +- test/foundry/IPAccountMetaTx.t.sol | 9 +- test/foundry/ModuleRegistry.t.sol | 6 +- test/foundry/governance/Governance.t.sol | 319 ++++++++++++++++++ test/foundry/modules/ModuleBase.t.sol | 8 +- .../registries/metadata/IPAssetRenderer.t.sol | 8 +- 16 files changed, 566 insertions(+), 29 deletions(-) create mode 100644 contracts/governance/Governable.sol create mode 100644 contracts/governance/Governance.sol create mode 100644 contracts/interfaces/governance/IGovernable.sol create mode 100644 contracts/interfaces/governance/IGovernance.sol create mode 100644 contracts/lib/GovernanceLib.sol create mode 100644 test/foundry/governance/Governance.t.sol diff --git a/contracts/AccessController.sol b/contracts/AccessController.sol index 3310cb769..57f454421 100644 --- a/contracts/AccessController.sol +++ b/contracts/AccessController.sol @@ -10,6 +10,7 @@ import { IPAccountChecker } from "contracts/lib/registries/IPAccountChecker.sol" import { IIPAccount } from "contracts/interfaces/IIPAccount.sol"; import { AccessPermission } from "contracts/lib/AccessPermission.sol"; import { Errors } from "contracts/lib/Errors.sol"; +import { Governable } from "contracts/governance/Governable.sol"; /// @title AccessController /// @dev This contract is used to control access permissions for different function calls in the protocol. @@ -26,7 +27,7 @@ import { Errors } from "contracts/lib/Errors.sol"; /// - setPermission: Sets the permission for a specific function call. /// - getPermission: Returns the permission level for a specific function call. /// - checkPermission: Checks if a specific function call is allowed. -contract AccessController is IAccessController { +contract AccessController is IAccessController, Governable { using IPAccountChecker for IIPAccountRegistry; address public IP_ACCOUNT_REGISTRY; @@ -34,15 +35,20 @@ contract AccessController is IAccessController { mapping(address => mapping(address => mapping(address => mapping(bytes4 => uint8)))) public permissions; - // TODO: can only be called by protocol admin - function initialize(address ipAccountRegistry_, address moduleRegistry_) external { - IP_ACCOUNT_REGISTRY = ipAccountRegistry_; - MODULE_REGISTRY = moduleRegistry_; + constructor(address governance) Governable(governance) {} + + function initialize(address ipAccountRegistry, address moduleRegistry) external onlyProtocolAdmin { + IP_ACCOUNT_REGISTRY = ipAccountRegistry; + MODULE_REGISTRY = moduleRegistry; } /// @notice Sets the permission for all IPAccounts - function setGlobalPermission(address signer_, address to_, bytes4 func_, uint8 permission_) external { - // TODO: access controller can only be called by protocol admin + function setGlobalPermission( + address signer_, + address to_, + bytes4 func_, + uint8 permission_ + ) external onlyProtocolAdmin { if (signer_ == address(0)) { revert Errors.AccessController__SignerIsZeroAddress(); } @@ -65,7 +71,13 @@ contract AccessController is IAccessController { /// @param to_ The recipient of the transaction (support wildcard permission) /// @param func_ The function selector (support wildcard permission) /// @param permission_ The permission level (0 => ABSTAIN, 1 => ALLOW, 3 => DENY) - function setPermission(address ipAccount_, address signer_, address to_, bytes4 func_, uint8 permission_) external { + function setPermission( + address ipAccount_, + address signer_, + address to_, + bytes4 func_, + uint8 permission_ + ) external whenNotPaused { // IPAccount and signer does not support wildcard permission if (ipAccount_ == address(0)) { revert Errors.AccessController__IPAccountIsZeroAddress(); @@ -117,7 +129,7 @@ contract AccessController is IAccessController { address signer_, address to_, bytes4 func_ - ) external view returns (bool) { + ) external view whenNotPaused returns (bool) { // ipAccount_ can only call registered modules or set Permissions if (to_ != address(this) && !IModuleRegistry(MODULE_REGISTRY).isRegistered(to_)) { return false; diff --git a/contracts/governance/Governable.sol b/contracts/governance/Governable.sol new file mode 100644 index 000000000..214784aac --- /dev/null +++ b/contracts/governance/Governable.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +import { Errors } from "contracts/lib/Errors.sol"; +import { IGovernance } from "contracts/interfaces/governance/IGovernance.sol"; +import { IGovernable } from "../interfaces/governance/IGovernable.sol"; +import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import { GovernanceLib } from "contracts/lib/GovernanceLib.sol"; +/// @title Governable +/// @dev All contracts managed by governance should inherit from this contract. +abstract contract Governable is IGovernable { + /// @notice The address of the governance. + address public governance; + + /// @dev Ensures that the function is called by the protocol admin. + modifier onlyProtocolAdmin() { + if(!IGovernance(governance).hasRole(GovernanceLib.PROTOCOL_ADMIN, msg.sender)) { + revert Errors.Governance__OnlyProtocolAdmin(); + } + _; + } + + modifier whenNotPaused() { + if (IGovernance(governance).getState() == GovernanceLib.ProtocolState.Paused) { + revert Errors.Governance__ProtocolPaused(); + } + _; + } + + /// @notice Constructs a new Governable contract. + /// @param governance_ The address of the governance. + constructor(address governance_) { + if (governance_ == address(0)) revert Errors.Governance__ZeroAddress(); + governance = governance_; + emit GovernanceUpdated(governance); + } + + /// @notice Sets a new governance address. + /// @param newGovernance The address of the new governance. + function setGovernance(address newGovernance) external onlyProtocolAdmin { + if (newGovernance == address(0)) revert Errors.Governance__ZeroAddress(); + if (!ERC165Checker.supportsInterface(newGovernance, type(IGovernance).interfaceId)) + revert Errors.Governance__UnsupportedInterface("IGovernance"); + if (IGovernance(newGovernance).getState() != IGovernance(governance).getState()) + revert Errors.Governance__InconsistentState(); + governance = newGovernance; + emit GovernanceUpdated(newGovernance); + } + + /// @notice Returns the current governance address. + /// @return The address of the current governance. + function getGovernance() external view returns (address) { + return governance; + } +} diff --git a/contracts/governance/Governance.sol b/contracts/governance/Governance.sol new file mode 100644 index 000000000..1a875dbb6 --- /dev/null +++ b/contracts/governance/Governance.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +import { Errors } from "contracts/lib/Errors.sol"; +import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; +import { IGovernance } from "contracts/interfaces/governance/IGovernance.sol"; +import { GovernanceLib } from "contracts/lib/GovernanceLib.sol"; + +/// @title Governance +/// @dev This contract is used for governance of the protocol. +contract Governance is AccessControl, IGovernance { + GovernanceLib.ProtocolState internal state; + + /// @notice Creates a new Governance contract. + /// @param admin The address of the initial admin. + constructor(address admin) { + if (admin == address(0)) revert Errors.Governance__ZeroAddress(); + _grantRole(GovernanceLib.PROTOCOL_ADMIN, admin); + } + + /// @notice Sets the state of the protocol. + /// @param newState The new state of the protocol. + function setState(GovernanceLib.ProtocolState newState) external override { + if (!hasRole(GovernanceLib.PROTOCOL_ADMIN, msg.sender)) revert Errors.Governance__OnlyProtocolAdmin(); + if (newState == state) revert Errors.Governance__NewStateIsTheSameWithOldState(); + emit StateSet(msg.sender, state, newState, block.timestamp); + state = newState; + } + + /// @notice Returns the current state of the protocol. + /// @return The current state of the protocol. + function getState() external view override returns (GovernanceLib.ProtocolState) { + return state; + } + + /// @notice Checks if the contract supports a specific interface. + /// @param interfaceId The id of the interface. + /// @return True if the contract supports the interface, false otherwise. + function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + return (interfaceId == type(IGovernance).interfaceId || super.supportsInterface(interfaceId)); + } +} diff --git a/contracts/interfaces/governance/IGovernable.sol b/contracts/interfaces/governance/IGovernable.sol new file mode 100644 index 000000000..73046baef --- /dev/null +++ b/contracts/interfaces/governance/IGovernable.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + + +/// @title IGovernable +/// @notice This is the interface for the Lens Protocol main governance functions. +interface IGovernable { + /// @notice Emitted when the governance is updated + /// @param newGovernance The address of the new governance + event GovernanceUpdated(address indexed newGovernance); + /// @notice Sets the governance address + /// @param newGovernance The address of the new governance + function setGovernance(address newGovernance) external; + /// @notice Returns the current governance address + /// @return The address of the current governance + function getGovernance() external view returns (address); +} diff --git a/contracts/interfaces/governance/IGovernance.sol b/contracts/interfaces/governance/IGovernance.sol new file mode 100644 index 000000000..a876b6043 --- /dev/null +++ b/contracts/interfaces/governance/IGovernance.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol"; +import { GovernanceLib } from "contracts/lib/GovernanceLib.sol"; + +/// @title IGovernance +/// @dev This interface defines the governance functionality for the protocol. +interface IGovernance is IAccessControl { + /// @notice Emitted when the protocol state is set + /// @param account The address that triggered the state change + /// @param prevState The previous state of the protocol + /// @param newState The new state of the protocol + /// @param timestamp The time when the state change occurred + event StateSet( + address indexed account, + GovernanceLib.ProtocolState prevState, + GovernanceLib.ProtocolState newState, + uint256 timestamp + ); + + /// @notice Sets the state of the protocol + /// @dev This function can only be called by an account with the appropriate role + /// @param newState The new state to set for the protocol + function setState(GovernanceLib.ProtocolState newState) external; + + /// @notice Returns the current state of the protocol + /// @return The current state of the protocol + function getState() external view returns (GovernanceLib.ProtocolState); +} diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index 1f88a4ec8..b1e1848fa 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -5,6 +5,16 @@ pragma solidity ^0.8.19; /// @title Errors Library /// @notice Library for all Story Protocol contract errors. library Errors { + //////////////////////////////////////////////////////////////////////////// + // Governance // + //////////////////////////////////////////////////////////////////////////// + error Governance__OnlyProtocolAdmin(); + error Governance__ZeroAddress(); + error Governance__ProtocolPaused(); + error Governance__InconsistentState(); + error Governance__NewStateIsTheSameWithOldState(); + error Governance__UnsupportedInterface(string interfaceName); + //////////////////////////////////////////////////////////////////////////// // IPAccount // //////////////////////////////////////////////////////////////////////////// diff --git a/contracts/lib/GovernanceLib.sol b/contracts/lib/GovernanceLib.sol new file mode 100644 index 000000000..ae01de17e --- /dev/null +++ b/contracts/lib/GovernanceLib.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +/// @title Governance +/// @dev This library provides types for Story Protocol Governance. +library GovernanceLib { + + bytes32 public constant PROTOCOL_ADMIN = bytes32(0); + + /// @notice An enum containing the different states the protocol can be in. + /// @param Unpaused The unpaused state. + /// @param Paused The paused state. + enum ProtocolState { + Unpaused, + Paused + } +} diff --git a/contracts/registries/ModuleRegistry.sol b/contracts/registries/ModuleRegistry.sol index 4a511a6da..389464b60 100644 --- a/contracts/registries/ModuleRegistry.sol +++ b/contracts/registries/ModuleRegistry.sol @@ -6,18 +6,21 @@ import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry import { Errors } from "contracts/lib/Errors.sol"; import { IModule } from "contracts/interfaces/modules/base/IModule.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { Governable } from "contracts/governance/Governable.sol"; /// @title ModuleRegistry -contract ModuleRegistry is IModuleRegistry { +contract ModuleRegistry is IModuleRegistry, Governable { using Strings for *; mapping(string => address) public _modules; mapping(address => bool) public _isModule; + constructor(address governance) Governable(governance) {} + /// @notice Registers a new module in the protocol. /// @param name The name of the module. /// @param moduleAddress The address of the module. - function registerModule(string memory name, address moduleAddress) external { + function registerModule(string memory name, address moduleAddress) external onlyProtocolAdmin { // TODO: check can only called by protocol admin if (moduleAddress == address(0)) { revert Errors.ModuleRegistry__ModuleAddressZeroAddress(); @@ -45,7 +48,7 @@ contract ModuleRegistry is IModuleRegistry { /// @notice Removes a module from the protocol. /// @param name The name of the module to be removed. - function removeModule(string memory name) external { + function removeModule(string memory name) external onlyProtocolAdmin { if (bytes(name).length == 0) { revert Errors.ModuleRegistry__NameEmptyString(); } diff --git a/script/foundry/deployment/Main.s.sol b/script/foundry/deployment/Main.s.sol index e02ba158d..a5c38c43e 100644 --- a/script/foundry/deployment/Main.s.sol +++ b/script/foundry/deployment/Main.s.sol @@ -28,6 +28,7 @@ import { RoyaltyModule } from "contracts/modules/royalty-module/RoyaltyModule.so import { DisputeModule } from "contracts/modules/dispute-module/DisputeModule.sol"; import { MockERC721 } from "contracts/mocks/MockERC721.sol"; import { IPResolver } from "contracts/resolvers/IPResolver.sol"; +import { Governance } from "contracts/governance/Governance.sol"; // script import { StringUtil } from "script/foundry/utils/StringUtil.sol"; @@ -38,6 +39,8 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler { using StringUtil for uint256; using stdJson for string; + Governance public governance; + address public constant ERC6551_REGISTRY = address(0x000000006551c19487814612e58FE06813775758); AccessController public accessController; @@ -91,11 +94,16 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler { function _deployProtocolContracts(address accessControldeployer) private { string memory contractKey; + contractKey = "Governance"; + _predeploy(contractKey); + governance = new Governance(accessControldeployer); + _postdeploy(contractKey, address(governance)); + mockNft = new MockERC721(); contractKey = "AccessController"; _predeploy(contractKey); - accessController = new AccessController(); + accessController = new AccessController(address(governance)); _postdeploy(contractKey, address(accessController)); contractKey = "IPAccountImpl"; @@ -105,7 +113,7 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler { contractKey = "ModuleRegistry"; _predeploy(contractKey); - moduleRegistry = new ModuleRegistry(); + moduleRegistry = new ModuleRegistry(address(governance)); _postdeploy(contractKey, address(moduleRegistry)); contractKey = "LicenseRegistry"; diff --git a/test/foundry/AccessController.t.sol b/test/foundry/AccessController.t.sol index 2b65e4cbb..2cebcf1f0 100644 --- a/test/foundry/AccessController.t.sol +++ b/test/foundry/AccessController.t.sol @@ -18,6 +18,7 @@ import { MockAccessController } from "test/foundry/mocks/MockAccessController.so import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; import { MockModule } from "test/foundry/mocks/MockModule.sol"; import { MockOrchestratorModule } from "test/foundry/mocks/MockOrchestratorModule.sol"; +import { Governance } from "contracts/governance/Governance.sol"; contract AccessControllerTest is Test { AccessController public accessController; @@ -31,27 +32,24 @@ contract AccessControllerTest is Test { ERC6551Registry public erc6551Registry = new ERC6551Registry(); address owner = vm.addr(1); uint256 tokenId = 100; + Governance public governance; function setUp() public { - accessController = new AccessController(); + governance = new Governance(address(this)); + accessController = new AccessController(address(governance)); implementation = new IPAccountImpl(); ipAccountRegistry = new IPAccountRegistry( address(erc6551Registry), address(accessController), address(implementation) ); - moduleRegistry = new ModuleRegistry(); + moduleRegistry = new ModuleRegistry(address(governance)); accessController.initialize(address(ipAccountRegistry), address(moduleRegistry)); nft.mintId(owner, tokenId); address deployedAccount = ipAccountRegistry.registerIpAccount(block.chainid, address(nft), tokenId); ipAccount = IIPAccount(payable(deployedAccount)); mockModule = new MockModule(address(ipAccountRegistry), address(moduleRegistry), "MockModule"); - // moduleWithoutPermission = new MockModule( - // address(ipAccountRegistry), - // address(moduleRegistry), - // "ModuleWithoutPermission" - // ); } // test owner can set permission diff --git a/test/foundry/IPAccount.t.sol b/test/foundry/IPAccount.t.sol index 624551fa1..dcd4de1b1 100644 --- a/test/foundry/IPAccount.t.sol +++ b/test/foundry/IPAccount.t.sol @@ -14,6 +14,7 @@ import "contracts/registries/ModuleRegistry.sol"; import "test/foundry/mocks/MockAccessController.sol"; import "test/foundry/mocks/MockERC721.sol"; import "test/foundry/mocks/MockModule.sol"; +import { Governance } from "contracts/governance/Governance.sol"; contract IPAccountTest is Test { IPAccountRegistry public registry; @@ -21,10 +22,13 @@ contract IPAccountTest is Test { MockERC721 nft = new MockERC721(); ERC6551Registry public erc6551Registry = new ERC6551Registry(); MockAccessController public accessController = new MockAccessController(); - ModuleRegistry public moduleRegistry = new ModuleRegistry(); + ModuleRegistry public moduleRegistry; MockModule public module; + Governance public governance; function setUp() public { + governance = new Governance(address(this)); + moduleRegistry = new ModuleRegistry(address(governance)); implementation = new IPAccountImpl(); registry = new IPAccountRegistry(address(erc6551Registry), address(accessController), address(implementation)); module = new MockModule(address(registry), address(moduleRegistry), "MockModule"); diff --git a/test/foundry/IPAccountMetaTx.t.sol b/test/foundry/IPAccountMetaTx.t.sol index d05faf40e..226804d62 100644 --- a/test/foundry/IPAccountMetaTx.t.sol +++ b/test/foundry/IPAccountMetaTx.t.sol @@ -22,14 +22,15 @@ import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/Mes import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; import { MetaTx } from "contracts/lib/MetaTx.sol"; import "contracts/lib/AccessPermission.sol"; +import { Governance } from "contracts/governance/Governance.sol"; contract IPAccountMetaTxTest is Test { IPAccountRegistry public registry; IPAccountImpl public implementation; MockERC721 nft = new MockERC721(); ERC6551Registry public erc6551Registry = new ERC6551Registry(); - AccessController public accessController = new AccessController(); - ModuleRegistry public moduleRegistry = new ModuleRegistry(); + AccessController public accessController; + ModuleRegistry public moduleRegistry; MockModule public module; MockMetaTxModule public metaTxModule; @@ -37,8 +38,12 @@ contract IPAccountMetaTxTest is Test { uint256 public callerPrivateKey; address public owner; address public caller; + Governance public governance; function setUp() public { + governance = new Governance(address(this)); + accessController = new AccessController(address(governance)); + moduleRegistry = new ModuleRegistry(address(governance)); ownerPrivateKey = 0xA11111; callerPrivateKey = 0xB22222; owner = vm.addr(ownerPrivateKey); diff --git a/test/foundry/ModuleRegistry.t.sol b/test/foundry/ModuleRegistry.t.sol index 242186265..5ab5ac705 100644 --- a/test/foundry/ModuleRegistry.t.sol +++ b/test/foundry/ModuleRegistry.t.sol @@ -15,16 +15,20 @@ import { Errors } from "contracts/lib/Errors.sol"; import { MockAccessController } from "test/foundry/mocks/MockAccessController.sol"; import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; import { MockModule } from "test/foundry/mocks/MockModule.sol"; +import { Governance } from "contracts/governance/Governance.sol"; contract ModuleRegistryTest is Test { IPAccountRegistry public registry; IPAccountImpl public implementation; - ModuleRegistry public moduleRegistry = new ModuleRegistry(); + ModuleRegistry public moduleRegistry; ERC6551Registry public erc6551Registry = new ERC6551Registry(); MockAccessController public accessController = new MockAccessController(); MockModule public module; + Governance public governance; function setUp() public { + governance = new Governance(address(this)); + moduleRegistry = new ModuleRegistry(address(governance)); implementation = new IPAccountImpl(); registry = new IPAccountRegistry(address(erc6551Registry), address(accessController), address(implementation)); module = new MockModule(address(registry), address(moduleRegistry), "MockModule"); diff --git a/test/foundry/governance/Governance.t.sol b/test/foundry/governance/Governance.t.sol new file mode 100644 index 000000000..b08c19a14 --- /dev/null +++ b/test/foundry/governance/Governance.t.sol @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import { Test } from "forge-std/Test.sol"; + +import { ERC6551Registry } from "lib/reference/src/ERC6551Registry.sol"; +import { IERC6551Account } from "lib/reference/src/interfaces/IERC6551Account.sol"; + +import { AccessController } from "contracts/AccessController.sol"; +import { IIPAccount } from "contracts/interfaces/IIPAccount.sol"; +import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; +import { IPAccountImpl } from "contracts/IPAccountImpl.sol"; +import { AccessPermission } from "contracts/lib/AccessPermission.sol"; +import { Errors } from "contracts/lib/Errors.sol"; +import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; +import { ModuleRegistry } from "contracts/registries/ModuleRegistry.sol"; +import { MockAccessController } from "test/foundry/mocks/MockAccessController.sol"; +import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; +import { MockModule } from "test/foundry/mocks/MockModule.sol"; +import { MockOrchestratorModule } from "test/foundry/mocks/MockOrchestratorModule.sol"; +import { Governance } from "contracts/governance/Governance.sol"; +import { IGovernable } from "contracts/interfaces/governance/IGovernable.sol"; +import { GovernanceLib } from "contracts/lib/GovernanceLib.sol"; + +contract GovernanceTest is Test { + AccessController public accessController; + IPAccountRegistry public ipAccountRegistry; + IModuleRegistry public moduleRegistry; + IPAccountImpl public implementation; + MockERC721 nft = new MockERC721(); + MockModule public mockModule; + MockModule public moduleWithoutPermission; + IIPAccount public ipAccount; + ERC6551Registry public erc6551Registry = new ERC6551Registry(); + address owner = vm.addr(1); + uint256 tokenId = 100; + Governance public governance; + + function setUp() public { + governance = new Governance(address(this)); + accessController = new AccessController(address(governance)); + implementation = new IPAccountImpl(); + ipAccountRegistry = new IPAccountRegistry( + address(erc6551Registry), + address(accessController), + address(implementation) + ); + moduleRegistry = new ModuleRegistry(address(governance)); + accessController.initialize(address(ipAccountRegistry), address(moduleRegistry)); + nft.mintId(owner, tokenId); + address deployedAccount = ipAccountRegistry.registerIpAccount(block.chainid, address(nft), tokenId); + ipAccount = IIPAccount(payable(deployedAccount)); + + mockModule = new MockModule(address(ipAccountRegistry), address(moduleRegistry), "MockModule"); + } + + function test_Governance_registerModuleSuccess() public { + moduleRegistry.registerModule("MockModule", address(mockModule)); + assertEq(moduleRegistry.getModule("MockModule"), address(mockModule)); + assertTrue(moduleRegistry.isRegistered(address(mockModule))); + } + + function test_Governance_removeModuleSuccess() public { + moduleRegistry.registerModule("MockModule", address(mockModule)); + assertEq(moduleRegistry.getModule("MockModule"), address(mockModule)); + assertTrue(moduleRegistry.isRegistered(address(mockModule))); + moduleRegistry.removeModule("MockModule"); + assertEq(moduleRegistry.getModule("MockModule"), address(0)); + assertFalse(moduleRegistry.isRegistered(address(mockModule))); + } + + function test_Governance_setGlobalPermissionSuccess() public { + MockModule mockModule2 = new MockModule(address(ipAccountRegistry), address(moduleRegistry), "MockModule2"); + moduleRegistry.registerModule("MockModule2", address(mockModule2)); + accessController.setGlobalPermission( + address(mockModule), + address(mockModule2), + bytes4(0), + AccessPermission.ALLOW + ); + assertEq( + accessController.getPermission(address(0), address(mockModule), address(mockModule2), bytes4(0)), + AccessPermission.ALLOW + ); + } + + function test_Governance_revert_registerModuleWithNonAdmin() public { + vm.prank(address(0x777)); + vm.expectRevert(abi.encodeWithSelector(Errors.Governance__OnlyProtocolAdmin.selector)); + moduleRegistry.registerModule("MockModule", address(mockModule)); + } + + function test_Governance_revert_removeModuleWithNonAdmin() public { + moduleRegistry.registerModule("MockModule", address(mockModule)); + assertEq(moduleRegistry.getModule("MockModule"), address(mockModule)); + assertTrue(moduleRegistry.isRegistered(address(mockModule))); + vm.prank(address(0x777)); + vm.expectRevert(abi.encodeWithSelector(Errors.Governance__OnlyProtocolAdmin.selector)); + moduleRegistry.removeModule("MockModule"); + } + + function test_Governance_revert_setGlobalPermissionNonAdmin() public { + MockModule mockModule2 = new MockModule(address(ipAccountRegistry), address(moduleRegistry), "MockModule2"); + moduleRegistry.registerModule("MockModule2", address(mockModule2)); + vm.prank(address(0x777)); + vm.expectRevert(abi.encodeWithSelector(Errors.Governance__OnlyProtocolAdmin.selector)); + accessController.setGlobalPermission( + address(mockModule), + address(mockModule2), + bytes4(0), + AccessPermission.ALLOW + ); + } + + function test_Governance_registerModuleWithNewAdmin() public { + address newAdmin = vm.addr(3); + governance.grantRole(GovernanceLib.PROTOCOL_ADMIN, newAdmin); + vm.prank(newAdmin); + moduleRegistry.registerModule("MockModule", address(mockModule)); + } + + function test_Governance_setGlobalPermissionWithNewAdmin() public { + MockModule mockModule2 = new MockModule(address(ipAccountRegistry), address(moduleRegistry), "MockModule2"); + moduleRegistry.registerModule("MockModule2", address(mockModule2)); + + address newAdmin = vm.addr(3); + governance.grantRole(GovernanceLib.PROTOCOL_ADMIN, newAdmin); + + vm.prank(newAdmin); + accessController.setGlobalPermission( + address(mockModule), + address(mockModule2), + bytes4(0), + AccessPermission.ALLOW + ); + } + + function test_Governance_revert_registerModuleWithOldAdmin() public { + address newAdmin = vm.addr(3); + governance.grantRole(GovernanceLib.PROTOCOL_ADMIN, newAdmin); + governance.revokeRole(GovernanceLib.PROTOCOL_ADMIN, address(this)); + + vm.expectRevert(abi.encodeWithSelector(Errors.Governance__OnlyProtocolAdmin.selector)); + moduleRegistry.registerModule("MockModule", address(mockModule)); + } + + function test_Governance_revert_removeModuleWithOldAdmin() public { + moduleRegistry.registerModule("MockModule", address(mockModule)); + assertEq(moduleRegistry.getModule("MockModule"), address(mockModule)); + assertTrue(moduleRegistry.isRegistered(address(mockModule))); + + address newAdmin = vm.addr(3); + governance.grantRole(GovernanceLib.PROTOCOL_ADMIN, newAdmin); + governance.revokeRole(GovernanceLib.PROTOCOL_ADMIN, address(this)); + + vm.expectRevert(abi.encodeWithSelector(Errors.Governance__OnlyProtocolAdmin.selector)); + moduleRegistry.removeModule("MockModule"); + } + + function test_Governance_revert_setGlobalPermissionWithOldAdmin() public { + MockModule mockModule2 = new MockModule(address(ipAccountRegistry), address(moduleRegistry), "MockModule2"); + moduleRegistry.registerModule("MockModule2", address(mockModule2)); + + address newAdmin = vm.addr(3); + governance.grantRole(GovernanceLib.PROTOCOL_ADMIN, newAdmin); + governance.revokeRole(GovernanceLib.PROTOCOL_ADMIN, address(this)); + + vm.expectRevert(abi.encodeWithSelector(Errors.Governance__OnlyProtocolAdmin.selector)); + accessController.setGlobalPermission( + address(mockModule), + address(mockModule2), + bytes4(0), + AccessPermission.ALLOW + ); + } + + function test_Governance_setNewGovernance() public { + address newAdmin = vm.addr(3); + Governance newGovernance = new Governance(newAdmin); + IGovernable(address(moduleRegistry)).setGovernance(address(newGovernance)); + assertEq(IGovernable(address(moduleRegistry)).getGovernance(), address(newGovernance)); + } + + function test_Governance_registerModuleWithNewGov() public { + address newAdmin = vm.addr(3); + Governance newGovernance = new Governance(newAdmin); + IGovernable(address(moduleRegistry)).setGovernance(address(newGovernance)); + vm.prank(newAdmin); + moduleRegistry.registerModule("MockModule", address(mockModule)); + } + + function test_Governance_setGlobalPermissionWithNewGov() public { + MockModule mockModule2 = new MockModule(address(ipAccountRegistry), address(moduleRegistry), "MockModule2"); + moduleRegistry.registerModule("MockModule2", address(mockModule2)); + + address newAdmin = vm.addr(3); + Governance newGovernance = new Governance(newAdmin); + IGovernable(address(accessController)).setGovernance(address(newGovernance)); + + vm.prank(newAdmin); + accessController.setGlobalPermission( + address(mockModule), + address(mockModule2), + bytes4(0), + AccessPermission.ALLOW + ); + } + + function test_Governance_revert_registerModuleWithOldGov() public { + address newAdmin = vm.addr(3); + Governance newGovernance = new Governance(newAdmin); + IGovernable(address(moduleRegistry)).setGovernance(address(newGovernance)); + vm.expectRevert(abi.encodeWithSelector(Errors.Governance__OnlyProtocolAdmin.selector)); + moduleRegistry.registerModule("MockModule", address(mockModule)); + } + + function test_Governance_revert_removeModuleWithOldGov() public { + moduleRegistry.registerModule("MockModule", address(mockModule)); + assertEq(moduleRegistry.getModule("MockModule"), address(mockModule)); + assertTrue(moduleRegistry.isRegistered(address(mockModule))); + + address newAdmin = vm.addr(3); + Governance newGovernance = new Governance(newAdmin); + IGovernable(address(moduleRegistry)).setGovernance(address(newGovernance)); + vm.expectRevert(abi.encodeWithSelector(Errors.Governance__OnlyProtocolAdmin.selector)); + moduleRegistry.removeModule("MockModule"); + } + + function test_Governance_revert_setGlobalPermissionWithOldGov() public { + MockModule mockModule2 = new MockModule(address(ipAccountRegistry), address(moduleRegistry), "MockModule2"); + moduleRegistry.registerModule("MockModule2", address(mockModule2)); + + address newAdmin = vm.addr(3); + Governance newGovernance = new Governance(newAdmin); + IGovernable(address(accessController)).setGovernance(address(newGovernance)); + + vm.expectRevert(abi.encodeWithSelector(Errors.Governance__OnlyProtocolAdmin.selector)); + accessController.setGlobalPermission( + address(mockModule), + address(mockModule2), + bytes4(0), + AccessPermission.ALLOW + ); + } + + function test_Governance_revert_setNewGovernanceZeroAddr() public { + vm.expectRevert(abi.encodeWithSelector(Errors.Governance__ZeroAddress.selector)); + IGovernable(address(moduleRegistry)).setGovernance(address(0)); + } + + function test_Governance_revert_setNewGovernanceNotContract() public { + vm.expectRevert(); + IGovernable(address(moduleRegistry)).setGovernance(address(0xbeefbeef)); + } + + function test_Governance_revert_setNewGovernanceNotSupportInterface() public { + vm.expectRevert(abi.encodeWithSelector(Errors.Governance__UnsupportedInterface.selector, "IGovernance")); + IGovernable(address(moduleRegistry)).setGovernance(address(mockModule)); + } + + function test_Governance_revert_setNewGovernanceInconsistentState() public { + address newAdmin = vm.addr(3); + Governance newGovernance = new Governance(newAdmin); + vm.prank(newAdmin); + newGovernance.setState(GovernanceLib.ProtocolState.Paused); + vm.expectRevert(abi.encodeWithSelector(Errors.Governance__InconsistentState.selector)); + IGovernable(address(moduleRegistry)).setGovernance(address(newGovernance)); + } + + function test_Governance_revert_setPermissionWhenPaused() public { + MockModule mockModule2 = new MockModule(address(ipAccountRegistry), address(moduleRegistry), "MockModule2"); + moduleRegistry.registerModule("MockModule2", address(mockModule2)); + governance.setState(GovernanceLib.ProtocolState.Paused); + vm.expectRevert(abi.encodeWithSelector(Errors.Governance__ProtocolPaused.selector)); + accessController.setPermission( + address(ipAccount), + address(mockModule), + address(mockModule2), + bytes4(0), + AccessPermission.ALLOW + ); + } + + function test_Governance_revert_checkPermissionWhenPaused() public { + MockModule mockModule2 = new MockModule(address(ipAccountRegistry), address(moduleRegistry), "MockModule2"); + moduleRegistry.registerModule("MockModule2", address(mockModule2)); + governance.setState(GovernanceLib.ProtocolState.Paused); + vm.expectRevert(abi.encodeWithSelector(Errors.Governance__ProtocolPaused.selector)); + accessController.checkPermission(address(ipAccount), address(mockModule), address(mockModule2), bytes4(0)); + } + + function test_Governance_revert_checkPermissionUnPausedThenPauseThenUnPause() public { + MockModule mockModule2 = new MockModule(address(ipAccountRegistry), address(moduleRegistry), "MockModule2"); + moduleRegistry.registerModule("MockModule2", address(mockModule2)); + assertFalse( + accessController.checkPermission(address(ipAccount), address(mockModule), address(mockModule2), bytes4(0)) + ); + + governance.setState(GovernanceLib.ProtocolState.Paused); + vm.expectRevert(abi.encodeWithSelector(Errors.Governance__ProtocolPaused.selector)); + accessController.checkPermission(address(ipAccount), address(mockModule), address(mockModule2), bytes4(0)); + + governance.setState(GovernanceLib.ProtocolState.Unpaused); + assertFalse( + accessController.checkPermission(address(ipAccount), address(mockModule), address(mockModule2), bytes4(0)) + ); + } + + function test_Governance_revert_setStateWithNonAdmin() public { + vm.prank(address(0x777)); + vm.expectRevert(abi.encodeWithSelector(Errors.Governance__OnlyProtocolAdmin.selector)); + governance.setState(GovernanceLib.ProtocolState.Paused); + } + + function test_Governance_revert_setSameState() public { + vm.expectRevert(abi.encodeWithSelector(Errors.Governance__NewStateIsTheSameWithOldState.selector)); + governance.setState(GovernanceLib.ProtocolState.Unpaused); + } +} diff --git a/test/foundry/modules/ModuleBase.t.sol b/test/foundry/modules/ModuleBase.t.sol index e4e7b8470..81386b9bd 100644 --- a/test/foundry/modules/ModuleBase.t.sol +++ b/test/foundry/modules/ModuleBase.t.sol @@ -19,6 +19,7 @@ import { IPAccountImpl} from "contracts/IPAccountImpl.sol"; import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; import { IP } from "contracts/lib/IP.sol"; import { Errors } from "contracts/lib/Errors.sol"; +import { Governance } from "contracts/governance/Governance.sol"; /// @title Module Base Test Contract /// @notice Base contract for testing standard module functionality. @@ -42,12 +43,15 @@ abstract contract ModuleBaseTest is BaseTest { /// @notice The module SUT. IModule public baseModule; + Governance public governance; + /// @notice Initializes the base module for testing. function setUp() public virtual override(BaseTest) { BaseTest.setUp(); + governance = new Governance(address(this)); licenseRegistry = new LicenseRegistry(""); - accessController = new AccessController(); - moduleRegistry = new ModuleRegistry(); + accessController = new AccessController(address(governance)); + moduleRegistry = new ModuleRegistry(address(governance)); ipAccountRegistry = new IPAccountRegistry( address(new ERC6551Registry()), address(accessController), diff --git a/test/foundry/registries/metadata/IPAssetRenderer.t.sol b/test/foundry/registries/metadata/IPAssetRenderer.t.sol index b9214d0e5..a436a9830 100644 --- a/test/foundry/registries/metadata/IPAssetRenderer.t.sol +++ b/test/foundry/registries/metadata/IPAssetRenderer.t.sol @@ -27,6 +27,7 @@ import { ModuleBaseTest } from "test/foundry/modules/ModuleBase.t.sol"; import { IP } from "contracts/lib/IP.sol"; import { Errors } from "contracts/lib/Errors.sol"; import { IP_RESOLVER_MODULE_KEY, REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; +import { Governance } from "contracts/governance/Governance.sol"; /// @title IP Asset Renderer Test Contract /// @notice Tests IP asset rendering functionality. @@ -75,13 +76,16 @@ contract IPAssetRendererTest is BaseTest { /// @notice Mock IP identifier for resolver testing. address public ipId; + Governance public governance; + /// @notice Initializes the base token contract for testing. function setUp() public virtual override(BaseTest) { BaseTest.setUp(); + governance = new Governance(address(this)); // TODO: Create an IP record registry mock instead. licenseRegistry = new LicenseRegistry(""); - accessController = new AccessController(); - moduleRegistry = new ModuleRegistry(); + accessController = new AccessController(address(governance)); + moduleRegistry = new ModuleRegistry(address(governance)); MockERC721 erc721 = new MockERC721(); ipAccountRegistry = new IPAccountRegistry( address(new ERC6551Registry()),