// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

/// @notice Modern, minimalist, and gas-optimized ERC20 implementation.
/// @author Solbase (https://github.com/Sol-DAO/solbase/blob/main/src/tokens/ERC20/ERC20.sol)
/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20/ERC20.sol)
/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it.
abstract contract ERC20 {
    /// -----------------------------------------------------------------------
    /// Events
    /// -----------------------------------------------------------------------

    event Transfer(address indexed from, address indexed to, uint256 amount);

    event Approval(
        address indexed owner,
        address indexed spender,
        uint256 amount
    );

    /// -----------------------------------------------------------------------
    /// Metadata Storage
    /// -----------------------------------------------------------------------

    string public name;

    string public symbol;

    uint8 public immutable decimals;

    /// -----------------------------------------------------------------------
    /// ERC20 Storage
    /// -----------------------------------------------------------------------

    uint256 public totalSupply;

    mapping(address => uint256) public balanceOf;

    mapping(address => mapping(address => uint256)) public allowance;

    /// -----------------------------------------------------------------------
    /// Constructor
    /// -----------------------------------------------------------------------

    constructor(string memory _name, string memory _symbol, uint8 _decimals) {
        name = _name;
        symbol = _symbol;
        decimals = _decimals;
    }

    /// -----------------------------------------------------------------------
    /// ERC20 Logic
    /// -----------------------------------------------------------------------

    function approve(
        address spender,
        uint256 amount
    ) public virtual returns (bool) {
        allowance[msg.sender][spender] = amount;

        emit Approval(msg.sender, spender, amount);

        return true;
    }

    function transfer(
        address to,
        uint256 amount
    ) public virtual returns (bool) {
        balanceOf[msg.sender] -= amount;

        // Cannot overflow because the sum of all user
        // balances can't exceed the max uint256 value.
        unchecked {
            balanceOf[to] += amount;
        }

        emit Transfer(msg.sender, to, amount);

        return true;
    }

    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual returns (bool) {
        uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals.

        if (allowed != type(uint256).max)
            allowance[from][msg.sender] = allowed - amount;

        balanceOf[from] -= amount;

        // Cannot overflow because the sum of all user
        // balances can't exceed the max uint256 value.
        unchecked {
            balanceOf[to] += amount;
        }

        emit Transfer(from, to, amount);

        return true;
    }

    /// -----------------------------------------------------------------------
    /// Internal Mint Logic
    /// -----------------------------------------------------------------------

    function _mint(address to, uint256 amount) internal virtual {
        totalSupply += amount;

        balanceOf[to] += amount;

        emit Transfer(address(0), to, amount);
    }
}

/// @notice ERC20 + EIP-2612 implementation, including EIP712 logic.
/** @dev Solbase ERC20Permit implementation (https://github.com/Sol-DAO/solbase/blob/main/src/tokens/ERC20/extensions/ERC20Permit.sol)
 ** plus Solbase EIP712 implementation (https://github.com/Sol-DAO/solbase/blob/main/src/utils/EIP712.sol)
 ** NOTE: only difference from Solbase EIP712 is the EIP-712 domain variables are internal rather than immutable,
 ** because they are read during contract creation */

abstract contract ERC20Permit is ERC20 {
    /// -----------------------------------------------------------------------
    /// EIP-712 Domain Variables
    /// -----------------------------------------------------------------------

    /// @dev `keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")`.
    bytes32 internal constant DOMAIN_TYPEHASH =
        0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f;

    bytes32 internal hashedDomainName;

    bytes32 internal hashedDomainVersion;

    bytes32 internal initialDomainSeparator;

    uint256 internal initialChainId;

    /// -----------------------------------------------------------------------
    /// EIP-2612 Constants
    /// -----------------------------------------------------------------------

    /// @dev `keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")`.
    bytes32 public constant PERMIT_TYPEHASH =
        0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;

    /// -----------------------------------------------------------------------
    /// EIP-2612 Storage
    /// -----------------------------------------------------------------------

    mapping(address => uint256) public nonces;

    /// -----------------------------------------------------------------------
    /// Custom Errors
    /// -----------------------------------------------------------------------

    error PermitExpired();

    error InvalidSigner();

    /// -----------------------------------------------------------------------
    /// Constructor
    /// -----------------------------------------------------------------------

    constructor(
        string memory _name,
        string memory _symbol,
        string memory _version,
        uint8 _decimals
    ) ERC20(_name, _symbol, _decimals) {
        hashedDomainName = keccak256(bytes(_name));

        hashedDomainVersion = keccak256(bytes(_version));

        initialDomainSeparator = _computeDomainSeparator();

        initialChainId = block.chainid;
    }

    /// -----------------------------------------------------------------------
    /// EIP-2612 Logic
    /// -----------------------------------------------------------------------

    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public virtual {
        if (block.timestamp > deadline) revert PermitExpired();

        // Unchecked because the only math done is incrementing
        // the owner's nonce which cannot realistically overflow.
        unchecked {
            address recoveredAddress = ecrecover(
                _computeDigest(
                    keccak256(
                        abi.encode(
                            PERMIT_TYPEHASH,
                            owner,
                            spender,
                            value,
                            nonces[owner]++,
                            deadline
                        )
                    )
                ),
                v,
                r,
                s
            );

            if (recoveredAddress == address(0)) revert InvalidSigner();

            if (recoveredAddress != owner) revert InvalidSigner();

            allowance[recoveredAddress][spender] = value;
        }

        emit Approval(owner, spender, value);
    }

    /// -----------------------------------------------------------------------
    /// EIP-712 Logic
    /// -----------------------------------------------------------------------

    function domainSeparator() public view virtual returns (bytes32) {
        return
            block.chainid == initialChainId
                ? initialDomainSeparator
                : _computeDomainSeparator();
    }

    function _computeDomainSeparator() internal view virtual returns (bytes32) {
        return
            keccak256(
                abi.encode(
                    DOMAIN_TYPEHASH,
                    hashedDomainName,
                    hashedDomainVersion,
                    block.chainid,
                    address(this)
                )
            );
    }

    function _computeDigest(
        bytes32 hashStruct
    ) internal view virtual returns (bytes32) {
        return
            keccak256(
                abi.encodePacked("\x19\x01", domainSeparator(), hashStruct)
            );
    }
}

/// @notice Enshroud ERC20 token contract
/// @dev mintable by isMinter role mapping; not burnable; ERC20Permit implemented
contract EnshroudToken is ERC20Permit {
    /// -----------------------------------------------------------------------
    /// ERC20 data
    /// -----------------------------------------------------------------------
    string public constant ENSHROUDTOKEN_NAME = "Enshroud Token";
    string public constant ENSHROUDTOKEN_SYMBOL = "ENSHROUD";
    string public constant ENSHROUDTOKEN_VERSION = "1";
    uint8 public constant ENSHROUDTOKEN_DECIMALS = 18;

    mapping(address => bool) private isMinter;

    /// -----------------------------------------------------------------------
    /// Errors
    /// -----------------------------------------------------------------------

    error NotMinter();

    /// -----------------------------------------------------------------------
    /// Events
    /// -----------------------------------------------------------------------

    event MinterStatusUpdated(address minter, bool status);

    event TokensMinted(address recipient, uint256 amount);

    /// -----------------------------------------------------------------------
    /// Constructor
    /// -----------------------------------------------------------------------

    /// include initial mint and isMinter status here
    /// @param _initialMinter: address initially designated as a minter, presumably deployer and/or DAO governance
    constructor(
        address _initialMinter
    )
        ERC20Permit(
            ENSHROUDTOKEN_NAME,
            ENSHROUDTOKEN_SYMBOL,
            ENSHROUDTOKEN_VERSION,
            ENSHROUDTOKEN_DECIMALS
        )
    {
        isMinter[_initialMinter] = true;
    }

    /// @notice mints '_amount' of Enshroud Token to '_addr'
    /// @param _addr address that will receive the minted $ENSHROUD tokens
    /// @param _amount amount of $ENSHROUD tokens that will be minted
    function mint(address _addr, uint256 _amount) external {
        if (!isMinter[msg.sender]) revert NotMinter();
        _mint(_addr, _amount);
        emit TokensMinted(_addr, _amount);
    }

    /// @notice updates if an address is authorized to mint tokens, by an existing minter
    /// @param _minterAddress address whose minter authorization status will be updated
    /// @param _minterStatus: updated minter authorization status
    function updateMinterStatus(
        address _minterAddress,
        bool _minterStatus
    ) external {
        if (!isMinter[msg.sender]) revert NotMinter();
        isMinter[_minterAddress] = _minterStatus;
        emit MinterStatusUpdated(_minterAddress, _minterStatus);
    }

    /// @notice returns if an address is authorized to mint tokens
    /// @param _addr queried minter address
    function getMinterStatus(address _addr) external view returns (bool) {
        return isMinter[_addr];
    }
}
