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();
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);
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:
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:
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:
435:
436:
437: await ethers.provider.send("evm_increaseTime", [86400 * 365 + 1]);
438: await ethers.provider.send("evm_mine");
439:
440:
441:
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:
450: await usdc
451: .connect(businessAccount)
452: .transfer(zeno1Address, cost);
453:
454:
455: const zenoBalance = await usdc.balanceOf(zeno1Address);
456: expect(zenoBalance).to.equal(cost);
457:
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);