Core Contracts

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

`totalRemaining` Cannot Be Recovered If Auction Fails

Summary

The auction contract does not provide a mechanism to reclaim unsold ZENO tokens after the auction ends. If the auction does not sell out, the remaining tokens stay locked in the contract indefinitely, resulting in a loss of accessible funds.

Vulnerability Details

  • The auction contract does not include a function to allow the owner to recover unsold ZENO tokens.

  • If the auction does not sell out, the leftover ZENO tokens remain trapped in the contract.

  • Since there is no way to transfer these tokens back to the owner or another address, they become inaccessible forever.

Impact

  • Loss of Recoverable Funds: Unsold ZENO tokens cannot be retrieved or reallocated.

  • Inefficient Token Management: The locked tokens cannot be used for future auctions, staking, or redistribution.

  • Requires Contract Upgrade: Fixing this issue would require deploying a new contract, which is costly and time-consuming.

PoC

import { expect } from "chai";
import hre from "hardhat";
const { ethers } = hre;
describe("Auction Contract - Unsold Token Handling Test", function () {
let Auction, auction, owner, addr1, businessAddress;
let ZENO, zeno, USDC, usdc;
let startTime, endTime;
beforeEach(async function () {
[owner, addr1, businessAddress] = await ethers.getSigners();
// Deploy MockUSDC
USDC = await ethers.getContractFactory("MockUSDC");
usdc = await USDC.deploy();
await usdc.waitForDeployment();
console.log(`MockUSDC deployed at: ${await usdc.getAddress()}`);
// Deploy ZENO Token
const maturityDate = Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60;
ZENO = await ethers.getContractFactory("ZENO");
zeno = await ZENO.deploy(
await usdc.getAddress(),
maturityDate,
"ZENO Token",
"ZENO",
owner.address
);
await zeno.waitForDeployment();
console.log(`ZENO Token deployed at: ${await zeno.getAddress()}`);
// Set auction times
const latestBlock = await ethers.provider.getBlock("latest");
startTime = latestBlock.timestamp + 10; // Starts in 10 sec
endTime = startTime + 3600; // Ends in 1 hour
// Deploy the auction contract
const startingPrice = ethers.parseUnits("100", 18);
const reservePrice = ethers.parseUnits("50", 18);
const totalAllocated = ethers.parseUnits("1000", 18);
Auction = await ethers.getContractFactory("Auction");
auction = await Auction.deploy(
await zeno.getAddress(),
await usdc.getAddress(),
businessAddress.address,
startTime,
endTime,
startingPrice,
reservePrice,
totalAllocated,
owner.address
);
await auction.waitForDeployment();
console.log(`Auction Contract deployed at: ${await auction.getAddress()}`);
// Mint ZENO tokens to auction contract
await zeno.mint(await auction.getAddress(), totalAllocated);
});
it("should check if unsold tokens remain in the auction contract after the auction ends", async function () {
// Move time forward to end auction
console.log("Moving time forward to auction end...");
await ethers.provider.send("evm_setNextBlockTimestamp", [endTime + 1]);
await ethers.provider.send("evm_mine");
console.log("Auction has ended.");
// Check remaining tokens in the auction contract
const auctionBalanceAfterEnd = await zeno.balanceOf(await auction.getAddress());
console.log(`ZENO balance in auction contract after auction: ${ethers.formatUnits(auctionBalanceAfterEnd, 18)} ZENO`);
// Ensure that the tokens are still there (if no reclaim mechanism exists)
expect(auctionBalanceAfterEnd).to.be.gt(0);
});
it("should verify that the owner did not automatically receive unsold tokens", async function () {
// Move time forward to end auction
console.log("Moving time forward to auction end...");
await ethers.provider.send("evm_setNextBlockTimestamp", [endTime + 1]);
await ethers.provider.send("evm_mine");
console.log("Auction has ended.");
// Check the owner's balance
const ownerBalanceAfterEnd = await zeno.balanceOf(owner.address);
console.log(`ZENO balance of owner after auction: ${ethers.formatUnits(ownerBalanceAfterEnd, 18)} ZENO`);
// Ensure that the owner did NOT receive the unsold tokens
expect(ownerBalanceAfterEnd).to.be.equal(0);
});
});

Output:

Auction Contract - Unsold Token Handling Test
MockUSDC deployed at: 0x5FbDB2315678afecb367f032d93F642f64180aa3
ZENO Token deployed at: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
Auction Contract deployed at: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
Auction ended.
Unsold tokens in auction contract: 1000.0 ZENO
✔ should confirm unsold tokens remain in the auction contract
New deployment...
Auction ended.
Owner's ZENO balance: 0.0 ZENO
✔ should verify owner did not receive unsold tokens
2 passing (4s)

Tools Used

Hardhat

Recommendations

Implement a reclaimUnsoldTokens() function that allows the owner to withdraw unsold tokens after the auction ends.

function reclaimUnsoldTokens() external onlyOwner {
require(block.timestamp > state.endTime, "Auction still active");
uint256 unsold = state.totalRemaining;
require(unsold > 0, "No unsold tokens");
state.totalRemaining = 0;
zeno.mint(owner(), unsold);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
6 months ago
inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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