Skip to content

Commit f0d0c44

Browse files
authored
Enable more flexible access controls for contract deployment (#219)
* Add support for RBAC contract deployment * Add custom error types * Improve comments * Fix inapplicable slither warning * Improve tests * Fix typo
1 parent 44835a6 commit f0d0c44

File tree

7 files changed

+619
-47
lines changed

7 files changed

+619
-47
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ coverage.json
55
typechain
66
typechain-types
77
node.json
8+
.idea/
89

910
# Hardhat files
1011
cache
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Copyright Immutable Pty Ltd 2018 - 2024
2+
// SPDX-License-Identifier: Apache 2.0
3+
pragma solidity 0.8.19;
4+
5+
import {IDeployer} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IDeployer.sol";
6+
import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
7+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
8+
import {AccessControlEnumerable} from "@openzeppelin/contracts/access/AccessControlEnumerable.sol";
9+
10+
contract AccessControlledDeployer is AccessControlEnumerable, Pausable {
11+
/// @notice Role identifier for those who can pause the deployer
12+
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER");
13+
14+
/// @notice Role identifier for those who can unpause the deployer
15+
bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER");
16+
17+
/// @notice Role identifier for those who can deploy contracts
18+
bytes32 public constant DEPLOYER_ROLE = keccak256("DEPLOYER");
19+
20+
/// @notice Emitted when the zero address is provided when it is not expected
21+
error ZeroAddress();
22+
23+
/// @notice Emitted when a provided list of deployer addresses is empty
24+
error EmptyDeployerList();
25+
26+
/// @notice Emitted if the caller, this contract, is not the owner of the targeted deployer
27+
error NotOwnerOfDeployer();
28+
29+
/**
30+
* @notice Construct a new RBACDeployer contract
31+
* @param admin The address to grant the DEFAULT_ADMIN_ROLE
32+
* @param pauser The address to grant the PAUSER_ROLE
33+
* @param unpauser The address to grant the UNPAUSER_ROLE
34+
*/
35+
constructor(address admin, address pauser, address unpauser) {
36+
if (admin == address(0) || pauser == address(0) || unpauser == address(0)) {
37+
revert ZeroAddress();
38+
}
39+
40+
_grantRole(DEFAULT_ADMIN_ROLE, admin);
41+
_grantRole(PAUSER_ROLE, pauser);
42+
_grantRole(UNPAUSER_ROLE, unpauser);
43+
}
44+
45+
/**
46+
* @notice Deploys a contract using a deployment method defined by `deployer`
47+
* @param deployer The create2 or create3 deployer contract that will deploy the contract
48+
* @param bytecode The bytecode of the contract to be deployed
49+
* @param salt A salt to influence the contract address
50+
* @dev Only address with DEPLOYER_ROLE can call this function
51+
* @dev This function requires that the current owner of `deployer` is this contract
52+
* @dev The function can only be called if the contract is not in a paused state
53+
* @dev The function emits `Deployed` event after the contract is deployed
54+
* @return The address of the deployed contract
55+
*/
56+
function deploy(
57+
IDeployer deployer,
58+
bytes memory bytecode,
59+
bytes32 salt
60+
) external payable whenNotPaused onlyRole(DEPLOYER_ROLE) returns (address) {
61+
if (address(deployer) == address(0)) {
62+
revert ZeroAddress();
63+
}
64+
return deployer.deploy{value: msg.value}(bytecode, salt);
65+
}
66+
67+
/**
68+
* @notice Deploys a contract using a deployment method defined by `deployer` and initializes it
69+
* @param deployer The create2 or create3 deployer contract that will deploy the contract
70+
* @param bytecode The bytecode of the contract to be deployed
71+
* @param salt A salt to influence the contract address
72+
* @param init Init data used to initialize the deployed contract
73+
* @dev Only address with DEPLOYER_ROLE can call this function
74+
* @dev This function requires that the current owner of `deployer` is this contract
75+
* @dev The function can only be called if the contract is not in a paused state
76+
* @dev The function emits `Deployed` event after the contract is deployed
77+
* @return The address of the deployed contract
78+
*/
79+
function deployAndInit(
80+
IDeployer deployer,
81+
bytes memory bytecode,
82+
bytes32 salt,
83+
bytes calldata init
84+
) external payable whenNotPaused onlyRole(DEPLOYER_ROLE) returns (address) {
85+
if (address(deployer) == address(0)) {
86+
revert ZeroAddress();
87+
}
88+
return deployer.deployAndInit{value: msg.value}(bytecode, salt, init);
89+
}
90+
91+
/**
92+
* @notice Grants a list of addresses the DEPLOYER_ROLE
93+
* @param deployers list of addresses to grant the DEPLOYER_ROLE
94+
* @dev Only address with DEFAULT_ADMIN_ROLE can call this function
95+
* @dev The function emits `RoleGranted` event for each address granted the DEPLOYER_ROLE.
96+
* This is not emitted if an address is already a deployer
97+
*/
98+
function grantDeployerRole(address[] memory deployers) public {
99+
if (deployers.length == 0) {
100+
revert EmptyDeployerList();
101+
}
102+
for (uint256 i = 0; i < deployers.length; i++) {
103+
if (deployers[i] == address(0)) {
104+
revert ZeroAddress();
105+
}
106+
grantRole(DEPLOYER_ROLE, deployers[i]);
107+
}
108+
}
109+
110+
/**
111+
* @notice Revokes the DEPLOYER_ROLE from a list of addresses
112+
* @param deployers list of addresses to revoke the DEPLOYER_ROLE from
113+
* @dev Only address with DEFAULT_ADMIN_ROLE can call this function
114+
* @dev The function emits `RoleRevoked` event for each address for which the DEPLOYER_ROLE was revoked
115+
* This is not emitted if an address was not a deployer
116+
*/
117+
function revokeDeployerRole(address[] memory deployers) public {
118+
if (deployers.length == 0) {
119+
revert EmptyDeployerList();
120+
}
121+
for (uint256 i = 0; i < deployers.length; i++) {
122+
if (deployers[i] == address(0)) {
123+
revert ZeroAddress();
124+
}
125+
revokeRole(DEPLOYER_ROLE, deployers[i]);
126+
}
127+
}
128+
129+
/**
130+
* @notice Transfers the ownership of `ownableDeployer` from this contract to `newOwner`
131+
* @param ownableDeployer The create2 or create3 ownable deployer contract to change the owner of
132+
* @param newOwner The new owner of the deployer contract
133+
* @dev Only address with DEFAULT_ADMIN_ROLE can call this function
134+
* @dev This function requires that the current owner of `ownableDeployer` is this contract
135+
* @dev The function emits `OwnershipTransferred` event if the ownership is successfully transferred
136+
*/
137+
function transferOwnershipOfDeployer(
138+
Ownable ownableDeployer,
139+
address newOwner
140+
) external onlyRole(DEFAULT_ADMIN_ROLE) {
141+
if (address(ownableDeployer) == address(0) || newOwner == address(0)) {
142+
revert ZeroAddress();
143+
}
144+
if (ownableDeployer.owner() != address(this)) {
145+
revert NotOwnerOfDeployer();
146+
}
147+
ownableDeployer.transferOwnership(newOwner);
148+
}
149+
150+
/**
151+
* @notice Pause the contract, preventing any new deployments
152+
* @dev Only PAUSER_ROLE can call this function
153+
*/
154+
function pause() external onlyRole(PAUSER_ROLE) {
155+
_pause();
156+
}
157+
158+
/**
159+
* @notice Unpause the contract if it was paused, re-enabling new deployments
160+
* @dev Only UNPAUSER_ROLE can call this function
161+
*/
162+
function unpause() external onlyRole(UNPAUSER_ROLE) {
163+
_unpause();
164+
}
165+
}

0 commit comments

Comments
 (0)