Core Contracts

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

Missing Input Validations in Auction Constructor: Invalid Initialization Leads to Auction Misbehavior and Potential Exploitation

Summary

The Auction contract is responsible for managing the auction process of ZENO tokens, including setting auction parameters such as start time, end time, starting price, reserve price, and the total allocated amount of tokens. However, during initialization in the constructor, there are no checks to ensure that critical parameters are valid. Specifically:

  • No validations are present to ensure that provided addresses (e.g., _zenoAddress, _usdcAddress, _businessAddress, _initialOwner) are non-zero.

  • No checks are implemented to ensure that numerical parameters (e.g., _startTime, _endTime, _startingPrice, _reservePrice, _totalAllocated) are non-zero and logically consistent.

  • There is no verification that the auction’s start time is before its end time.

  • The auction price logic (in getPrice) requires that the startingPrice be greater than or equal to the reservePrice, yet no enforcement exists in the constructor.

  • Additionally, totalAllocated should be at least as high as the startingPrice (since totalAllocated is assigned to totalRemaining and decreases as tokens are sold).

These oversights can result in the Auction contract being deployed with invalid or nonsensical parameters, leading to misbehavior during the auction process. This could allow the auction to start and end immediately, or worse, enable exploitation where bids are accepted under faulty pricing conditions.

Vulnerability Details

How It Begins

The Auction contract’s constructor is defined as follows:

constructor(
address _zenoAddress,
address _usdcAddress,
address _businessAddress,
uint256 _startTime,
uint256 _endTime,
uint256 _startingPrice,
uint256 _reservePrice,
uint256 _totalAllocated,
address _initialOwner
) Ownable(_initialOwner) {
// @info: missing zero address and zero value checks
zeno = ZENO(_zenoAddress);
usdc = IUSDC(_usdcAddress);
businessAddress = _businessAddress;
state = AuctionState({
startTime: _startTime,
endTime: _endTime,
startingPrice: _startingPrice,
reservePrice: _reservePrice,
totalAllocated: _totalAllocated,
totalRemaining: _totalAllocated,
lastBidTime: 0,
lastBidder: address(0)
});
}

Issues:

  1. Missing Address Checks:
    The constructor does not verify that the addresses passed (_zenoAddress, _usdcAddress, _businessAddress, _initialOwner) are not the zero address. This omission may allow the contract to be initialized with invalid addresses, leading to unexpected behavior when interacting with external contracts or sending funds.

  2. Missing Value Checks:
    There are no checks ensuring that the numerical parameters (_startTime, _endTime, _startingPrice, _reservePrice, _totalAllocated) are non-zero. For instance, if _startTime or _endTime is zero, the auction timing logic will fail.

  3. Inconsistent Time Dimensions:
    The constructor does not ensure that _startTime is earlier than _endTime. An auction with a start time after its end time would be nonsensical.

  4. Invalid Price Relationship:
    The getPrice function requires that startingPrice be greater than or equal to reservePrice:

    function getPrice() public view returns (uint256) {
    if (block.timestamp < state.startTime) return state.startingPrice;
    if (block.timestamp >= state.endTime) return state.reservePrice;
    return state.startingPrice
    - (
    (state.startingPrice - state.reservePrice) * (block.timestamp - state.startTime)
    / (state.endTime - state.startTime)
    );
    }

    If startingPrice is less than reservePrice, the arithmetic would underflow or yield incorrect values. No such check exists in the constructor.

  5. Total Allocation Constraint:
    totalAllocated should be greater than or equal to startingPrice to ensure that there are enough tokens available for sale relative to the price. This constraint is not enforced.

How the Issue Can Rekt the Protocol

  • Immediate Auction Misbehavior:
    The auction may start or end immediately if start or end times are set to zero or in the wrong order, disrupting the bidding process.

  • Faulty Price Calculation:
    If startingPrice < reservePrice, the price curve calculated by getPrice will underflow or produce nonsensical results, leading to an auction that does not reflect market conditions.

  • Invalid Contract Addresses:
    Using the zero address for critical external dependencies (like the ZENO or USDC tokens) can result in failed transfers and broken auction functionality.

  • Economic Exploitation:
    An auction initialized with incorrect parameters might be exploited by bidders who recognize the mispricing or timing flaws, leading to unfair advantages and potential financial losses for the protocol.

Proof of Concept

Scenario Walkthrough

  1. Faulty Auction Initialization:
    An admin deploys the Auction contract with:

    • Zero addresses for _zenoAddress, _usdcAddress, or _businessAddress.

    • _startTime set to 0 and _endTime set to 0.

    • _startingPrice less than _reservePrice.

    • _totalAllocated set to 0.

    This results in an auction state where:

    • state.startTime and state.endTime are both zero.

    • The price calculation in getPrice would return 0 for both pre-auction and post-auction periods.

    • No tokens are allocated for sale.

  2. Test Suite Example:

    The provided test suite instantiates an Auction with all zero values (except _initialOwner is non-zero) and verifies that the state is not checked:

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.19;
    import {Test, console} from "forge-std/Test.sol";
    import {ZENO} from "../src/zeno/ZENO.sol";
    import {Auction} from "../src/zeno/Auction.sol";
    import {USDCMock} from "./mocks/USDCMock.m.sol";
    import {BusinessMock} from "./mocks/BusinessMock.m.sol";
    import {IAuction} from "../src/interfaces/zeno/IAuction.sol";
    contract ZenoAuction is Test {
    ZENO zeno;
    Auction auction;
    USDCMock usdcMock;
    BusinessMock businessMock;
    address USDC_MOCK_OWNER = makeAddr("USDC_MOCK_OWNER");
    address BUSINESS_MOCK_OWNER = makeAddr("USDC_MOCK_OWNER");
    address ZENO_OWNER = makeAddr("ZENO_OWNER");
    address AUCTION_OWNER = makeAddr("AUCTION_OWNER");
    uint256 startTime = block.timestamp;
    uint256 endTime = block.timestamp + 30 days;
    uint256 startingPrice = 10e18;
    uint256 reservePrice = 5e18;
    uint256 totalAllocated = 20e18;
    uint256 zenoMaturityDate = block.timestamp + 10 days;
    string zenoName = "ZENO_V1";
    string zenoSymbol = "ZENOV1";
    function setUp() public {
    // ignore the setUP function...
    vm.startPrank(USDC_MOCK_OWNER);
    usdcMock = new USDCMock();
    vm.stopPrank();
    vm.startPrank(BUSINESS_MOCK_OWNER);
    businessMock = new BusinessMock();
    vm.stopPrank();
    vm.startPrank(ZENO_OWNER);
    zeno = new ZENO(address(usdcMock), zenoMaturityDate, zenoName, zenoSymbol, ZENO_OWNER);
    vm.stopPrank();
    vm.startPrank(AUCTION_OWNER);
    auction = new Auction(
    address(zeno),
    address(usdcMock),
    address(businessMock),
    startTime,
    endTime,
    startingPrice,
    reservePrice,
    totalAllocated,
    AUCTION_OWNER
    );
    vm.stopPrank();
    }
    function testZeroValuesAndAddressesInAuction() public {
    // Deploy a new Auction with zero addresses and zero values.
    Auction newAuction = new Auction(address(0), address(0), address(0), 0, 0, 0, 0, 0, address(1));
    (
    uint256 auctionEndTime,
    uint256 auctionStartTime,
    uint256 auctionStartingPrice,
    uint256 auctionReservePrice,
    uint256 auctionTotalAllocated,
    uint256 totalRemaining,
    uint256 lastBidTime,
    address lastBidder
    ) = newAuction.state();
    assertEq(address(newAuction.zeno()), address(0));
    assertEq(address(newAuction.usdc()), address(0));
    assertEq(newAuction.businessAddress(), address(0));
    assertEq(auctionEndTime, 0);
    assertEq(auctionStartTime, 0);
    assertEq(auctionStartingPrice, 0);
    assertEq(auctionReservePrice, 0);
    assertEq(auctionTotalAllocated, 0);
    assertEq(totalRemaining, 0);
    assertEq(lastBidTime, 0);
    assertEq(lastBidder, address(0));
    }
    function testAuctionGetPriceDoS() public {
    vm.startPrank(AUCTION_OWNER);
    Auction newAuction = new Auction(
    address(zeno),
    address(usdcMock),
    address(businessMock),
    startTime,
    endTime,
    startingPrice,
    startingPrice + 1e18,
    totalAllocated,
    AUCTION_OWNER
    );
    vm.stopPrank();
    vm.warp(block.timestamp + 10 days);
    // expected revert msg: Arithmetic underflow
    vm.expectRevert();
    newAuction.getPrice();
    // buy function will also get reverted becaues of getPrice function
    vm.expectRevert();
    newAuction.buy(startingPrice);
    }
    }

    The above tests confirms that the Auction contract allows deployment with invalid parameters.

How to Run the Test

  1. Create a Foundry Project:

    forge init my-foundry-project
  2. Place the Auction Contract:
    Move your Auction contract into the src directory.

  3. Create a Test Directory:
    Create a test directory adjacent to src and add the provided test file (e.g., AuctionTest.t.sol).

  4. Run the Test:

    forge test --mt testZeroValuesAndAddressesInAuction -vv
    forge test --mt testAuctionGetPriceDoS -vv
  5. Expected Output:
    The tests should pass, confirming that the Auction contract is initialized with zero addresses and zero values without reverting—thus demonstrating the absence of input validations and a DoS on retrieval of price via getPrice getter and on purchasing via buy function.

Impact

  • Auction Misbehavior:
    Auctions initialized with invalid parameters can malfunction—e.g., they might never start, end immediately, or calculate prices incorrectly—resulting in a broken auction process.

  • Economic Vulnerabilities:
    Invalid auction parameters can be exploited by malicious actors to manipulate auction outcomes, potentially draining funds or acquiring tokens at unfair prices.

  • Protocol Trust Erosion:
    Stakeholders and participants will lose confidence in the auction mechanism if auctions can be deployed with invalid parameters, undermining overall trust in the protocol.

  • Long-Term Instability:
    If left unaddressed, the lack of input validations could lead to widespread deployment issues, economic losses, and challenges in governance due to the misallocation of resources.

Tools Used

  • Manual Review

  • Foundry

Recommendations

To ensure that the Auction contract is initialized with valid parameters, implement the following checks in the constructor:

  1. Zero Address Checks:
    Ensure that none of the critical addresses (_zenoAddress, _usdcAddress, _businessAddress, _initialOwner) are the zero address.

  2. Non-Zero Value Checks:
    Validate that numerical parameters (_startTime, _endTime, _startingPrice, _reservePrice, _totalAllocated) are non-zero.

  3. Time Dimension Validation:
    Verify that _startTime < _endTime to ensure a valid auction period.

  4. Price Relationship Enforcement:
    Check that _startingPrice >= _reservePrice to avoid arithmetic underflow in price calculations.

  5. Allocation Constraint:
    Ensure that _totalAllocated >= _startingPrice to guarantee that the auction has sufficient tokens relative to the price.

Proposed Diff for Auction Constructor

constructor(
address _zenoAddress,
address _usdcAddress,
address _businessAddress,
uint256 _startTime,
uint256 _endTime,
uint256 _startingPrice,
uint256 _reservePrice,
uint256 _totalAllocated,
address _initialOwner
) Ownable(_initialOwner) {
- // Missing zero address and zero value checks
- zeno = ZENO(_zenoAddress);
- usdc = IUSDC(_usdcAddress);
- businessAddress = _businessAddress;
- state = AuctionState({
- startTime: _startTime,
- endTime: _endTime,
- startingPrice: _startingPrice,
- reservePrice: _reservePrice,
- totalAllocated: _totalAllocated,
- totalRemaining: _totalAllocated,
- lastBidTime: 0,
- lastBidder: address(0)
- });
+ // Validate addresses are non-zero
+ if (_zenoAddress == address(0)) revert InvalidAddress();
+ if (_usdcAddress == address(0)) revert InvalidAddress();
+ if (_businessAddress == address(0)) revert InvalidAddress();
+ if (_initialOwner == address(0)) revert InvalidAddress();
+
+ // Validate numerical parameters are non-zero
+ if (_startTime == 0 || _endTime == 0 || _startingPrice == 0 || _reservePrice == 0 || _totalAllocated == 0)
+ revert InvalidValue();
+
+ // Validate time dimensions
+ if (_startTime >= _endTime) revert InvalidTimePeriod();
+
+ // Validate price relationship
+ if (_startingPrice < _reservePrice) revert InvalidPriceRelation();
+
+ // Validate allocation constraint
+ if (_totalAllocated < _startingPrice) revert InvalidAllocation();
+
+ zeno = ZENO(_zenoAddress);
+ usdc = IUSDC(_usdcAddress);
+ businessAddress = _businessAddress;
+ state = AuctionState({
+ startTime: _startTime,
+ endTime: _endTime,
+ startingPrice: _startingPrice,
+ reservePrice: _reservePrice,
+ totalAllocated: _totalAllocated,
+ totalRemaining: _totalAllocated,
+ lastBidTime: 0,
+ lastBidder: address(0)
+ });
+}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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

Give us feedback!