From f0d0c447261d036de753728da20efceac3347c82 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 15 May 2024 17:08:26 +1000 Subject: [PATCH] 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 --- .gitignore | 1 + .../deployer/AccessControlledDeployer.sol | 165 ++++++++ test/deployer/AccessControlledDeployer.t.sol | 382 ++++++++++++++++++ test/deployer/create2/Create2Utils.sol | 22 + .../create2/OwnableCreate2Deployer.t.sol | 60 ++- test/deployer/create3/Create3Utils.sol | 12 + .../create3/OwnableCreate3Deployer.t.sol | 24 +- 7 files changed, 619 insertions(+), 47 deletions(-) create mode 100644 contracts/deployer/AccessControlledDeployer.sol create mode 100644 test/deployer/AccessControlledDeployer.t.sol create mode 100644 test/deployer/create2/Create2Utils.sol create mode 100644 test/deployer/create3/Create3Utils.sol diff --git a/.gitignore b/.gitignore index 5d3779bc..9abc4d0b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ coverage.json typechain typechain-types node.json +.idea/ # Hardhat files cache diff --git a/contracts/deployer/AccessControlledDeployer.sol b/contracts/deployer/AccessControlledDeployer.sol new file mode 100644 index 00000000..9f220388 --- /dev/null +++ b/contracts/deployer/AccessControlledDeployer.sol @@ -0,0 +1,165 @@ +// Copyright Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {IDeployer} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IDeployer.sol"; +import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {AccessControlEnumerable} from "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; + +contract AccessControlledDeployer is AccessControlEnumerable, Pausable { + /// @notice Role identifier for those who can pause the deployer + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER"); + + /// @notice Role identifier for those who can unpause the deployer + bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER"); + + /// @notice Role identifier for those who can deploy contracts + bytes32 public constant DEPLOYER_ROLE = keccak256("DEPLOYER"); + + /// @notice Emitted when the zero address is provided when it is not expected + error ZeroAddress(); + + /// @notice Emitted when a provided list of deployer addresses is empty + error EmptyDeployerList(); + + /// @notice Emitted if the caller, this contract, is not the owner of the targeted deployer + error NotOwnerOfDeployer(); + + /** + * @notice Construct a new RBACDeployer contract + * @param admin The address to grant the DEFAULT_ADMIN_ROLE + * @param pauser The address to grant the PAUSER_ROLE + * @param unpauser The address to grant the UNPAUSER_ROLE + */ + constructor(address admin, address pauser, address unpauser) { + if (admin == address(0) || pauser == address(0) || unpauser == address(0)) { + revert ZeroAddress(); + } + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(PAUSER_ROLE, pauser); + _grantRole(UNPAUSER_ROLE, unpauser); + } + + /** + * @notice Deploys a contract using a deployment method defined by `deployer` + * @param deployer The create2 or create3 deployer contract that will deploy the contract + * @param bytecode The bytecode of the contract to be deployed + * @param salt A salt to influence the contract address + * @dev Only address with DEPLOYER_ROLE can call this function + * @dev This function requires that the current owner of `deployer` is this contract + * @dev The function can only be called if the contract is not in a paused state + * @dev The function emits `Deployed` event after the contract is deployed + * @return The address of the deployed contract + */ + function deploy( + IDeployer deployer, + bytes memory bytecode, + bytes32 salt + ) external payable whenNotPaused onlyRole(DEPLOYER_ROLE) returns (address) { + if (address(deployer) == address(0)) { + revert ZeroAddress(); + } + return deployer.deploy{value: msg.value}(bytecode, salt); + } + + /** + * @notice Deploys a contract using a deployment method defined by `deployer` and initializes it + * @param deployer The create2 or create3 deployer contract that will deploy the contract + * @param bytecode The bytecode of the contract to be deployed + * @param salt A salt to influence the contract address + * @param init Init data used to initialize the deployed contract + * @dev Only address with DEPLOYER_ROLE can call this function + * @dev This function requires that the current owner of `deployer` is this contract + * @dev The function can only be called if the contract is not in a paused state + * @dev The function emits `Deployed` event after the contract is deployed + * @return The address of the deployed contract + */ + function deployAndInit( + IDeployer deployer, + bytes memory bytecode, + bytes32 salt, + bytes calldata init + ) external payable whenNotPaused onlyRole(DEPLOYER_ROLE) returns (address) { + if (address(deployer) == address(0)) { + revert ZeroAddress(); + } + return deployer.deployAndInit{value: msg.value}(bytecode, salt, init); + } + + /** + * @notice Grants a list of addresses the DEPLOYER_ROLE + * @param deployers list of addresses to grant the DEPLOYER_ROLE + * @dev Only address with DEFAULT_ADMIN_ROLE can call this function + * @dev The function emits `RoleGranted` event for each address granted the DEPLOYER_ROLE. + * This is not emitted if an address is already a deployer + */ + function grantDeployerRole(address[] memory deployers) public { + if (deployers.length == 0) { + revert EmptyDeployerList(); + } + for (uint256 i = 0; i < deployers.length; i++) { + if (deployers[i] == address(0)) { + revert ZeroAddress(); + } + grantRole(DEPLOYER_ROLE, deployers[i]); + } + } + + /** + * @notice Revokes the DEPLOYER_ROLE from a list of addresses + * @param deployers list of addresses to revoke the DEPLOYER_ROLE from + * @dev Only address with DEFAULT_ADMIN_ROLE can call this function + * @dev The function emits `RoleRevoked` event for each address for which the DEPLOYER_ROLE was revoked + * This is not emitted if an address was not a deployer + */ + function revokeDeployerRole(address[] memory deployers) public { + if (deployers.length == 0) { + revert EmptyDeployerList(); + } + for (uint256 i = 0; i < deployers.length; i++) { + if (deployers[i] == address(0)) { + revert ZeroAddress(); + } + revokeRole(DEPLOYER_ROLE, deployers[i]); + } + } + + /** + * @notice Transfers the ownership of `ownableDeployer` from this contract to `newOwner` + * @param ownableDeployer The create2 or create3 ownable deployer contract to change the owner of + * @param newOwner The new owner of the deployer contract + * @dev Only address with DEFAULT_ADMIN_ROLE can call this function + * @dev This function requires that the current owner of `ownableDeployer` is this contract + * @dev The function emits `OwnershipTransferred` event if the ownership is successfully transferred + */ + function transferOwnershipOfDeployer( + Ownable ownableDeployer, + address newOwner + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (address(ownableDeployer) == address(0) || newOwner == address(0)) { + revert ZeroAddress(); + } + if (ownableDeployer.owner() != address(this)) { + revert NotOwnerOfDeployer(); + } + ownableDeployer.transferOwnership(newOwner); + } + + /** + * @notice Pause the contract, preventing any new deployments + * @dev Only PAUSER_ROLE can call this function + */ + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); + } + + /** + * @notice Unpause the contract if it was paused, re-enabling new deployments + * @dev Only UNPAUSER_ROLE can call this function + */ + function unpause() external onlyRole(UNPAUSER_ROLE) { + _unpause(); + } +} diff --git a/test/deployer/AccessControlledDeployer.t.sol b/test/deployer/AccessControlledDeployer.t.sol new file mode 100644 index 00000000..9f811718 --- /dev/null +++ b/test/deployer/AccessControlledDeployer.t.sol @@ -0,0 +1,382 @@ +// Copyright Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IDeployer} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IDeployer.sol"; +import {ERC20MintableBurnable} from + "@axelar-network/axelar-gmp-sdk-solidity/contracts/test/token/ERC20MintableBurnable.sol"; +import {ERC20MintableBurnableInit} from + "@axelar-network/axelar-gmp-sdk-solidity/contracts/test/token/ERC20MintableBurnableInit.sol"; + +import {OwnableCreate2Deployer} from "../../contracts/deployer/create2/OwnableCreate2Deployer.sol"; +import {AccessControlledDeployer} from "../../contracts/deployer/AccessControlledDeployer.sol"; +import {OwnableCreate3Deployer} from "../../contracts/deployer/create3/OwnableCreate3Deployer.sol"; + +import {Create2Utils} from "./create2/Create2Utils.sol"; +import {Create3Utils} from "./create3/Create3Utils.sol"; + +contract AccessControlledDeployerTest is Test, Create2Utils, Create3Utils { + address private admin = makeAddr("admin"); + address private pauser = makeAddr("pauser"); + address private unpauser = makeAddr("unpauser"); + address[] private authDeployers; + AccessControlledDeployer private rbacDeployer; + + event Deployed(address indexed deployedAddress, address indexed sender, bytes32 indexed salt, bytes32 bytecodeHash); + + error ZeroAddress(); + error EmptyDeployerList(); + error NotOwnerOfDeployer(); + + function setUp() public { + rbacDeployer = new AccessControlledDeployer(admin, pauser, unpauser); + + authDeployers.push(makeAddr("deployer1")); + vm.prank(admin); + rbacDeployer.grantDeployerRole(authDeployers); + } + + /** + * Constructor + */ + function test_Constructor_RevertIf_AdminIsZeroAddress() public { + vm.expectRevert(ZeroAddress.selector); + new AccessControlledDeployer(address(0), pauser, unpauser); + } + + function test_Constructor_RevertIf_PauserIsZeroAddress() public { + vm.expectRevert(ZeroAddress.selector); + new AccessControlledDeployer(admin, address(0), unpauser); + } + + function test_Constructor_RevertIf_UnpauserIsZeroAddress() public { + vm.expectRevert(ZeroAddress.selector); + new AccessControlledDeployer(admin, pauser, address(0)); + } + + function test_Constructor_AssignsRoles() public { + assertTrue(rbacDeployer.hasRole(rbacDeployer.DEFAULT_ADMIN_ROLE(), admin)); + assertTrue(rbacDeployer.hasRole(rbacDeployer.PAUSER_ROLE(), pauser)); + assertTrue(rbacDeployer.hasRole(rbacDeployer.UNPAUSER_ROLE(), unpauser)); + } + + /** + * Admin role management + */ + function test_AdminCanAssignRoles() public { + address newPauser = makeAddr("newPauser"); + address newUnpauser = makeAddr("newUnpauser"); + + assertFalse(rbacDeployer.hasRole(rbacDeployer.PAUSER_ROLE(), newPauser)); + assertFalse(rbacDeployer.hasRole(rbacDeployer.UNPAUSER_ROLE(), newUnpauser)); + + vm.startPrank(admin); + rbacDeployer.grantRole(rbacDeployer.PAUSER_ROLE(), newPauser); + rbacDeployer.grantRole(rbacDeployer.UNPAUSER_ROLE(), newUnpauser); + + assertTrue(rbacDeployer.hasRole(rbacDeployer.PAUSER_ROLE(), newPauser)); + assertTrue(rbacDeployer.hasRole(rbacDeployer.UNPAUSER_ROLE(), newUnpauser)); + } + + function test_AdminCanRevokeRoles() public { + vm.startPrank(admin); + assertTrue(rbacDeployer.hasRole(rbacDeployer.PAUSER_ROLE(), pauser)); + assertTrue(rbacDeployer.hasRole(rbacDeployer.UNPAUSER_ROLE(), unpauser)); + + rbacDeployer.revokeRole(rbacDeployer.PAUSER_ROLE(), pauser); + rbacDeployer.revokeRole(rbacDeployer.UNPAUSER_ROLE(), unpauser); + + assertFalse(rbacDeployer.hasRole(rbacDeployer.PAUSER_ROLE(), pauser)); + assertFalse(rbacDeployer.hasRole(rbacDeployer.UNPAUSER_ROLE(), unpauser)); + } + + function test_RevertIf_TransferDeployerOwnership_ByNonAdmin() public { + OwnableCreate2Deployer create2Deployer = new OwnableCreate2Deployer(address(rbacDeployer)); + vm.expectRevert(); + rbacDeployer.transferOwnershipOfDeployer(create2Deployer, makeAddr("newOwner2")); + } + + function test_RevertIf_TransferDeployerOwnership_WithZeroOwnerAddress() public { + OwnableCreate2Deployer create2Deployer = new OwnableCreate2Deployer(address(rbacDeployer)); + vm.startPrank(admin); + vm.expectRevert(ZeroAddress.selector); + rbacDeployer.transferOwnershipOfDeployer(create2Deployer, address(0)); + } + + function test_RevertIf_TransferDeployerOwnership_WhenNotCurrentOwner() public { + OwnableCreate2Deployer create2Deployer = new OwnableCreate2Deployer(makeAddr("currentOwner")); + vm.startPrank(admin); + vm.expectRevert(NotOwnerOfDeployer.selector); + rbacDeployer.transferOwnershipOfDeployer(create2Deployer, makeAddr("newOwner2")); + } + + function test_RevertIf_TransferDeployerOwnership_WithZeroDeployerAddress() public { + vm.startPrank(admin); + vm.expectRevert(ZeroAddress.selector); + rbacDeployer.transferOwnershipOfDeployer(Ownable(address(0)), makeAddr("newOwner2")); + } + + function test_TransferDeployerOwnership_ForOwnableCreate2Deployer() public { + OwnableCreate2Deployer create2Deployer = new OwnableCreate2Deployer(address(rbacDeployer)); + assertTrue(create2Deployer.owner() == address(rbacDeployer)); + + address newOwner = makeAddr("newOwner"); + vm.startPrank(admin); + rbacDeployer.transferOwnershipOfDeployer(create2Deployer, newOwner); + assertTrue(create2Deployer.owner() == newOwner); + } + + function test_TransferDeployerOwnership_ForOwnableCreate3Deployer() public { + OwnableCreate3Deployer create3Deployer = new OwnableCreate3Deployer(address(rbacDeployer)); + assertTrue(create3Deployer.owner() == address(rbacDeployer)); + + address newOwner = makeAddr("newOwner"); + vm.startPrank(admin); + rbacDeployer.transferOwnershipOfDeployer(create3Deployer, newOwner); + assertTrue(create3Deployer.owner() == newOwner); + } + + /** + * Pauser and Unpauser role management + */ + function test_OnlyPauserRoleCanPause() public { + // check random user can't pause + vm.expectRevert(); + rbacDeployer.pause(); + + // check admin can't pause + vm.prank(admin); + vm.expectRevert(); + rbacDeployer.pause(); + + // check address with pauser role can pause + vm.startPrank(pauser); + rbacDeployer.pause(); + assertTrue(rbacDeployer.paused()); + } + + function test_OnlyUnpauserRoleCanUnpause() public { + // pause first + vm.prank(pauser); + rbacDeployer.pause(); + assertTrue(rbacDeployer.paused()); + + // check random address can't unpause + vm.expectRevert(); + rbacDeployer.pause(); + + // check admin can't unpause + vm.prank(admin); + vm.expectRevert(); + rbacDeployer.pause(); + + // check unpauser role can unpause + vm.startPrank(unpauser); + rbacDeployer.unpause(); + assertFalse(rbacDeployer.paused()); + } + + /** + * Deployer role management + */ + function test_RevertIf_GrantDeployerRole_WithEmptyArray() public { + address[] memory emptyDeployers = new address[](0); + vm.expectRevert(EmptyDeployerList.selector); + vm.prank(admin); + rbacDeployer.grantDeployerRole(emptyDeployers); + } + + function test_RevertIf_GrantDeployerRole_ContainsZeroAddress() public { + address[] memory newDeployers = new address[](2); + newDeployers[0] = makeAddr("deployer2"); + // note that second deployer in the array is the zero address + + vm.prank(admin); + vm.expectRevert(ZeroAddress.selector); + rbacDeployer.grantDeployerRole(newDeployers); + } + + function test_GrantDeployerRole_WithOneDeployer() public { + address[] memory newDeployers = new address[](1); + newDeployers[0] = makeAddr("deployer2"); + + assertFalse(rbacDeployer.hasRole(rbacDeployer.DEPLOYER_ROLE(), newDeployers[0])); + + vm.prank(admin); + rbacDeployer.grantDeployerRole(newDeployers); + + assertTrue(rbacDeployer.hasRole(rbacDeployer.DEPLOYER_ROLE(), newDeployers[0])); + } + + function test_GrantDeployerRole_WithMultipleDeployers() public { + address[] memory newDeployers = new address[](2); + newDeployers[0] = makeAddr("deployer2"); + newDeployers[1] = makeAddr("deployer3"); + + assertFalse(rbacDeployer.hasRole(rbacDeployer.DEPLOYER_ROLE(), newDeployers[0])); + assertFalse(rbacDeployer.hasRole(rbacDeployer.DEPLOYER_ROLE(), newDeployers[1])); + + vm.prank(admin); + rbacDeployer.grantDeployerRole(newDeployers); + + assertTrue(rbacDeployer.hasRole(rbacDeployer.DEPLOYER_ROLE(), newDeployers[0])); + assertTrue(rbacDeployer.hasRole(rbacDeployer.DEPLOYER_ROLE(), newDeployers[1])); + } + + function test_RevertIf_RevokeDeployerRole_WithEmptyArray() public { + address[] memory emptyDeployers = new address[](0); + vm.expectRevert(EmptyDeployerList.selector); + vm.prank(admin); + rbacDeployer.revokeDeployerRole(emptyDeployers); + } + + function test_RevertIf_RevokeDeployerRole_ContainsZeroAddress() public { + address[] memory existingDeployers = new address[](2); + existingDeployers[0] = makeAddr("deployer1"); + // note that second deployer in the array is the zero address + + vm.prank(admin); + vm.expectRevert(ZeroAddress.selector); + rbacDeployer.grantDeployerRole(existingDeployers); + } + + function test_RevokeDeployerRole_GivenOneDeployer() public { + assertTrue(rbacDeployer.hasRole(rbacDeployer.DEPLOYER_ROLE(), authDeployers[0])); + + vm.prank(admin); + rbacDeployer.revokeDeployerRole(authDeployers); + + assertFalse(rbacDeployer.hasRole(rbacDeployer.DEPLOYER_ROLE(), authDeployers[0])); + } + + function test_RevokeDeployerRole_GivenMultipleDeployers() public { + address[] memory newDeployers = new address[](2); + newDeployers[0] = makeAddr("deployer2"); + newDeployers[1] = makeAddr("deployer3"); + + vm.prank(admin); + rbacDeployer.grantDeployerRole(newDeployers); + + assertTrue(rbacDeployer.hasRole(rbacDeployer.DEPLOYER_ROLE(), newDeployers[0])); + assertTrue(rbacDeployer.hasRole(rbacDeployer.DEPLOYER_ROLE(), newDeployers[1])); + + vm.prank(admin); + rbacDeployer.revokeDeployerRole(newDeployers); + + assertFalse(rbacDeployer.hasRole(rbacDeployer.DEPLOYER_ROLE(), newDeployers[0])); + assertFalse(rbacDeployer.hasRole(rbacDeployer.DEPLOYER_ROLE(), newDeployers[1])); + } + + /** + * Contract Deployment + */ + function test_RevertIf_Deploy_WithUnauthorizedAddress() public { + vm.expectRevert(); + rbacDeployer.deploy(IDeployer(address(0)), new bytes(0), bytes32(0)); + } + + function test_Deploy_UsingCreate2() public { + OwnableCreate2Deployer create2Deployer = new OwnableCreate2Deployer(address(rbacDeployer)); + bytes memory erc20MintableBytecode = + abi.encodePacked(type(ERC20MintableBurnable).creationCode, abi.encode("Test Token", "TEST", 10)); + bytes32 erc20MintableSalt = createSaltFromKey("erc20-mintable-burnable-v1", address(rbacDeployer)); + + address expectedAddress = predictCreate2Address( + erc20MintableBytecode, address(create2Deployer), address(rbacDeployer), erc20MintableSalt + ); + + vm.startPrank(authDeployers[0]); + vm.expectEmit(); + emit Deployed(expectedAddress, address(rbacDeployer), erc20MintableSalt, keccak256(erc20MintableBytecode)); + address deployedAddress = rbacDeployer.deploy(create2Deployer, erc20MintableBytecode, erc20MintableSalt); + ERC20MintableBurnable deployed = ERC20MintableBurnable(deployedAddress); + + assertEq(deployedAddress, expectedAddress, "deployed address does not match expected"); + assertEq(deployed.name(), "Test Token", "deployed contract does not match expected"); + assertEq(deployed.symbol(), "TEST", "deployed contract does not match expected"); + assertEq(deployed.decimals(), 10, "deployed contract does not match expected"); + } + + function test_DeployAndInit_UsingCreate2() public { + OwnableCreate2Deployer create2Deployer = new OwnableCreate2Deployer(address(rbacDeployer)); + bytes memory mintableInitBytecode = + abi.encodePacked(type(ERC20MintableBurnableInit).creationCode, abi.encode(10)); + + bytes32 mintableInitSalt = createSaltFromKey("erc20-mintable-burnable-init-v1", address(rbacDeployer)); + + address expectedAddress = predictCreate2Address( + mintableInitBytecode, address(create2Deployer), address(rbacDeployer), mintableInitSalt + ); + + bytes memory initPayload = abi.encodeWithSelector(ERC20MintableBurnableInit.init.selector, "Test Token", "TEST"); + vm.startPrank(authDeployers[0]); + vm.expectEmit(); + emit Deployed(expectedAddress, address(rbacDeployer), mintableInitSalt, keccak256(mintableInitBytecode)); + address deployedAddress = + rbacDeployer.deployAndInit(create2Deployer, mintableInitBytecode, mintableInitSalt, initPayload); + ERC20MintableBurnableInit deployed = ERC20MintableBurnableInit(deployedAddress); + + assertEq(deployedAddress, expectedAddress, "deployed address does not match expected"); + assertEq(deployed.name(), "Test Token", "deployed contract does not match expected"); + assertEq(deployed.symbol(), "TEST", "deployed contract does not match expected"); + assertEq(deployed.decimals(), 10, "deployed contract does not match expected"); + } + + function test_Deploy_UsingCreate3() public { + OwnableCreate3Deployer create3Deployer = new OwnableCreate3Deployer(address(rbacDeployer)); + bytes memory erc20MintableBytecode = + abi.encodePacked(type(ERC20MintableBurnable).creationCode, abi.encode("Test Token", "TEST", 10)); + bytes32 erc20MintableSalt = createSaltFromKey("erc20-mintable-burnable-v1", address(rbacDeployer)); + + address expectedAddress = predictCreate3Address(create3Deployer, address(rbacDeployer), erc20MintableSalt); + + vm.startPrank(authDeployers[0]); + vm.expectEmit(); + emit Deployed(expectedAddress, address(rbacDeployer), erc20MintableSalt, keccak256(erc20MintableBytecode)); + address deployedAddress = rbacDeployer.deploy(create3Deployer, erc20MintableBytecode, erc20MintableSalt); + ERC20MintableBurnable deployed = ERC20MintableBurnable(deployedAddress); + + assertEq(deployedAddress, expectedAddress, "deployed address does not match expected"); + assertEq(deployed.name(), "Test Token", "deployed contract does not match expected"); + assertEq(deployed.symbol(), "TEST", "deployed contract does not match expected"); + assertEq(deployed.decimals(), 10, "deployed contract does not match expected"); + } + + function test_DeployAndInit_UsingCreate3() public { + OwnableCreate3Deployer create3Deployer = new OwnableCreate3Deployer(address(rbacDeployer)); + bytes memory erc20MintableInitBytcode = + abi.encodePacked(type(ERC20MintableBurnableInit).creationCode, abi.encode(10)); + + bytes32 erc20MintableSalt = createSaltFromKey("erc20-mintable-burnable-init-v1", address(rbacDeployer)); + + address expectedAddress = predictCreate3Address(create3Deployer, address(rbacDeployer), erc20MintableSalt); + + vm.startPrank(authDeployers[0]); + bytes memory initPayload = abi.encodeWithSelector(ERC20MintableBurnableInit.init.selector, "Test Token", "TEST"); + vm.expectEmit(); + emit Deployed(expectedAddress, address(rbacDeployer), erc20MintableSalt, keccak256(erc20MintableInitBytcode)); + address deployedAddress = + rbacDeployer.deployAndInit(create3Deployer, erc20MintableInitBytcode, erc20MintableSalt, initPayload); + ERC20MintableBurnableInit deployed = ERC20MintableBurnableInit(deployedAddress); + + assertEq(deployedAddress, expectedAddress, "deployed address does not match expected"); + assertEq(deployed.name(), "Test Token", "deployed contract does not match expected"); + assertEq(deployed.symbol(), "TEST", "deployed contract does not match expected"); + assertEq(deployed.decimals(), 10, "deployed contract does not match expected"); + } + + function test_DeployFails_WhenPaused() public { + vm.startPrank(pauser); + + rbacDeployer.pause(); + assertTrue(rbacDeployer.paused()); + + vm.expectRevert("Pausable: paused"); + rbacDeployer.deploy(IDeployer(address(0)), new bytes(0), bytes32(0)); + + vm.expectRevert("Pausable: paused"); + rbacDeployer.deployAndInit(IDeployer(address(0)), new bytes(0), bytes32(0), new bytes(0)); + } +} diff --git a/test/deployer/create2/Create2Utils.sol b/test/deployer/create2/Create2Utils.sol new file mode 100644 index 00000000..d3f335ef --- /dev/null +++ b/test/deployer/create2/Create2Utils.sol @@ -0,0 +1,22 @@ +// Copyright Immutable Pty Ltd 2018 - 2023 +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; + +contract Create2Utils is Test { + function predictCreate2Address(bytes memory _bytecode, address _deployer, address _sender, bytes32 _salt) + public + pure + returns (address) + { + bytes32 deploySalt = keccak256(abi.encode(_sender, _salt)); + return address( + uint160(uint256(keccak256(abi.encodePacked(hex"ff", address(_deployer), deploySalt, keccak256(_bytecode))))) + ); + } + + function createSaltFromKey(string memory key, address owner) public pure returns (bytes32) { + return keccak256(abi.encode(address(owner), key)); + } +} diff --git a/test/deployer/create2/OwnableCreate2Deployer.t.sol b/test/deployer/create2/OwnableCreate2Deployer.t.sol index 3b1047a7..ceb3f249 100644 --- a/test/deployer/create2/OwnableCreate2Deployer.t.sol +++ b/test/deployer/create2/OwnableCreate2Deployer.t.sol @@ -3,15 +3,19 @@ pragma solidity 0.8.19; import "forge-std/Test.sol"; -import {ERC20Mock} from "@openzeppelin/contracts/mocks/ERC20Mock.sol"; import {IDeploy} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IDeploy.sol"; -import {ERC20MintableBurnable} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/test/token/ERC20MintableBurnable.sol"; -import {ERC20MintableBurnableInit} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/test/token/ERC20MintableBurnableInit.sol"; + +import {ERC20Mock} from "@openzeppelin/contracts/mocks/ERC20Mock.sol"; +import {ERC20MintableBurnable} from + "@axelar-network/axelar-gmp-sdk-solidity/contracts/test/token/ERC20MintableBurnable.sol"; +import {ERC20MintableBurnableInit} from + "@axelar-network/axelar-gmp-sdk-solidity/contracts/test/token/ERC20MintableBurnableInit.sol"; import {ContractAddress} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/libs/ContractAddress.sol"; import {OwnableCreate2Deployer} from "../../../contracts/deployer/create2/OwnableCreate2Deployer.sol"; +import {Create2Utils} from "./Create2Utils.sol"; -contract OwnableCreate2DeployerTest is Test { +contract OwnableCreate2DeployerTest is Test, Create2Utils { OwnableCreate2Deployer private factory; bytes private erc20MockBytecode; bytes32 private erc20MockSalt; @@ -26,7 +30,7 @@ contract OwnableCreate2DeployerTest is Test { factory = new OwnableCreate2Deployer(factoryOwner); erc20MockBytecode = type(ERC20Mock).creationCode; - erc20MockSalt = createSaltFromKey("erc20-mock-v1"); + erc20MockSalt = Create2Utils.createSaltFromKey("erc20-mock-v1", factoryOwner); vm.startPrank(factoryOwner); } @@ -57,8 +61,9 @@ contract OwnableCreate2DeployerTest is Test { /// @dev ensure contracts are deployed at the expected address function test_deploy_DeploysContractAtExpectedAddress() public { - address expectedAddress = - predictCreate2Address(erc20MockBytecode, address(factory), address(factoryOwner), erc20MockSalt); + address expectedAddress = Create2Utils.predictCreate2Address( + erc20MockBytecode, address(factory), address(factoryOwner), erc20MockSalt + ); /// forward the nonce of the owner and the factory, to confirm they doesn't influence address vm.setNonce(factoryOwner, vm.getNonce(factoryOwner) + 10); @@ -75,10 +80,11 @@ contract OwnableCreate2DeployerTest is Test { function test_deploy_DeploysContractWithConstructor() public { bytes memory erc20MintableBytecode = abi.encodePacked(type(ERC20MintableBurnable).creationCode, abi.encode("Test Token", "TEST", 18)); - bytes32 erc20MintableSalt = createSaltFromKey("erc20-mintable-burnable-v1"); + bytes32 erc20MintableSalt = Create2Utils.createSaltFromKey("erc20-mintable-burnable-v1", factoryOwner); - address expectedAddress = - predictCreate2Address(erc20MintableBytecode, address(factory), address(factoryOwner), erc20MintableSalt); + address expectedAddress = Create2Utils.predictCreate2Address( + erc20MintableBytecode, address(factory), address(factoryOwner), erc20MintableSalt + ); vm.expectEmit(); emit Deployed(expectedAddress, address(factoryOwner), erc20MintableSalt, keccak256(erc20MintableBytecode)); @@ -94,7 +100,7 @@ contract OwnableCreate2DeployerTest is Test { function test_deploy_DeploysSameContractToDifferentAddresses_GivenDifferentSalts() public { address deployed1 = factory.deploy(erc20MockBytecode, erc20MockSalt); - bytes32 newSalt = createSaltFromKey("create2-deployer-test-v2"); + bytes32 newSalt = Create2Utils.createSaltFromKey("create2-deployer-test-v2", factoryOwner); address deployed2 = factory.deploy(erc20MockBytecode, newSalt); assertEq(deployed1.code, deployed2.code, "bytecodes of deployed contracts do not match"); @@ -114,7 +120,7 @@ contract OwnableCreate2DeployerTest is Test { // test that the new owner can deploy vm.startPrank(newOwner); address expectedAddress = - predictCreate2Address(erc20MockBytecode, address(factory), address(newOwner), erc20MockSalt); + Create2Utils.predictCreate2Address(erc20MockBytecode, address(factory), address(newOwner), erc20MockSalt); vm.expectEmit(); emit Deployed(expectedAddress, address(newOwner), erc20MockSalt, keccak256(erc20MockBytecode)); @@ -140,10 +146,11 @@ contract OwnableCreate2DeployerTest is Test { bytes memory mintableInitBytecode = abi.encodePacked(type(ERC20MintableBurnableInit).creationCode, abi.encode(18)); - bytes32 mintableInitSalt = createSaltFromKey("erc20-mintable-burnable-init-v1"); + bytes32 mintableInitSalt = Create2Utils.createSaltFromKey("erc20-mintable-burnable-init-v1", factoryOwner); - address expectedAddress = - predictCreate2Address(mintableInitBytecode, address(factory), address(factoryOwner), mintableInitSalt); + address expectedAddress = Create2Utils.predictCreate2Address( + mintableInitBytecode, address(factory), address(factoryOwner), mintableInitSalt + ); bytes memory initPayload = abi.encodeWithSelector(ERC20MintableBurnableInit.init.selector, "Test Token", "TEST"); vm.expectEmit(); @@ -162,29 +169,12 @@ contract OwnableCreate2DeployerTest is Test { function test_deployedAddress_ReturnsPredictedAddress() public { address deployAddress = factory.deployedAddress(erc20MockBytecode, address(factoryOwner), erc20MockSalt); - address predictedAddress = - predictCreate2Address(erc20MockBytecode, address(factory), address(factoryOwner), erc20MockSalt); + address predictedAddress = Create2Utils.predictCreate2Address( + erc20MockBytecode, address(factory), address(factoryOwner), erc20MockSalt + ); address deployedAddress = factory.deploy(erc20MockBytecode, erc20MockSalt); assertEq(deployAddress, predictedAddress, "deployment address did not match predicted address"); assertEq(deployAddress, deployedAddress, "deployment address did not match deployed address"); } - - /** - * private helper functions - */ - function predictCreate2Address(bytes memory _bytecode, address _deployer, address _sender, bytes32 _salt) - private - pure - returns (address) - { - bytes32 deploySalt = keccak256(abi.encode(_sender, _salt)); - return address( - uint160(uint256(keccak256(abi.encodePacked(hex"ff", address(_deployer), deploySalt, keccak256(_bytecode))))) - ); - } - - function createSaltFromKey(string memory key) private view returns (bytes32) { - return keccak256(abi.encode(address(factoryOwner), key)); - } } diff --git a/test/deployer/create3/Create3Utils.sol b/test/deployer/create3/Create3Utils.sol new file mode 100644 index 00000000..cd27a524 --- /dev/null +++ b/test/deployer/create3/Create3Utils.sol @@ -0,0 +1,12 @@ +// Copyright Immutable Pty Ltd 2018 - 2023 +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; +import {IDeployer} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IDeployer.sol"; + +contract Create3Utils is Test { + function predictCreate3Address(IDeployer _deployer, address _sender, bytes32 _salt) public view returns (address) { + return _deployer.deployedAddress("", _sender, _salt); + } +} diff --git a/test/deployer/create3/OwnableCreate3Deployer.t.sol b/test/deployer/create3/OwnableCreate3Deployer.t.sol index bc6816ce..5732fb04 100644 --- a/test/deployer/create3/OwnableCreate3Deployer.t.sol +++ b/test/deployer/create3/OwnableCreate3Deployer.t.sol @@ -3,16 +3,20 @@ pragma solidity 0.8.19; import "forge-std/Test.sol"; -import {ERC20Mock} from "@openzeppelin/contracts/mocks/ERC20Mock.sol"; import {IDeploy} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IDeploy.sol"; -import {ERC20MintableBurnable} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/test/token/ERC20MintableBurnable.sol"; -import {ERC20MintableBurnableInit} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/test/token/ERC20MintableBurnableInit.sol"; + +import {ERC20Mock} from "@openzeppelin/contracts/mocks/ERC20Mock.sol"; +import {ERC20MintableBurnable} from + "@axelar-network/axelar-gmp-sdk-solidity/contracts/test/token/ERC20MintableBurnable.sol"; +import {ERC20MintableBurnableInit} from + "@axelar-network/axelar-gmp-sdk-solidity/contracts/test/token/ERC20MintableBurnableInit.sol"; import {ContractAddress} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/libs/ContractAddress.sol"; import {OwnableCreate3Deployer} from "../../../contracts/deployer/create3/OwnableCreate3Deployer.sol"; import {OwnableCreateDeploy} from "../../../contracts/deployer/create/OwnableCreateDeploy.sol"; +import {Create3Utils} from "./Create3Utils.sol"; -contract OwnableCreate3DeployerTest is Test { +contract OwnableCreate3DeployerTest is Test, Create3Utils { OwnableCreate3Deployer private factory; bytes private erc20MockBytecode; bytes32 private erc20MockSalt; @@ -58,7 +62,7 @@ contract OwnableCreate3DeployerTest is Test { /// @dev ensure contracts are deployed at the expected address function test_deploy_DeploysContractAtExpectedAddress() public { - address expectedAddress = _predictCreate3Address(address(factoryOwner), erc20MockSalt); + address expectedAddress = predictCreate3Address(factory, address(factoryOwner), erc20MockSalt); /// forward the nonce of the owner and the factory, to confirm they doesn't influence address vm.setNonce(factoryOwner, vm.getNonce(factoryOwner) + 10); @@ -77,7 +81,7 @@ contract OwnableCreate3DeployerTest is Test { abi.encodePacked(type(ERC20MintableBurnable).creationCode, abi.encode("Test Token", "TEST", 18)); bytes32 erc20MintableSalt = _createSaltFromKey("erc20-mintable-burnable-v1"); - address expectedAddress = _predictCreate3Address(address(factoryOwner), erc20MintableSalt); + address expectedAddress = predictCreate3Address(factory, address(factoryOwner), erc20MintableSalt); vm.expectEmit(); emit Deployed(expectedAddress, address(factoryOwner), erc20MintableSalt, keccak256(erc20MintableBytecode)); @@ -112,7 +116,7 @@ contract OwnableCreate3DeployerTest is Test { // test that the new owner can deploy vm.startPrank(newOwner); - address expectedAddress = _predictCreate3Address(address(newOwner), erc20MockSalt); + address expectedAddress = predictCreate3Address(factory, address(newOwner), erc20MockSalt); vm.expectEmit(); emit Deployed(expectedAddress, address(newOwner), erc20MockSalt, keccak256(erc20MockBytecode)); @@ -161,7 +165,7 @@ contract OwnableCreate3DeployerTest is Test { bytes32 mintableInitSalt = _createSaltFromKey("erc20-mintable-burnable-init-v1"); - address expectedAddress = _predictCreate3Address(address(factoryOwner), mintableInitSalt); + address expectedAddress = predictCreate3Address(factory, address(factoryOwner), mintableInitSalt); bytes memory initPayload = abi.encodeWithSelector(ERC20MintableBurnableInit.init.selector, "Test Token", "TEST"); vm.expectEmit(); @@ -234,10 +238,6 @@ contract OwnableCreate3DeployerTest is Test { /** * private helper functions */ - function _predictCreate3Address(address _sender, bytes32 _salt) private view returns (address) { - return factory.deployedAddress("", _sender, _salt); - } - function _createSaltFromKey(string memory key) private view returns (bytes32) { return keccak256(abi.encode(address(factoryOwner), key)); }