Core Contracts

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

Incorrect decimal handling in `Auction::buy()` leads to massive overpayment for ZENO tokens

Summary

The buy() function in the Auction contract fails to account for the decimal difference between USDC (6 decimals) and ZENO (18 decimals), causing users to overpay by a factor of 10^18 when purchasing ZENO tokens.

Vulnerability Details

The buy() function calculates the cost of ZENO tokens using:

uint256 cost = price * amount;

However, this calculation doesn't account for:

  • USDC has 6 decimals

  • ZENO has 18 decimals

  • Price is in USDC (6 decimals)

  • Amount is in ZENO (18 decimals)

The correct calculation should be:

uint256 cost = (price * amount) / 1e18;

Example:

  • User wants to buy 1 ZENO (1e18 wei)

  • Price is 100 USDC (100e6)

  • Current calculation: 100e6 1e18 = 100e24 USDC (100 quintillion USDC)

  • Correct calculation: (100e6 1e18) / 1e18 = 100e6 USDC (100 USDC)

Impact

Users attempting to purchase ZENO tokens will:

  1. Have their transactions revert due to insufficient USDC balance

  2. If they have enough balance, they will overpay by a factor of 10^18

  3. Makes the auction effectively unusable

Tools Used

Manual review

Proof of Concept

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

import { time } from "@nomicfoundation/hardhat-network-helpers";
// ... other code ...
it("Incorrect decimal handling in `Auction::buy()`", async function () {
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);
// Set Auction contract
auctionStartTime = currentTime + 1 * 3600; // 1 hour later
auctionEndTime = auctionStartTime + 86400; // 1 day later
startingPrice = ethers.parseUnits("100", usdcDecimals); // 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);
// 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 amountToBuy = ethers.parseUnits("1", zenoDecimals); // 1 ZENO bond
const usdcBalanceBefore = await usdc.balanceOf(addr1.address);
await auction.connect(addr1).buy(amountToBuy);
const zenoBalanceAfter = await zeno.balanceOf(addr1.address);
// Gets 1 ZENO bond
expect(zenoBalanceAfter).to.equal(amountToBuy);
const cost = await auction.getPrice();
const expectedCost = (cost * amountToBuy) / ethers.parseUnits("1", zenoDecimals);
const actualInflatedCost = (cost * amountToBuy);
// Expected cost: 99.998959 USDC
expect(expectedCost).to.equal(ethers.parseUnits("99.998959", usdcDecimals));
expect(actualInflatedCost).to.equal(ethers.parseUnits("99.998959", zenoDecimals + usdcDecimals));
const usdcBalanceAfter = await usdc.balanceOf(addr1.address);
expect(usdcBalanceAfter).to.not.equal(usdcBalanceBefore - expectedCost);
expect(usdcBalanceAfter).to.equal(usdcBalanceBefore - actualInflatedCost);
// Actual cost: 99,998,959,000,000,000,000.000000 USDC
console.log("actualInflatedCost", actualInflatedCost);
expect(actualInflatedCost).to.equal(expectedCost * ethers.parseUnits("1", 18));
});

Recommendations

Modify the buy() function to properly handle decimals:

function buy(uint256 amount) external whenActive {
require(amount <= state.totalRemaining, "Not enough ZENO remaining");
uint256 price = getPrice();
// Divide by 1e18 to account for ZENO decimals
- uint256 cost = price * amount;
+ uint256 cost = (price * amount) / 1e18; // adjusted for ZENO decimals
require(usdc.transferFrom(msg.sender, businessAddress, cost), "Transfer failed");
// ... rest of the function
}
Updates

Lead Judging Commences

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

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

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

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.