Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ERC 20 contracts V2 #222

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions contracts/token/common/HubOwner.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) Immutable Pty Ltd 2018 - 2024
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {AccessControlEnumerable, AccessControl, IAccessControl} from "@openzeppelin/contracts/access/AccessControlEnumerable.sol";

/**
* @notice Adds the concept of a hub owner.
* @dev This contract has the concept of a hubOwner, called _hubOwner in the constructor.
* This account has no rights to execute any administrative actions within the contract,
* with the exception of renouncing their ownership.
* The Immutable Hub uses this function to help associate the ERC 20 contract
* with a specific Immutable Hub account.
*/
abstract contract HubOwner is AccessControlEnumerable {
// Report an error if renounceRole is called for the last DEFAULT_ADMIN_ROLE or
// OWNER_ROLE.
error RenounceLastNotAllowed();

/// @notice Role to indicate owner for Immutable Hub and other applications.
bytes32 public constant HUB_OWNER_ROLE = bytes32("HUB_OWNER_ROLE");

/**
* @param _roleAdmin The account that administers other roles and other
* accounts with DEFAULT_ADMIN_ROLE.
* @param _hubOwner The account associated with Immutable Hub and other applications that need an "owner".
*/
constructor(address _roleAdmin, address _hubOwner) {
_grantRole(DEFAULT_ADMIN_ROLE, _roleAdmin);
_grantRole(HUB_OWNER_ROLE, _hubOwner);
}

/**
* @dev Renounces the role `role` from the calling account. Prevents the last hub owner and admin from
* renouncing their role.
* @param role The role to renounce.
* @param account The account to renounce the role from.
*/
function renounceRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) {
if ((role == HUB_OWNER_ROLE || role == DEFAULT_ADMIN_ROLE) && (getRoleMemberCount(role) == 1)) {
revert RenounceLastNotAllowed();
}
super.renounceRole(role, account);
}

/**
* @notice Returns the addresses which have a certain role.
* @dev In the unlikely event that there are many accounts with a certain role,
* this function might cause out of memory issues, and fail.
* @param _role Role to return array of admins for.
* @return admins The array of admins with the requested role.
*/
function getAdmins(bytes32 _role) public view returns (address[] memory admins) {
uint256 adminCount = getRoleMemberCount(_role);
admins = new address[](adminCount);
for (uint256 i; i < adminCount; i++) {
admins[i] = getRoleMember(_role, i);
}
return admins;
}

/**
* @notice Return the first account that has OWNER_ROLE.
* @dev Some applications assume there is only one owner and it is returned by the owner function.
* @return address The "owner" of the contract.
*/
function owner() external view returns (address) {
bytes32 role = HUB_OWNER_ROLE;
if (getRoleMemberCount(role) == 0) {
// The only way that there could be no owners is if an account with DEFAULT_ADMIN_ROLE
// revoked all of the accounts with OWNER_ROLE.
return address(0);
}
return getRoleMember(role, 0);
}
}
21 changes: 21 additions & 0 deletions contracts/token/common/MintingHubOwner.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright Immutable Pty Ltd 2018 - 2023
// SPDX-License-Identifier: Apache 2.0
pragma solidity 0.8.19;

// solhint-disable no-unused-import
import {HubOwner, AccessControlEnumerable, AccessControl, IAccessControl} from "./HubOwner.sol";

abstract contract MintingHubOwner is HubOwner {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which application/feature will use MintingHubOwner role?

/// @notice Role to mint tokens
bytes32 public constant MINTER_ROLE = bytes32("MINTER_ROLE");

/**
* @param _roleAdmin The account that administers other roles and other
* accounts with DEFAULT_ADMIN_ROLE.
* @param _hubOwner The account associated with Immutable Hub.
* @param _minterAdmin An account with minter role.
*/
constructor(address _roleAdmin, address _hubOwner, address _minterAdmin) HubOwner(_roleAdmin, _hubOwner) {
_grantRole(MINTER_ROLE, _minterAdmin);
}
}
6 changes: 0 additions & 6 deletions contracts/token/erc20/preset/Errors.sol

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ pragma solidity 0.8.19;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IImmutableERC20Errors} from "./Errors.sol";

/**
* @notice ERC 20 contract that mints a fixed total supply of tokens when the contract
Expand All @@ -16,6 +15,9 @@ import {IImmutableERC20Errors} from "./Errors.sol";
* with a specific Immutable Hub account.
*/
contract ImmutableERC20FixedSupplyNoBurn is Ownable, ERC20 {
// Report an error on attempts to renounce ownership.
error RenounceOwnershipNotAllowed();

/**
* @dev Mints `_totalSupply` number of token and transfers them to `_owner`.
*
Expand All @@ -40,6 +42,6 @@ contract ImmutableERC20FixedSupplyNoBurn is Ownable, ERC20 {
* @notice Prevent calls to renounce ownership.
*/
function renounceOwnership() public pure override {
revert IImmutableERC20Errors.RenounceOwnershipNotAllowed();
revert RenounceOwnershipNotAllowed();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Immutable Pty Ltd 2018 - 2024
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {HubOwner} from "../../common/HubOwner.sol";

/**
* @notice ERC 20 contract that mints a fixed total supply of tokens when the contract
* is deployed.
* @dev This contract has the concept of a hubOwner, called _hubOwner in the constructor.
* This account has no rights to execute any administrative actions within the contract,
* with the exception of renouncing their ownership.
* The Immutable Hub uses this function to help associate the ERC 20 contract
* with a specific Immutable Hub account.
*/

contract ImmutableERC20FixedSupplyNoBurnV2 is HubOwner, ERC20 {
/**
* @dev Mints `_totalSupply` number of token and transfers them to `_hubOwner`.
* @param _roleAdmin The account that has the DEFAULT_ADMIN_ROLE.
* @param _treasurer Initial owner of entire supply of all tokens.
* @param _hubOwner The account associated with Immutable Hub.
* @param _name Name of the token.
* @param _symbol Token symbol.
* @param _totalSupply The fixed supply to be minted.
*/
constructor(
address _roleAdmin,
address _treasurer,
address _hubOwner,
string memory _name,
string memory _symbol,
uint256 _totalSupply
) HubOwner(_roleAdmin, _hubOwner) ERC20(_name, _symbol) {
_mint(_treasurer, _totalSupply);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import {ERC20Permit, ERC20} from "@openzeppelin/contracts/token/ERC20/extensions
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {ERC20Capped} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
import {MintingAccessControl, AccessControl, IAccessControl} from "../../../access/MintingAccessControl.sol";
import {IImmutableERC20Errors} from "./Errors.sol";

/**
* @notice This contract is now deprecated in favour of ImmutableERC20MinterBurnerPermitV2.sol.
*
* @notice ERC 20 contract that wraps Open Zeppelin's ERC 20 contract.
* This contract has the concept of a hubOwner, called _hubOwner in the constructor.
* This account has no rights to execute any administrative actions within the contract,
Expand All @@ -17,6 +18,9 @@ import {IImmutableERC20Errors} from "./Errors.sol";
* with a specific Immutable Hub account.
*/
contract ImmutableERC20MinterBurnerPermit is ERC20Capped, ERC20Burnable, ERC20Permit, MintingAccessControl {
// Report an error if renounceOwnership is called.
error RenounceOwnershipNotAllowed();

/// @notice Role to mint tokens
bytes32 public constant HUB_OWNER_ROLE = bytes32("HUB_OWNER_ROLE");

Expand Down Expand Up @@ -59,7 +63,7 @@ contract ImmutableERC20MinterBurnerPermit is ERC20Capped, ERC20Burnable, ERC20Pe
*/
function renounceRole(bytes32 role, address account) public override(AccessControl, IAccessControl) {
if (getRoleMemberCount(role) == 1 && (role == HUB_OWNER_ROLE || role == DEFAULT_ADMIN_ROLE)) {
revert IImmutableERC20Errors.RenounceOwnershipNotAllowed();
revert RenounceOwnershipNotAllowed();
}
super.renounceRole(role, account);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Immutable Pty Ltd 2018 - 2024
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {ERC20Permit, ERC20} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {ERC20Capped} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
import {MintingHubOwner} from "../../common/MintingHubOwner.sol";

/**
* @notice ERC 20 contract that wraps Open Zeppelin's ERC 20 Permit contract.
* @dev This contract has the concept of a hubOwner, called _hubOwner in the constructor.
* This account has no rights to execute any administrative actions within the contract,
* with the exception of renouncing their ownership.
* The Immutable Hub uses this function to help associate the ERC 20 contract
* with a specific Immutable Hub account.
*/
contract ImmutableERC20MinterBurnerPermitV2 is MintingHubOwner, ERC20Capped, ERC20Burnable, ERC20Permit {
/**
* @dev Delegate to Open Zeppelin's contract.
* @param _roleAdmin The account that has the DEFAULT_ADMIN_ROLE.
* @param _minterAdmin The account that has the MINTER_ROLE.
* @param _hubOwner The account that owns the contract and is associated with Immutable Hub.
* @param _name Name of the token.
* @param _symbol Token symbol.
* @param _maxTokenSupply The maximum supply of the token.
*/
constructor(
address _roleAdmin,
address _minterAdmin,
address _hubOwner,
string memory _name,
string memory _symbol,
uint256 _maxTokenSupply
)
MintingHubOwner(_roleAdmin, _hubOwner, _minterAdmin)
ERC20(_name, _symbol)
ERC20Permit(_name)
ERC20Capped(_maxTokenSupply)
{}

/**
* @dev Mints `amount` number of token and transfers them to the `to` address.
* @param to the address to mint the tokens to.
* @param amount The amount of tokens to mint.
*/
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
_mint(to, amount);
}

/**
* @dev Delegate to Open Zeppelin's ERC20Capped contract.
*/
function _mint(address account, uint256 amount) internal override(ERC20, ERC20Capped) {
ERC20Capped._mint(account, amount);
}
}
2 changes: 1 addition & 1 deletion test/deployer/create2/Create2Utils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity 0.8.19;

import "forge-std/Test.sol";

contract Create2Utils is Test {
abstract contract Create2Utils is Test {
function predictCreate2Address(bytes memory _bytecode, address _deployer, address _sender, bytes32 _salt)
public
pure
Expand Down
2 changes: 1 addition & 1 deletion test/deployer/create3/Create3Utils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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 {
abstract contract Create3Utils is Test {
function predictCreate3Address(IDeployer _deployer, address _sender, bytes32 _salt) public view returns (address) {
return _deployer.deployedAddress("", _sender, _salt);
}
Expand Down
98 changes: 98 additions & 0 deletions test/token/common/HubOwner.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import "forge-std/Test.sol";

import {HubOwner} from "contracts/token/common/HubOwner.sol";


// Contract that isn't abstract, and hence allows the contract to be instantiated
contract HubOwnerImpl is HubOwner {
constructor(address _roleAdmin, address _hubOwner) HubOwner(_roleAdmin, _hubOwner) {
}
}

contract HubOwnerTest is Test {
HubOwner public tokenContract;

address public hubOwner;
address public admin;

function setUp() public virtual {
admin = makeAddr("admin");
hubOwner = makeAddr("hubOwner");

tokenContract = new HubOwnerImpl(admin, hubOwner);
}

function testInit() public {
assertEq(tokenContract.owner(), hubOwner, "owner");
assertTrue(tokenContract.hasRole(tokenContract.DEFAULT_ADMIN_ROLE(), admin));
assertEq(tokenContract.getRoleMemberCount(tokenContract.DEFAULT_ADMIN_ROLE()), 1, "one admin");
assertTrue(tokenContract.hasRole(tokenContract.HUB_OWNER_ROLE(), hubOwner), "hub owner");
assertEq(tokenContract.getRoleMemberCount(tokenContract.HUB_OWNER_ROLE()), 1, "one hub owner");

address[] memory admins = tokenContract.getAdmins(tokenContract.DEFAULT_ADMIN_ROLE());
assertEq(admins.length, 1, "admins length");
assertEq(admins[0], admin, "admins[0]");

address[] memory hubOwners = tokenContract.getAdmins(tokenContract.HUB_OWNER_ROLE());
assertEq(hubOwners.length, 1, "hub owners length");
assertEq(hubOwners[0], hubOwner, "hub owners[0]");
}

function testRenounceAdmin() public {
address secondAdmin = makeAddr("secondAdmin");
vm.startPrank(admin);
tokenContract.grantRole(tokenContract.DEFAULT_ADMIN_ROLE(), secondAdmin);
assertTrue(tokenContract.hasRole(tokenContract.DEFAULT_ADMIN_ROLE(), secondAdmin));

tokenContract.renounceRole(tokenContract.DEFAULT_ADMIN_ROLE(), admin);
assertFalse(tokenContract.hasRole(tokenContract.DEFAULT_ADMIN_ROLE(), admin));
vm.stopPrank();
}

function testRenounceLastAdminBlocked() public {
bytes32 defaultAdminRole = tokenContract.DEFAULT_ADMIN_ROLE();
vm.prank(admin);
vm.expectRevert(abi.encodeWithSelector(HubOwner.RenounceLastNotAllowed.selector));
tokenContract.renounceRole(defaultAdminRole, admin);
}

function testRenounceHubOwner() public {
address secondHubOwner = makeAddr("secondHubOwner");
vm.startPrank(admin);
tokenContract.grantRole(tokenContract.HUB_OWNER_ROLE(), secondHubOwner);
assertTrue(tokenContract.hasRole(tokenContract.HUB_OWNER_ROLE(), secondHubOwner));
vm.stopPrank();

vm.startPrank(hubOwner);
tokenContract.renounceRole(tokenContract.HUB_OWNER_ROLE(), hubOwner);
assertFalse(tokenContract.hasRole(tokenContract.HUB_OWNER_ROLE(), hubOwner));
vm.stopPrank();
}

function testRenounceLastHubOwnerBlocked() public {
bytes32 hubOwnerRole = tokenContract.HUB_OWNER_ROLE();
vm.prank(hubOwner);
vm.expectRevert(abi.encodeWithSelector(HubOwner.RenounceLastNotAllowed.selector));
tokenContract.renounceRole(hubOwnerRole, hubOwner);
}

// Check what happens when owner() is called when there is no hub owner.
function testOwnerWhenNoHubOwner() public {
bytes32 hubOwnerRole = tokenContract.HUB_OWNER_ROLE();
vm.prank(admin);
tokenContract.revokeRole(hubOwnerRole, hubOwner);

// Check the revoke worked.
assertEq(tokenContract.getRoleMemberCount(hubOwnerRole), 0, "no hub owner");

// Check getAdmins worked in this situation too.
address[] memory hubOwners = tokenContract.getAdmins(hubOwnerRole);
assertEq(hubOwners.length, 0, "hub owners length");

address theOwner = tokenContract.owner();
assertEq(theOwner, address(0), "owner when there are now owners");
}
}
14 changes: 14 additions & 0 deletions test/token/common/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Test Plan for Common Token contracts

## HubOwner.sol
This section defines tests for contracts/token/common/HubOwner.sol.
All of these tests are in test/token/common/HubOwner.t.sol.

| Test name |Description | Happy Case | Implemented |
|---------------------------------| --------------------------------------------------|------------|-------------|
| testInit | Check that deployment work. | Yes | Yes |
| testRenounceAdmin | Check that default admins can call renounce. | Yes | Yes |
| testRenounceLastAdminBlocked | Check that the last admin can not call renounce. | No | Yes |
| testRenounceHubOwner | Check that hub owners can call renounce. | Yes | Yes |
| testRenounceLastHubOwnerBlocked | Check that the last hub owner can not call renounce. | No | Yes |
| testOwnerWhenNoHubOwner | Check operation when there are no hub owners. | No | Yes |
Loading
Loading