Summary
A critical vulnerability was identified in the Auction.sol contract, where it allows an invalid price range (startingPrice < reservePrice
). Additionally, the getPrice()
function may cause an underflow, leading to a potential Denial of Service (DoS) attack. These issues could compromise the integrity of the auction process.
Vulnerability Details
-> Auction Deploys with Invalid Price Ranges
The contract allows deployment even when startingPrice < reservePrice
.
This breaks auction logic, as the auction price is expected to start high and decrease over time, but instead, it starts below the reserve price.
Attackers or users could exploit this bug to manipulate bidding behavior.
Test Failure Output:
Testing invalid price range...
WARNING: Vulnerability detected! The contract allowed an invalid price range.
AssertionError: Auction should not deploy when startingPrice < reservePrice
-> getPrice() Underflow May Cause DoS
If price calculations underflow, getPrice()
may fail and revert transactions.
This can cause a Denial of Service (DoS) risk, preventing users from interacting with the auction.
Test Failure Output:
Deploying vulnerable contract...
WARNING: Vulnerability detected! Auction deployed with an invalid price range.
Calling getPrice()...
AssertionError: Expected transaction to be reverted
How This Leads to Denial of Service (DoS)?
If startingPrice < reservePrice, the price calculation could cause an underflow when subtracting values.
Solidity reverts the transaction when an underflow occurs, blocking all further interactions with the contract.
When getPrice()
fails and reverts, users cannot get the current auction price.
Since price retrieval is necessary for placing bids, bidders are unable to participate.
This creates a Denial of Service (DoS) attack vector, where the auction becomes unusable, preventing valid buyers from purchasing assets.
-> Exploit Scenario
getPrice()
reverts when called, locking the auction.
Since no bids can be placed, the auction fails, potentially allowing the attacker to manipulate or disrupt the sale
Impact
Auction Integrity Compromised: Invalid price ranges break the descending auction logic, allowing unintended price manipulation.
Denial of Service (DoS): If getPrice()
underflows, bidders may not be able to place bids.
Potential Financial Exploits: Malicious actors could force invalid price conditions, leading to incorrect auction results.
PoC
-> Deploying Auction with an Invalid Price Range
The contract incorrectly allows deployment when startingPrice < reservePrice
. This should be blocked, but the current implementation does not enforce this check.
it("should revert when startingPrice is less than reservePrice", async function () {
const invalidStartingPrice = ethers.parseUnits("50", 18);
const invalidReservePrice = ethers.parseUnits("100", 18);
console.log("🚀 Testing invalid price range...");
try {
auction = await Auction.deploy(
await zeno.getAddress(),
await usdc.getAddress(),
businessAddress.address,
startTime,
endTime,
invalidStartingPrice,
invalidReservePrice,
ethers.parseUnits("1000", 18),
owner.address
);
await auction.waitForDeployment();
console.warn("❌ WARNING: Contract allowed an invalid price range!");
expect.fail("Auction should not deploy when startingPrice < reservePrice");
} catch (error) {
console.log("✅ Test passed: Contract correctly rejected invalid prices");
}
});
-> getPrice() Underflow DoS Risk
getPrice()
does not properly handle underflow, leading to potential transaction reverts. This can lock the auction, preventing users from retrieving the price and placing bids.
it("should revert if getPrice() underflows", async function () {
const invalidStartingPrice = ethers.parseUnits("50", 18);
const invalidReservePrice = ethers.parseUnits("100", 18);
console.log("🚀 Deploying auction to test getPrice() behavior...");
try {
auction = await Auction.deploy(
await zeno.getAddress(),
await usdc.getAddress(),
businessAddress.address,
startTime,
endTime,
invalidStartingPrice,
invalidReservePrice,
ethers.parseUnits("1000", 18),
owner.address
);
await auction.waitForDeployment();
console.warn("❌ WARNING: Auction deployed with an invalid price range!");
expect.fail("Auction should not deploy with invalid prices");
} catch (error) {
console.log("✅ Test passed: Auction deployment correctly reverted.");
}
console.log("🚀 Calling getPrice()...");
await expect(auction.getPrice()).to.be.reverted;
console.log("✅ getPrice() correctly reverted due to underflow risk.");
});
Full Code:
import { expect } from "chai";
import hre from "hardhat";
const { ethers } = hre;
describe("Auction Contract", function () {
let Auction, auction, owner, addr1, businessAddress;
let ZENO, zeno, USDC, usdc;
let startTime, endTime;
beforeEach(async function () {
[owner, addr1, businessAddress] = await ethers.getSigners();
USDC = await ethers.getContractFactory("MockUSDC");
usdc = await USDC.deploy();
await usdc.waitForDeployment();
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();
startTime = (await ethers.provider.getBlock("latest")).timestamp + 100;
endTime = startTime + 3600;
Auction = await ethers.getContractFactory("Auction");
});
it("should deploy successfully with valid startingPrice and reservePrice", async function () {
const startingPrice = ethers.parseUnits("100", 18);
const reservePrice = ethers.parseUnits("50", 18);
const totalAllocated = ethers.parseUnits("1000", 18);
console.log("Deploying Auction contract...");
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 deployed successfully!");
expect(await auction.zeno()).to.equal(await zeno.getAddress());
expect(await auction.usdc()).to.equal(await usdc.getAddress());
});
it("If this test passes, there is a vulnerability!", async function () {
const invalidStartingPrice = ethers.parseUnits("50", 18);
const invalidReservePrice = ethers.parseUnits("100", 18);
const totalAllocated = ethers.parseUnits("1000", 18);
console.log("Testing invalid price range...");
let vulnerabilityDetected = false;
try {
auction = await Auction.deploy(
await zeno.getAddress(),
await usdc.getAddress(),
businessAddress.address,
startTime,
endTime,
invalidStartingPrice,
invalidReservePrice,
totalAllocated,
owner.address
);
await auction.waitForDeployment();
vulnerabilityDetected = true;
} catch (error) {
console.log("Test passed: Contract correctly rejected invalid prices");
}
if (vulnerabilityDetected) {
console.warn("WARNING: Vulnerability detected! The contract allowed an invalid price range.");
expect.fail("Auction should not deploy when startingPrice < reservePrice");
}
});
it("If getPrice() underflows, there is a Denial of Service risk!", async function () {
const invalidStartingPrice = ethers.parseUnits("50", 18);
const invalidReservePrice = ethers.parseUnits("100", 18);
const totalAllocated = ethers.parseUnits("1000", 18);
console.log("Deploying vulnerable contract...");
auction = await Auction.deploy(
await zeno.getAddress(),
await usdc.getAddress(),
businessAddress.address,
startTime,
endTime,
invalidStartingPrice,
invalidReservePrice,
totalAllocated,
owner.address
);
await auction.waitForDeployment();
console.warn("WARNING: Vulnerability detected! Auction deployed with an invalid price range.");
console.log("Calling getPrice()...");
await expect(auction.getPrice()).to.be.reverted;
console.log("Test passed: getPrice() correctly reverted due to underflow");
});
});
Output:
Auction Contract
Deploying Auction contract...
Auction deployed successfully!
✔ should deploy successfully with valid startingPrice and reservePrice (52ms)
Testing invalid price range...
WARNING: Vulnerability detected! The contract allowed an invalid price range.
1) If this test passes, there is a vulnerability!
Deploying vulnerable contract...
WARNING: Vulnerability detected! Auction deployed with an invalid price range.
Calling getPrice()...
2) If getPrice() underflows, there is a Denial of Service risk!
1 passing (4s)
2 failing
1) Auction Contract
If this test passes, there is a vulnerability!:
AssertionError: Auction should not deploy when startingPrice < reservePrice
2) Auction Contract
If getPrice() underflows, there is a Denial of Service risk!:
AssertionError: Expected transaction to be reverted
Tools Used
Hardhat
Recommendations
-> Enforce Proper Price Constraints in the Constructor
Modify the Auction
contract to strictly enforce the rule that startingPrice > = reservePrice
.
Fix in Solidity (Auction.sol):
require(startingPrice >= reservePrice, "Starting price must be >= reserve price");
-> Fix getPrice() to Prevent Underflows
Check that getPrice()
does not allow underflows, and ensure it returns a valid price in all cases.
Fix in Solidity (Auction.sol):
function getPrice() public view returns (uint256) {
uint256 currentTime = block.timestamp;
if (currentTime < startTime) {
return startingPrice;
}
if (currentTime >= endTime) {
return reservePrice;
}
uint256 priceDifference = startingPrice - reservePrice;
uint256 elapsedTime = currentTime - startTime;
uint256 auctionDuration = endTime - startTime;
uint256 priceReduction = (priceDifference * elapsedTime) / auctionDuration;
if (priceReduction > startingPrice) {
return reservePrice;
}
return startingPrice - priceReduction;
}