europe-stablecoin
Cantina Security Report
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
Blocklist can be bypassed by renouncing the BLOCKED_ROLE
State
Severity
- Severity: Medium
Submitted by
Haxatron
Context
Description
The EUROPE contract implements a blocklist for token transfers using
AccessControlUpgradeableand assigning theBLOCKED_ROLEto blocklisted users.// 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
AccessControlUpgradeablecontains therenounceRolefunction which allows themsg.senderto remove their own roles, a blocklisted user can renounce their ownBLOCKED_ROLEand 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.Admin requires allowance to burn tokens from an account through burnFrom()
State
Severity
- Severity: Medium
Submitted by
MiloTruck
Context:
Description:
The contract includes a
burnFrom()function, presumably for an admin withBURNER_ROLEto 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 viasuper.burnFrom()above) requires the caller to have allowance fromaccountto 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 callERC20Upgradeable._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
ERC20 tokens that do not return a bool on transfer cannot be rescued
State
Severity
- Severity: Low
Submitted by
Haxatron
Context
Description
Certain non-compliant ERC20 tokens such as USDT do not return a
boolontransfer.The rescue function uses
IERC20.transferto rescue the tokens that expects aboolas return value/** * @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); }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
IERC20interface will expect aboolto be returned butUSDT.transferdoesn't return abool.Recommendation
In
rescueERC20, consider usingSafeERC20.safeTransferto allow handling of non-compliant ERC20 tokens such as USDT.Perper: Fixed in commit 425c9f6.
Cantina: Verified, the recommendation was implemented.
Informational1 finding
Minor code improvements
State
Severity
- Severity: Informational
Submitted by
MiloTruck
Context: See below.
Description/Recommendation:
-
EUROPE.sol#L15 -
InitializableandERC20Upgradeabledo not needed to be inherited byEUROPEas the other contracts already inherit them. -
EUROPE.sol#L107-L111 - In
transferFrom(), consider checking thatmsg.senderdoes not haveBLOCKED_ROLEas well to prevent blacklisted addresses performing transfers via allowances. -
EUROPE.sol#L120 - Consider removing the
address(token) != address(this)check inrescueERC20()to allowEUROPEto be rescued from the contract. -
EUROPE.sol#L82-L88 - Consider removing the
whenNotBlockedmodifier as it is not used in the contract.
Perper: Fixed in commit 425c9f6 and 745e39f.
Cantina: Verified, the recommendation was implemented.