diff --git a/contracts/multicall/GuardedMulticaller.sol b/contracts/multicall/GuardedMulticaller.sol index 69f68278..0a21d639 100644 --- a/contracts/multicall/GuardedMulticaller.sol +++ b/contracts/multicall/GuardedMulticaller.sol @@ -33,6 +33,7 @@ contract GuardedMulticaller is AccessControl, ReentrancyGuard, EIP712 { mapping(bytes32 => bool) private replayProtection; /// @dev Only those with MULTICALL_SIGNER_ROLE can generate valid signatures for execute function. + // forge-lint: disable-next-line(unsafe-typecast) bytes32 public constant MULTICALL_SIGNER_ROLE = bytes32("MULTICALL_SIGNER_ROLE"); /// @dev EIP712 typehash for execute function diff --git a/contracts/token/erc1155/abstract/ImmutableERC1155Base.sol b/contracts/token/erc1155/abstract/ImmutableERC1155Base.sol index a92adfbd..8f35f4c4 100644 --- a/contracts/token/erc1155/abstract/ImmutableERC1155Base.sol +++ b/contracts/token/erc1155/abstract/ImmutableERC1155Base.sol @@ -8,9 +8,9 @@ import {ERC1155Permit, ERC1155} from "./ERC1155Permit.sol"; import {ERC2981} from "openzeppelin-contracts-4/token/common/ERC2981.sol"; import {OperatorAllowlistEnforced} from "../../../allowlist/OperatorAllowlistEnforced.sol"; -import {AccessControlEnumerable, MintingAccessControl} from "../../../access/MintingAccessControl.sol"; +import {AccessControlEnumerable, MintingAccessControlOz4} from "../../utils/MintingAccessControlOz4.sol"; -abstract contract ImmutableERC1155Base is OperatorAllowlistEnforced, ERC1155Permit, ERC2981, MintingAccessControl { +abstract contract ImmutableERC1155Base is OperatorAllowlistEnforced, ERC1155Permit, ERC2981, MintingAccessControlOz4 { /// @dev Contract level metadata string public contractURI; diff --git a/contracts/token/erc20/preset/ImmutableERC20FixedSupplyNoBurn.sol b/contracts/token/erc20/preset/ImmutableERC20FixedSupplyNoBurn.sol index 653592fe..3a3a1d46 100644 --- a/contracts/token/erc20/preset/ImmutableERC20FixedSupplyNoBurn.sol +++ b/contracts/token/erc20/preset/ImmutableERC20FixedSupplyNoBurn.sol @@ -2,8 +2,8 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.19 <=0.8.27; -import {Ownable} from "openzeppelin-contracts-4/access/Ownable.sol"; -import {ERC20} from "openzeppelin-contracts-4/token/ERC20/ERC20.sol"; +import {Ownable} from "openzeppelin-contracts-5/access/Ownable.sol"; +import {ERC20} from "openzeppelin-contracts-5/token/ERC20/ERC20.sol"; import {IImmutableERC20Errors} from "./IImmutableERC20Errors.sol"; /** @@ -26,10 +26,9 @@ contract ImmutableERC20FixedSupplyNoBurn is Ownable, ERC20 { * @param _hubOwner The account associated with Immutable Hub. */ constructor(string memory _name, string memory _symbol, uint256 _totalSupply, address _treasurer, address _hubOwner) - ERC20(_name, _symbol) + ERC20(_name, _symbol) Ownable(_hubOwner) { _mint(_treasurer, _totalSupply); - _transferOwnership(_hubOwner); } /** diff --git a/contracts/token/erc20/preset/ImmutableERC20MinterBurnerPermit.sol b/contracts/token/erc20/preset/ImmutableERC20MinterBurnerPermit.sol index 5ca061cf..f1f3e110 100644 --- a/contracts/token/erc20/preset/ImmutableERC20MinterBurnerPermit.sol +++ b/contracts/token/erc20/preset/ImmutableERC20MinterBurnerPermit.sol @@ -2,11 +2,11 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.19 <=0.8.27; -import {ERC20Permit, ERC20} from "openzeppelin-contracts-4/token/ERC20/extensions/ERC20Permit.sol"; -import {ERC20Burnable} from "openzeppelin-contracts-4/token/ERC20/extensions/ERC20Burnable.sol"; -import {ERC20Capped} from "openzeppelin-contracts-4/token/ERC20/extensions/ERC20Capped.sol"; -import {AccessControl, IAccessControl} from "openzeppelin-contracts-4/access/AccessControl.sol"; -import {MintingAccessControl} from "../../../access/MintingAccessControl.sol"; +import {ERC20Permit, ERC20} from "openzeppelin-contracts-5/token/ERC20/extensions/ERC20Permit.sol"; +import {ERC20Burnable} from "openzeppelin-contracts-5/token/ERC20/extensions/ERC20Burnable.sol"; +import {ERC20Capped} from "openzeppelin-contracts-5/token/ERC20/extensions/ERC20Capped.sol"; +import {AccessControl, IAccessControl} from "openzeppelin-contracts-5/access/AccessControl.sol"; +import {MintingAccessControl} from "../../utils/MintingAccessControl.sol"; import {IImmutableERC20Errors} from "./IImmutableERC20Errors.sol"; /** @@ -66,10 +66,8 @@ contract ImmutableERC20MinterBurnerPermit is ERC20Capped, ERC20Burnable, ERC20Pe super.renounceRole(role, account); } - /** - * @dev Delegate to Open Zeppelin's ERC20Capped contract. - */ - function _mint(address account, uint256 amount) internal override(ERC20, ERC20Capped) { - ERC20Capped._mint(account, amount); - } + /// @inheritdoc ERC20 + function _update(address from, address to, uint256 value) internal virtual override (ERC20Capped, ERC20) { + ERC20Capped._update(from, to, value); + } } diff --git a/contracts/token/erc721/README.md b/contracts/token/erc721/README.md index 684cf92f..fea34872 100644 --- a/contracts/token/erc721/README.md +++ b/contracts/token/erc721/README.md @@ -65,16 +65,6 @@ implement interfaces. However, the preset contracts implement the following inte The contract hierarchy for the preset contracts is shown below. The _Base_ layer combines the ERC 721 capabilities with the operator allow list and access control. The _Permit_ layer adds in the Permit capability. The _Hybrid_ contracts combine mint by ID and mint by quantity capabilities. The _PSI_ contracts provide mint by quantity capability. ``` -ImmutableERC721 -|- ImmutableERC721HybridBase - |- OperatorAllowlistEnforced - |- MintingAccessControl - |- ERC721HybridPermit - |- ERC721Hybrid - |- ERC721PsiBurnable - | |- ERC721Psi - |- Open Zeppelin's ERC721 - ImmutableERC721V2 |- ImmutableERC721HybridBaseV2 |- OperatorAllowlistEnforced diff --git a/contracts/token/erc721/abstract/ImmutableERC721Base.sol b/contracts/token/erc721/abstract/ImmutableERC721Base.sol index 7503afc0..5e77f7ca 100644 --- a/contracts/token/erc721/abstract/ImmutableERC721Base.sol +++ b/contracts/token/erc721/abstract/ImmutableERC721Base.sol @@ -11,7 +11,7 @@ import {OperatorAllowlistEnforced} from "../../../allowlist/OperatorAllowlistEnf // Utils import {BitMaps} from "openzeppelin-contracts-4/utils/structs/BitMaps.sol"; -import {AccessControlEnumerable, MintingAccessControl} from "../../../access/MintingAccessControl.sol"; +import {AccessControlEnumerable, MintingAccessControlOz4} from "../../utils/MintingAccessControlOz4.sol"; // forge-lint: disable-start(pascal-case-struct) @@ -21,7 +21,7 @@ import {AccessControlEnumerable, MintingAccessControl} from "../../../access/Min own minting functionality to meet the needs of the inheriting contract. */ -abstract contract ImmutableERC721Base is OperatorAllowlistEnforced, MintingAccessControl, ERC721Permit, ERC2981 { +abstract contract ImmutableERC721Base is OperatorAllowlistEnforced, MintingAccessControlOz4, ERC721Permit, ERC2981 { using BitMaps for BitMaps.BitMap; /// ===== State Variables ===== diff --git a/contracts/token/erc721/abstract/ImmutableERC721HybridBaseV2.sol b/contracts/token/erc721/abstract/ImmutableERC721HybridBaseV2.sol index 97399286..212159de 100644 --- a/contracts/token/erc721/abstract/ImmutableERC721HybridBaseV2.sol +++ b/contracts/token/erc721/abstract/ImmutableERC721HybridBaseV2.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.19 <=0.8.27; import {ERC721, IERC721} from "openzeppelin-contracts-4/token/ERC721/ERC721.sol"; -import {AccessControlEnumerable, MintingAccessControl} from "../../../access/MintingAccessControl.sol"; +import {AccessControlEnumerable, MintingAccessControlOz4} from "../../utils/MintingAccessControlOz4.sol"; import {ERC2981} from "openzeppelin-contracts-4/token/common/ERC2981.sol"; import {OperatorAllowlistEnforced} from "../../../allowlist/OperatorAllowlistEnforced.sol"; import {ERC721HybridPermitV2} from "./ERC721HybridPermitV2.sol"; @@ -11,7 +11,7 @@ import {ERC721HybridV2} from "./ERC721HybridV2.sol"; abstract contract ImmutableERC721HybridBaseV2 is ERC721HybridPermitV2, - MintingAccessControl, + MintingAccessControlOz4, OperatorAllowlistEnforced, ERC2981 { diff --git a/contracts/token/erc721/interfaces/IImmutableERC721.sol b/contracts/token/erc721/interfaces/IImmutableERC721.sol index d4682e8b..4fdac4fa 100644 --- a/contracts/token/erc721/interfaces/IImmutableERC721.sol +++ b/contracts/token/erc721/interfaces/IImmutableERC721.sol @@ -6,13 +6,13 @@ import {IERC721Metadata} from "openzeppelin-contracts-4/interfaces/IERC721Metada import {IERC2981} from "openzeppelin-contracts-4/interfaces/IERC2981.sol"; import {IERC5267} from "openzeppelin-contracts-4/interfaces/IERC5267.sol"; import {IERC4494} from "../abstract/IERC4494.sol"; -import {IMintingAccessControl} from "../../../access/IMintingAccessControl.sol"; +import {IMintingAccessControlOz4} from "../../utils/IMintingAccessControlOz4.sol"; import {IImmutableERC721Structs} from "./IImmutableERC721Structs.sol"; import {IImmutableERC721Errors} from "./IImmutableERC721Errors.sol"; interface IImmutableERC721 is - IMintingAccessControl, + IMintingAccessControlOz4, IERC2981, IERC721Metadata, IImmutableERC721Structs, diff --git a/contracts/access/IMintingAccessControl.sol b/contracts/token/utils/IMintingAccessControl.sol similarity index 87% rename from contracts/access/IMintingAccessControl.sol rename to contracts/token/utils/IMintingAccessControl.sol index 1f1df80f..8b00ecd5 100644 --- a/contracts/access/IMintingAccessControl.sol +++ b/contracts/token/utils/IMintingAccessControl.sol @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache 2.0 pragma solidity >=0.8.19 <=0.8.27; -import {IAccessControlEnumerable} from "openzeppelin-contracts-4/access/IAccessControlEnumerable.sol"; +import {IAccessControlEnumerable} from "openzeppelin-contracts-5/access/extensions/IAccessControlEnumerable.sol"; interface IMintingAccessControl is IAccessControlEnumerable { /** diff --git a/contracts/token/utils/IMintingAccessControlOz4.sol b/contracts/token/utils/IMintingAccessControlOz4.sol new file mode 100644 index 00000000..1dd4c310 --- /dev/null +++ b/contracts/token/utils/IMintingAccessControlOz4.sol @@ -0,0 +1,29 @@ +// Copyright Immutable Pty Ltd 2018 - 2026 +// SPDX-License-Identifier: Apache 2.0 +pragma solidity >=0.8.19 <=0.8.27; + +import {IAccessControlEnumerable} from "openzeppelin-contracts-4/access/IAccessControlEnumerable.sol"; + +interface IMintingAccessControlOz4 is IAccessControlEnumerable { + /** + * @notice Role to mint tokens + */ + function MINTER_ROLE() external returns (bytes32); + + /** + * @notice Allows admin grant `user` `MINTER` role + * @param user The address to grant the `MINTER` role to + */ + function grantMinterRole(address user) external; + + /** + * @notice Allows admin to revoke `MINTER_ROLE` role from `user` + * @param user The address to revoke the `MINTER` role from + */ + function revokeMinterRole(address user) external; + + /** + * @notice Returns the addresses which have DEFAULT_ADMIN_ROLE + */ + function getAdmins() external view returns (address[] memory); +} diff --git a/contracts/access/MintingAccessControl.sol b/contracts/token/utils/MintingAccessControl.sol similarity index 92% rename from contracts/access/MintingAccessControl.sol rename to contracts/token/utils/MintingAccessControl.sol index 9c79cd64..aac223c3 100644 --- a/contracts/access/MintingAccessControl.sol +++ b/contracts/token/utils/MintingAccessControl.sol @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache 2.0 pragma solidity >=0.8.19 <=0.8.27; -import {AccessControlEnumerable} from "openzeppelin-contracts-4/access/AccessControlEnumerable.sol"; +import {AccessControlEnumerable} from "openzeppelin-contracts-5/access/extensions/AccessControlEnumerable.sol"; abstract contract MintingAccessControl is AccessControlEnumerable { /// @notice Role to mint tokens diff --git a/contracts/token/utils/MintingAccessControlOz4.sol b/contracts/token/utils/MintingAccessControlOz4.sol new file mode 100644 index 00000000..cb75cffb --- /dev/null +++ b/contracts/token/utils/MintingAccessControlOz4.sol @@ -0,0 +1,39 @@ +// Copyright Immutable Pty Ltd 2018 - 2026 +// SPDX-License-Identifier: Apache 2.0 +pragma solidity >=0.8.19 <=0.8.27; + +import {AccessControlEnumerable} from "openzeppelin-contracts-4/access/AccessControlEnumerable.sol"; + +abstract contract MintingAccessControlOz4 is AccessControlEnumerable { + /// @notice Role to mint tokens + // forge-lint: disable-next-line(unsafe-typecast) + bytes32 public constant MINTER_ROLE = bytes32("MINTER_ROLE"); + + /** + * @notice Allows admin grant `user` `MINTER` role + * @param user The address to grant the `MINTER` role to + */ + function grantMinterRole(address user) public onlyRole(DEFAULT_ADMIN_ROLE) { + grantRole(MINTER_ROLE, user); + } + + /** + * @notice Allows admin to revoke `MINTER_ROLE` role from `user` + * @param user The address to revoke the `MINTER` role from + */ + function revokeMinterRole(address user) public onlyRole(DEFAULT_ADMIN_ROLE) { + revokeRole(MINTER_ROLE, user); + } + + /** + * @notice Returns the addresses which have DEFAULT_ADMIN_ROLE + */ + function getAdmins() public view returns (address[] memory) { + uint256 adminCount = getRoleMemberCount(DEFAULT_ADMIN_ROLE); + address[] memory admins = new address[](adminCount); + for (uint256 i; i < adminCount; i++) { + admins[i] = getRoleMember(DEFAULT_ADMIN_ROLE, i); + } + return admins; + } +} diff --git a/test/multicall/GuardedMulticaller.t.sol b/test/multicall/GuardedMulticaller.t.sol index b5705a2c..1aaa54ff 100644 --- a/test/multicall/GuardedMulticaller.t.sol +++ b/test/multicall/GuardedMulticaller.t.sol @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache 2.0 pragma solidity ^0.8.19; -import "forge-std/Test.sol"; +import {Test} from "forge-std/Test.sol"; import {GuardedMulticaller} from "../../contracts/multicall/GuardedMulticaller.sol"; import {MockFunctions} from "./MockFunctions.sol"; import {SigUtils} from "./SigUtils.t.sol"; @@ -327,13 +327,13 @@ contract GuardedMulticallerTest is Test { assertFalse(gmc.hasBeenExecuted(invalidRef)); } - function testIsFunctionPermitted() public { + function testIsFunctionPermitted() public view { assertTrue(gmc.isFunctionPermitted(address(mock), MockFunctions.succeed.selector)); assertTrue(gmc.isFunctionPermitted(address(mock), MockFunctions.revertWithNoReason.selector)); assertFalse(gmc.isFunctionPermitted(address(mock), MockFunctions.notPermitted.selector)); } - function testHashBytesArray() public { + function testHashBytesArray() public view { bytes[] memory data = new bytes[](2); data[0] = abi.encodeWithSignature("succeed()"); data[1] = abi.encodeWithSignature("notSucceed()"); diff --git a/test/token/erc20/preset/ImmutableERC20MinterBurnerPermit.t.sol b/test/token/erc20/preset/ImmutableERC20MinterBurnerPermit.t.sol index af6a4e9c..e945b8bf 100644 --- a/test/token/erc20/preset/ImmutableERC20MinterBurnerPermit.t.sol +++ b/test/token/erc20/preset/ImmutableERC20MinterBurnerPermit.t.sol @@ -5,6 +5,8 @@ import {Test} from "forge-std/Test.sol"; import {ImmutableERC20MinterBurnerPermit} from "contracts/token/erc20/preset/ImmutableERC20MinterBurnerPermit.sol"; import {IImmutableERC20Errors} from "contracts/token/erc20/preset/IImmutableERC20Errors.sol"; +import {ERC20Capped} from "openzeppelin-contracts-5/token/ERC20/extensions/ERC20Capped.sol"; +import {IAccessControl} from "openzeppelin-contracts-5/access/IAccessControl.sol"; contract ImmutableERC20MinterBurnerPermitTest is Test { ImmutableERC20MinterBurnerPermit public erc20; @@ -83,10 +85,9 @@ contract ImmutableERC20MinterBurnerPermitTest is Test { function testOnlyMinterCanMint() public { address to = makeAddr("to"); uint256 amount = 100; + bytes32 minterRole = erc20.MINTER_ROLE(); vm.prank(hubOwner); - vm.expectRevert( - "AccessControl: account 0xa268ae5516b47694c3f15805a560258dbcdefd08 is missing role 0x4d494e5445525f524f4c45000000000000000000000000000000000000000000" - ); + vm.expectRevert(abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, hubOwner, minterRole)); erc20.mint(to, amount); } @@ -110,12 +111,14 @@ contract ImmutableERC20MinterBurnerPermitTest is Test { function testCanOnlyMintUpToMaxSupply() public { address to = makeAddr("to"); - uint256 amount = 1000; + uint256 amount = maxSupply; vm.startPrank(minter); erc20.mint(to, amount); - assertEq(erc20.balanceOf(to), amount); - vm.expectRevert("ERC20Capped: cap exceeded"); - erc20.mint(to, 1); + assertEq(erc20.balanceOf(to), amount, "Balance "); + assertEq(erc20.totalSupply(), amount, "Total supply"); + uint256 extra = 1; + vm.expectRevert(abi.encodeWithSelector(ERC20Capped.ERC20ExceededCap.selector, (amount + extra), maxSupply)); + erc20.mint(to, extra); vm.stopPrank(); } @@ -126,7 +129,7 @@ contract ImmutableERC20MinterBurnerPermitTest is Test { erc20.mint(tokenReceiver, amount); assertEq(erc20.balanceOf(tokenReceiver), 100); vm.prank(tokenReceiver); - erc20.increaseAllowance(operator, amount); + erc20.approve(operator, amount); vm.prank(operator); erc20.burnFrom(tokenReceiver, amount); assertEq(erc20.balanceOf(tokenReceiver), 0);