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";
it("Redeem decimal conversion error", async function() {
const currentTime = await time.latest()
const maturityDate = currentTime + 86400 * 365;
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();
expect(zenoDecimals).to.equal(18);
const oneZenoPriceAtMaturity = ethers.parseUnits("100", usdcDecimals);
auctionStartTime = currentTime + 1 * 3600;
auctionEndTime = auctionStartTime + 86400;
startingPrice = oneZenoPriceAtMaturity;
reservePrice = ethers.parseUnits("10", usdcDecimals);
totalZENOAllocated = ethers.parseUnits("10", zenoDecimals);
totalZENORemaining = totalZENOAllocated;
await auctionFactory.createAuction(
zeno1Address,
usdcAddress,
businessAddress,
auctionStartTime,
auctionEndTime,
startingPrice,
reservePrice,
totalZENOAllocated
);
const auction1Address = await auctionFactory.getAuction(1);
const auction = (await ethers.getContractAt("Auction", auction1Address));
zenoFactory.transferZenoOwnership(1, auction1Address);
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);
await auction.connect(addr1).buy(oneZeno);
const zenoBalanceAfter = await zeno.balanceOf(addr1.address);
expect(zenoBalanceAfter).to.equal(oneZeno);
await usdc.mint(zeno.target, ethers.parseUnits("10000000000000000000000", 6));
await time.setNextBlockTimestamp(maturityDate);
const usdcBeforeRedeem = await usdc.balanceOf(addr1.address);
await zeno.connect(addr1).redeem(oneZeno);
const usdcAfterRedeem = await usdc.balanceOf(addr1.address);
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);
}