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
AccessControlUpgradeable
and assigning theBLOCKED_ROLE
to 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
AccessControlUpgradeable
contains therenounceRole
function which allows themsg.sender
to remove their own roles, a blocklisted user can renounce their ownBLOCKED_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.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_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 viasuper.burnFrom()
above) requires the caller to have allowance fromaccount
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 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
bool
ontransfer
.The rescue function uses
IERC20.transfer
to rescue the tokens that expects abool
as 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
IERC20
interface will expect abool
to be returned butUSDT.transfer
doesn't return abool
.Recommendation
In
rescueERC20
, consider usingSafeERC20.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
Minor code improvements
State
Severity
- Severity: Informational
Submitted by
MiloTruck
Context: See below.
Description/Recommendation:
-
EUROPE.sol#L15 -
Initializable
andERC20Upgradeable
do not needed to be inherited byEUROPE
as the other contracts already inherit them. -
EUROPE.sol#L107-L111 - In
transferFrom()
, consider checking thatmsg.sender
does not haveBLOCKED_ROLE
as well to prevent blacklisted addresses performing transfers via allowances. -
EUROPE.sol#L120 - Consider removing the
address(token) != address(this)
check inrescueERC20()
to allowEUROPE
to be rescued from the contract. -
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.