Core Contracts

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

Wrong Zeno Amount Minted in `Auction::buy()`

Summary

The Auction::buy() function mints ZENO tokens without adjusting for decimal precision. While USDC (6 decimals) is correctly transferred, ZENO (18 decimals) is minted incorrectly, leading to users receiving significantly fewer tokens than expected. also the amount in the ZENOPurchased event is also wrong.

Vulnerability Details

Lets take an example. If a user wants to buys 1 ZENO, in the buy function the correct cost calculation in USDC with example price is 1 * 98533334 = 98533334 = 98.533334 USDC. The businessAddress gets the correct amount of USDC. However, the buy function does not scale the amount correctly, resulting in only 1 Wei of ZENO being minted instead of 1e18 ZENO.

The decimals of zeno is suppose to be 6 which is a separate issue i have submitted.

/contracts/zeno/Auction.sol:84
84: function buy(uint256 amount) external whenActive {
85: require(amount <= state.totalRemaining, "Not enough ZENO remaining");
86: uint256 price = getPrice(); // @note in USDC decimals i.e. 6
87: uint256 cost = price * amount;
88:
89: require(usdc.transferFrom(msg.sender, businessAddress, cost), "Transfer failed");
90:
91: bidAmounts[msg.sender] += amount;
92: state.totalRemaining -= amount;
93: state.lastBidTime = block.timestamp;
94: state.lastBidder = msg.sender;
95:
96: zeno.mint(msg.sender, amount); // @audit zeno amount will be wrong
97: emit ZENOPurchased(msg.sender, amount, price);
98: }

Here we can see that at line number 87 we are computing the amount of usdc to be transferred to businessAddress. and at line number 96 the amount variable is being passed to zeno::mint() function.
The issue here is that user will transfer more USDC than zeno we mint for user , So when user redeem zeno token he will receive less USDC.

POC

/test/unit/Zeno/Integration.test.js:400
400: it.only("POC: Incorrect Burn and Transfer in ZENO::redeem() Due to Decimal Mismatch ", async function () {
401: // Increase time to auctionStartTime
402: await ethers.provider.send("evm_increaseTime", [3600 * 1.5]);
403: await ethers.provider.send("evm_mine");
404:
405: const amountToBuy = 5;
406:
407:
408:
409: const initialUserUSDCAmount = await usdc.balanceOf(addr1.address);
410:
411: const price = await auction1.getPrice();
412: console.log(price);
413: const cost = parseFloat(price) * amountToBuy;
414: console.log(cost);
415:
416: // Buy ZENO bonds
417: let allowance = await usdc.allowance(addr1.address, auction1Address);
418:
419: if (allowance < cost) {
420: const approveTx = await usdc
421: .connect(addr1)
422: .approve(auction1Address, cost);
423: await approveTx.wait();
424: }
425:
426: await auction1.connect(addr1).buy(amountToBuy);
427:
428: expect(Number(await zeno1.balanceOf(addr1.address))).to.equal(amountToBuy*10**(Number(await zeno1.decimals())));
429:
430: const userUSDCAmountAfterBuy = await usdc.balanceOf(addr1.address);
431:
432: expect(Number(userUSDCAmountAfterBuy)).to.be.equal(Number(initialUserUSDCAmount) - Number(cost));
433:
434: // ========= TRANSFER FROM BUSINESS TO ZENO ============
435:
436: // Increase time to maturity date: one year
437: await ethers.provider.send("evm_increaseTime", [86400 * 365 + 1]);
438: await ethers.provider.send("evm_mine");
439:
440:
441: // Approve the zeno contract
442: allowance = await usdc.allowance(businessAddress, zeno1Address);
443:
444: const approveTx = await usdc
445: .connect(businessAccount)
446: .approve(zeno1Address, cost);
447: await approveTx.wait();
448:
449: // send money from business to zeno1
450: await usdc
451: .connect(businessAccount)
452: .transfer(zeno1Address, cost);
453:
454: // check zeno1 received the money
455: const zenoBalance = await usdc.balanceOf(zeno1Address);
456: expect(zenoBalance).to.equal(cost);
457: // ======== REDEEM ==========
458: await zeno1.connect(addr1).redeem(amountToBuy);
459:
460: expect(await zeno1.balanceOf(addr1.address)).to.equal(0);
461:
462: expect(Number(await usdc.balanceOf(addr1.address)) ).to.be.equal(Number(userUSDCAmountAfterBuy)+ Number(cost));
463:
464: });

Run the POC using npx hardhat test

Impact

Users receive significantly fewer ZENO tokens than they should, leading to loos of USDC amount when user redeem from Zeno contract.

Tools Used

Manual Review, Unit Testing

Recommendations

Update the buy function with the below recommended fix. As the Zeno decimals should be 6 instead of 18, we can pass cost variable instead of amount variable.

/contracts/zeno/Auction.sol
- zeno.mint(msg.sender, amount);
+ zeno.mint(msg.sender, cost);
Updates

Lead Judging Commences

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

ZENO.sol implements fixed 1:1 redemption with USDC regardless of auction purchase price, breaking zero-coupon bond economics and causing user funds to be permanently lost

Support

FAQs

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

Give us feedback!