// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {IPermissionedMintingManager} from "./interfaces/IPermissionedMintingManager.sol"; /** * @title PermissionedMintingERC20 * @dev ERC20 token contract with permissioned minting, burning, and cross-chain transfer functionality. * The contract uses AccessControl to manage roles, ensuring that only accounts with the appropriate roles can update the permissioned minting manager. */ abstract contract PermissionedMintingERC20 is ERC20, AccessControl, ReentrancyGuard { // Role definitions for updating the permissioned manager and admin access bytes32 public constant UPDATER_ROLE = keccak256("UPDATER_ROLE"); bytes32 public constant UPDATER_ADMIN_ROLE = keccak256("UPDATER_ADMIN_ROLE"); // Address of the contract that manages permissioned minting rules IPermissionedMintingManager public permissionedMintingManager; // Events /** * @dev Emitted when the permissioned minting manager is updated. * @param newManagerAddress The address of the new permissioned minting manager. * @param senderAddress The address of the account that updated the manager. */ event NewPermissionedMintingManager( address indexed newManagerAddress, address indexed senderAddress ); /** * @dev Emitted when the roles of the contract is transferred to a new account. * @param newAddress The address of the new admin. * @param oldAddress The address of the previous admin. */ event RolesTransferred( address indexed newAddress, address indexed oldAddress ); // Custom Errors error NoZeroAddress(); // Thrown when a zero address is provided error CrossChainTransferNotSupported(); // There is no support for this feature error NewAdminIsOldAdmin(); // The new admin is same as the old admin /** * @dev Constructor that initializes the token with its name, symbol, and default roles. * @param tokenName The name of the ERC20 token. * @param tokenSymbol The symbol of the ERC20 token. * @param permissionedMintingManagerAddress Initial PermissionedMintingManager Address. * @param initialOwner The address of the initial owner of the contract. */ constructor( string memory tokenName, string memory tokenSymbol, address permissionedMintingManagerAddress, address initialOwner ) ERC20(tokenName, tokenSymbol) { _grantRole(DEFAULT_ADMIN_ROLE, initialOwner); // Admin role for managing roles _grantRole(UPDATER_ROLE, initialOwner); // Role for updating the minting manager _grantRole(UPDATER_ADMIN_ROLE, initialOwner); // Admin role for managing UPDATER_ROLE _setRoleAdmin(UPDATER_ROLE, UPDATER_ADMIN_ROLE); // UPDATER_ADMIN_ROLE manages UPDATER_ROLE permissionedMintingManager = IPermissionedMintingManager( permissionedMintingManagerAddress ); emit NewPermissionedMintingManager( permissionedMintingManagerAddress, msg.sender ); } /** * @notice Transfers roles of the contract to a new address. * @dev Can only be called by accounts with the DEFAULT_ADMIN_ROLE. The new admin will receive all roles. * @param newAdmin The address of the new admin. */ function transferRoles( address newAdmin ) external onlyRole(DEFAULT_ADMIN_ROLE) { if (newAdmin == address(0)) { revert NoZeroAddress(); } address oldAdmin = msg.sender; if (oldAdmin == newAdmin) { revert NewAdminIsOldAdmin(); } // Grant all roles to new owner _grantRole(DEFAULT_ADMIN_ROLE, newAdmin); _grantRole(UPDATER_ROLE, newAdmin); _grantRole(UPDATER_ADMIN_ROLE, newAdmin); // revoke roles from old owner _revokeRole(UPDATER_ROLE, oldAdmin); _revokeRole(UPDATER_ADMIN_ROLE, oldAdmin); _revokeRole(DEFAULT_ADMIN_ROLE, oldAdmin); emit RolesTransferred(newAdmin, oldAdmin); } /** * @notice Updates the permissioned minting manager contract. * @dev Can only be called by accounts with the UPDATER_ROLE. If a previous manager exists, it checks if the update is allowed. * If the update is allowed, the new contract is set and the state is migrated from the old manager to the new one. * @param newManagerAddress The address of the new permissioned minting manager contract. */ function setPermissionedMintingManager( address newManagerAddress ) external onlyRole(UPDATER_ROLE) nonReentrant { // Check if a permissioned minting manager already exists if (_isPermissionManagerSet()) { // slither-disable-next-line reentrancy-no-eth bool isAllowed = permissionedMintingManager.isUpdateAllowed( newManagerAddress, msg.sender ); if (!isAllowed) return; } // Store the old permissioned manager address for state migration address oldManagerAddress = address(permissionedMintingManager); // Set the new permissioned manager permissionedMintingManager = IPermissionedMintingManager( newManagerAddress ); // If there was a previous manager, migrate the state to the new manager if (oldManagerAddress != address(0)) { permissionedMintingManager.migrateStateFromContract( oldManagerAddress ); } // Emit an event to notify the change in the permissioned manager emit NewPermissionedMintingManager(newManagerAddress, msg.sender); } /** * @notice Checks if the permissioned minting manager exists. * @return bool True if a permissioned manager is set, false otherwise. */ function _isPermissionManagerSet() private view returns (bool) { return address(permissionedMintingManager) != address(0); } /** * @dev Overriding ERC20's _update function to include hooks for minting and burning. * @dev This function is called when tokens are minted, burned, or transferred. * @param from The address from which tokens are transferred. * @param to The address to which tokens are transferred. * @param value The amount of tokens to transfer. */ function _update( address from, address to, uint256 value ) internal override { if (from == address(0)) { // check before mint hook _beforeMint(to, value); } if (from != address(0) && to != address(0)) { // check before transfer hook _beforeTransfer(from, to, value); } super._update(from, to, value); if (to == address(0)) { // execute after burn hook _afterBurn(from, value); } } /** * @dev Internal transfer function that checks permissions with the permissioned minting manager before transferring tokens. * If there is no permission manager, the transfer will go through. * @param from The address from which tokens are transferred. * @param to The address to which tokens are transferred. * @param value The amount of tokens to transfer. */ function _beforeTransfer(address from, address to, uint256 value) internal { if (_isPermissionManagerSet()) { permissionedMintingManager.checkTransferPermission(from, to, value); } } /** * @dev Internal mint function that checks permissions with the permissioned minting manager before minting tokens. * If there is no permission manager, the transaction will go through. * @param account The account to receive the minted tokens. * @param amount The amount of tokens to mint. */ function _beforeMint(address account, uint256 amount) internal { if (_isPermissionManagerSet()) { permissionedMintingManager.checkMintingPermission( account, amount, msg.sender ); } } /** * @dev Internal burn function that triggers redemption with the permissioned manager after burning tokens. * @param account The account from which tokens will be burned. * @param amount The amount of tokens to burn. */ function _afterBurn(address account, uint256 amount) internal { if (_isPermissionManagerSet()) { permissionedMintingManager.triggerRedemption(account, amount); } } /** * @notice Performs a cross-chain token transfer. * @dev This burns tokens on the current chain and notifies the permissioned manager to transfer * the tokens to the destination chain. * @param chainId The ID of the destination chain. * @param to The recipient address on the destination chain. * @param amount The amount of tokens to transfer. */ function crossChainTransfer( uint16 chainId, address to, uint256 amount ) external nonReentrant { if (!_isPermissionManagerSet()) { revert CrossChainTransferNotSupported(); } super._burn(msg.sender, amount); // Burn tokens on the current chain permissionedMintingManager.transferToChain(chainId, to, amount); // Trigger cross-chain transfer } }