Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Valid

Decimals Mismatch Between `ZENO` and `USDC` in Redemption Functions in `ZENO.sol` contract

Summary

The redeem and redeemAll functions in the ZENO.sol contract fail to account for the difference in decimal places between ZENO (18 decimals) and USDC (6 decimals). As a result, users may receive an incorrect amount of USDC when redeeming ZENO tokens. This can lead to a severe financial imbalance where users may either receive excess or insufficient USDC, making the contract vulnerable to exploitation.

Vulnerability Details

In Solidity, ERC-20 tokens typically have a decimals function that returns the number of decimal places the token uses. Since your ZENO.sol contract inherits from ERC20, the decimals() function is already defined in OpenZeppelin's ERC20 contract.

This is the link to the OpenZeppelin documentation for ERC20:

By default, ERC20 uses a value of 18 for decimals.
To use a different value, you will need to override the decimals() function in your contract.

So, the default decimals in OpenZeppelin’s ERC20 contract is 18 unless overridden.
Since, the ZENO.sol contract does not override the decimals() function, ZENO has 18 decimals by default.

This is the ZENO.sol smart contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20, ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "../interfaces/zeno/IZENO.sol";
/**
Keep track of the maturity date of the bond
*/
contract ZENO is IZENO, ERC20, Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
IERC20 public immutable USDC;
uint256 public immutable MATURITY_DATE;
uint256 public totalZENOMinted;
uint256 public totalZENORedeemed;
constructor(
address _usdc,
uint256 _maturityDate,
string memory _name,
string memory _symbol,
address _initialOwner
) Ownable(_initialOwner) ERC20(_name, _symbol) {
USDC = IERC20(_usdc);
MATURITY_DATE = _maturityDate;
}
/**
CAN BE CALLED ONLY BY ASSOCIATED AUCTION CONTRACT (THE OWNER)
*/
function mint(address to, uint256 amount) external onlyOwner {
if (amount == 0) {
revert ZeroAmount();
}
_mint(to, amount);
totalZENOMinted += amount;
}
function isRedeemable() public view returns (bool _redeemable) {
_redeemable = (block.timestamp >= MATURITY_DATE);
}
function redeem(uint amount) external nonReentrant {
if (!isRedeemable()) {
revert BondNotRedeemable();
}
if (amount == 0) {
revert ZeroAmount();
}
uint256 totalAmount = balanceOf(msg.sender);
if (amount > totalAmount) {
revert InsufficientBalance();
}
totalZENORedeemed += amount;
_burn(msg.sender, amount);
USDC.safeTransfer(msg.sender, amount); //@audit
}
function redeemAll() external nonReentrant {
if (!isRedeemable()) {
revert BondNotRedeemable();
}
uint256 amount = balanceOf(msg.sender);
totalZENORedeemed += amount;
_burn(msg.sender, amount);
USDC.safeTransfer(msg.sender, amount); //@audit
}
function getDetails() external view returns (ZENODetails memory) {
return ZENODetails(address(this), MATURITY_DATE, name(), symbol());
}
}
  • ZENO tokens have 18 decimals (1 ZENO = 10¹⁸ smallest units).

  • USDC tokens have 6 decimals (1 USDC = 10⁶ smallest units).

  • The redeem and redeemAll function directly transfers amount of USDC without adjusting for decimal difference.

  • This causes a 1e12 (10¹²) scaling error, leading to significant financial discrepancies.

Proof of Concept (PoC)

Assumptions:

  • User holds 1 ZENO (which is 1 * 10¹⁸ in its smallest unit).

  • Expected USDC equivalent should be 1 USDC (which is 1 * 10⁶ in its smallest unit).

  • However, due to the lack of decimal conversion, the contract transfers 1 * 10¹⁸ USDC, which is 1 trillion times the expected amount!

Steps to Reproduce:

  1. A user calls redeem(1 * 10¹⁸).

  2. The contract burns 1 ZENO.

  3. The contract transfers 1 * 10¹⁸ USDC instead of 1 * 10⁶ USDC.

  4. The user receives excess USDC, leading to a financial imbalance.

Impact

  • High: Users can receive a significantly larger amount of USDC than expected, causing loss of funds for the protocol.

  • High: Alternatively, if the contract were modified for a different conversion rate, users might receive significantly less USDC than they should.

  • Critical Financial Risk: If exploited, users can drain the USDC reserves by repeatedly redeeming ZENO tokens.

Tools Used

Manual Review

Recommendations

To fix this issue, ensure proper decimal conversion before transferring USDC

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Decimal precision mismatch between ZENO token (18 decimals) and USDC (6 decimals) not accounted for in redemption, causing calculation errors and incorrect payments

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.