Perper

europe-stablecoin

Cantina Security Report

Organization

@Perper

Engagement Type

Cantina Reviews

Period

-


Findings

Medium Risk

2 findings

2 fixed

0 acknowledged

Low Risk

1 findings

1 fixed

0 acknowledged

Informational

1 findings

1 fixed

0 acknowledged


Medium Risk2 findings

  1. Blocklist can be bypassed by renouncing the BLOCKED_ROLE

    Severity

    Severity: Medium

    Submitted by

    Haxatron


    Context

    Description

    The EUROPE contract implements a blocklist for token transfers using AccessControlUpgradeable and assigning the BLOCKED_ROLE to blocklisted users.

    EUROPE.sol#L100-L111

    // Override transfer function to block transfers for blocked users    function transfer(address to, uint256 amount) public override returns (bool) {        require(!hasRole(BLOCKED_ROLE, _msgSender()), "ERC20: sender is blocked");        require(!hasRole(BLOCKED_ROLE, to), "ERC20: recipient is blocked");        return super.transfer(to, amount);    }
        // Override transferFrom function to block transfers for blocked users    function transferFrom(address from, address to, uint256 amount) public override returns (bool) {        require(!hasRole(BLOCKED_ROLE, from), "ERC20: sender is blocked");        require(!hasRole(BLOCKED_ROLE, to), "ERC20: recipient is blocked");        return super.transferFrom(from, to, amount);    }

    The problem is that since AccessControlUpgradeable contains the renounceRole function which allows the msg.sender to remove their own roles, a blocklisted user can renounce their own BLOCKED_ROLE and thus remove themself from the blocklist.

    AccessControlUpgradeable.sol#L162-L184

    /**     * @dev Revokes `role` from the calling account.     *     * Roles are often managed via {grantRole} and {revokeRole}: this function's     * purpose is to provide a mechanism for accounts to lose their privileges     * if they are compromised (such as when a trusted device is misplaced).     *     * If the calling account had been revoked `role`, emits a {RoleRevoked}     * event.     *     * Requirements:     *     * - the caller must be `callerConfirmation`.     *     * May emit a {RoleRevoked} event.     */    function renounceRole(bytes32 role, address callerConfirmation) public virtual {        if (callerConfirmation != _msgSender()) {            revert AccessControlBadConfirmation();        }
            _revokeRole(role, callerConfirmation);    }

    Recommendation

    Prevent renouncing the BLOCKED_ROLE.

    function renounceRole(bytes32 role, address account) public override {    require(role != BLOCKED_ROLE, "Cannot renounce BLOCKED_ROLE");    super.renounceRole(role, account);}

    Perper: Fixed in commit 425c9f6.

    Cantina: Verified, renounceRole() has been overridden to always revert.

  2. Admin requires allowance to burn tokens from an account through burnFrom()

    Severity

    Severity: Medium

    Submitted by

    MiloTruck


    Context:

    Description:

    The contract includes a burnFrom() function, presumably for an admin with BURNER_ROLE to burn tokens from malicious addresses:

    function burnFrom(address account, uint256 amount)    public    override    onlyRole(BURNER_ROLE){    super.burnFrom(account, amount);}

    However, ERC20BurnableUpgradeable.burnFrom() (which is called via super.burnFrom() above) requires the caller to have allowance from account to burn tokens:

    function burnFrom(address account, uint256 value) public virtual {    _spendAllowance(account, _msgSender(), value);    _burn(account, value);}

    As a result, the admin will not be able to tokens from an address, unless that address explicitly grants allowance to the admin.

    Recommendation:

    Modify burnFrom() to directly call ERC20Upgradeable._burn():

    function burnFrom(address account, uint256 amount)      public      override      onlyRole(BURNER_ROLE)  {-     super.burnFrom(account, amount);+     _burn(account, amount);  }

    Perper: Fixed in commit 425c9f6.

    Cantina: Verified, the recommendation was implemented.

Low Risk1 finding

  1. ERC20 tokens that do not return a bool on transfer cannot be rescued

    Severity

    Severity: Low

    Submitted by

    Haxatron


    Context

    Description

    Certain non-compliant ERC20 tokens such as USDT do not return a bool on transfer.

    The rescue function uses IERC20.transfer to rescue the tokens that expects a bool as return value

    EUROPE.sol#L113-L122

    /**     * @dev Rescues ERC20 tokens sent to the contract by mistake.     * @param token The ERC20 token contract to rescue.     * @param to The address to send the rescued tokens to.     * @param amount The amount of tokens to rescue.     */    function rescueERC20(IERC20 token, address to, uint256 amount) external onlyRole(RESCUER_ROLE) {        require(address(token) != address(this), "Cannot rescue the same token as this contract");        token.transfer(to, amount);    }

    IERC20.sol#L41

    function transfer(address to, uint256 value) external returns (bool);

    Therefore, when attempting to rescue a token such as USDT, the function will revert as the IERC20 interface will expect a bool to be returned but USDT.transfer doesn't return a bool.

    Recommendation

    In rescueERC20, consider using SafeERC20.safeTransfer to allow handling of non-compliant ERC20 tokens such as USDT.

    Perper: Fixed in commit 425c9f6.

    Cantina: Verified, the recommendation was implemented.

Informational1 finding

  1. Minor code improvements

    Severity

    Severity: Informational

    Submitted by

    MiloTruck


    Context: See below.

    Description/Recommendation:

    1. EUROPE.sol#L15 - Initializable and ERC20Upgradeable do not needed to be inherited by EUROPE as the other contracts already inherit them.

    2. EUROPE.sol#L107-L111 - In transferFrom(), consider checking that msg.sender does not have BLOCKED_ROLE as well to prevent blacklisted addresses performing transfers via allowances.

    3. EUROPE.sol#L120 - Consider removing the address(token) != address(this) check in rescueERC20() to allow EUROPE to be rescued from the contract.

    4. EUROPE.sol#L82-L88 - Consider removing the whenNotBlocked modifier as it is not used in the contract.

    Perper: Fixed in commit 425c9f6 and 745e39f.

    Cantina: Verified, the recommendation was implemented.