Core Contracts

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

Decimal Mismatch Between ZENO and USDC

Summary

After reviewing ZENO.sol, I can confirm there is a critical decimal precision mismatch between ZENO tokens (18 decimals) and USDC (6 decimals) that leads to significant economic vulnerabilities.

Vulnerable Code

contract ZENO is IZENO, ERC20, Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
IERC20 public immutable USDC; // 6 decimals
// Inherits 18 decimals from ERC20
constructor(
address _usdc,
uint256 _maturityDate,
string memory _name,
string memory _symbol,
address _initialOwner
) Ownable(_initialOwner) ERC20(_name, _symbol) {
USDC = IERC20(_usdc);
}
function redeem(uint amount) external nonReentrant {
// ...
_burn(msg.sender, amount); // Burns 18 decimal ZENO
USDC.safeTransfer(msg.sender, amount); // Transfers same amount in 6 decimal USDC
}
}

Impact

  1. Economic Exploitation

// Attack Scenario
function exploitDecimals() {
// 1. Buy 1 ZENO token (1e18 units)
auction.buy(1e18);
// 2. Redeem 1 ZENO after maturity
zeno.redeem(1e18);
// Expected: Receive 1 USDC (1e6 units)
// Actual: Receive 1e18 USDC units (1 trillion USDC)
// Profit: 999,999,999,999 USDC
}
  1. Protocol Insolvency

Initial USDC Balance: 1,000,000 USDC (1e6 * 1e6 units)
User redeems 1 ZENO: Attempts to transfer 1e18 USDC units
Result: Contract depleted, unable to honor redemptions

Proof of Concept

contract DecimalMismatchTest {
ZENO zeno;
IERC20 usdc;
function testDecimalMismatch() public {
// Setup
usdc.approve(address(zeno), type(uint256).max);
// Deposit 1 USDC
uint256 depositAmount = 1e6; // 1 USDC
// Get ZENO tokens
uint256 zenoAmount = 1e18; // 1 ZENO
// Redeem after maturity
vm.warp(zeno.MATURITY_DATE() + 1);
zeno.redeem(zenoAmount);
// Check USDC received
uint256 usdcReceived = usdc.balanceOf(address(this));
assertEq(usdcReceived, 1e18); // Receives 1e12 times more USDC
}
}

Recommended Mitigation

contract ZENO is IZENO, ERC20, Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
uint8 public constant USDC_DECIMALS = 6;
uint8 public constant ZENO_DECIMALS = 18;
uint256 public constant DECIMAL_ADJUSTMENT = 10**(ZENO_DECIMALS - USDC_DECIMALS);
constructor(
address _usdc,
uint256 _maturityDate,
string memory _name,
string memory _symbol,
address _initialOwner
) Ownable(_initialOwner) ERC20(_name, _symbol) {
USDC = IERC20(_usdc);
require(ERC20(_usdc).decimals() == USDC_DECIMALS, "Invalid USDC decimals");
}
function redeem(uint256 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);
// Adjust for decimal difference
uint256 usdcAmount = amount / DECIMAL_ADJUSTMENT;
USDC.safeTransfer(msg.sender, usdcAmount);
emit Redeemed(msg.sender, amount, usdcAmount);
}
}

Additional Recommendations

  1. Add decimal validation in constructor:

require(decimals() == ZENO_DECIMALS, "Invalid ZENO decimals");
  1. Add events for tracking:

event Redeemed(
address indexed user,
uint256 zenoAmount,
uint256 usdcAmount
);
  1. Update tests to verify decimal handling:

function testCorrectDecimalHandling() public {
uint256 zenoAmount = 1e18; // 1 ZENO
uint256 expectedUsdc = 1e6; // 1 USDC
zeno.redeem(zenoAmount);
assertEq(usdc.balanceOf(address(this)), expectedUsdc);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 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

Auction.sol's buy() function multiplies ZENO amount (18 decimals) by price (6 decimals) without normalization, causing users to pay 1 trillion times the intended USDC amount

Support

FAQs

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

Give us feedback!