Core Contracts

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

Incorrect Ownership Configuration in Auction and ZENO Contracts: Denial of Service in Token Minting

Summary

The Auction contract enables users to purchase ZENO tokens in exchange for USDC through its buy function. This function, after processing the bid, calls the mint function in the ZENO contract to mint tokens for the buyer. However, the ZENO contract’s mint function is restricted by an onlyOwner modifier, meaning that only its owner can mint tokens. According to the intended design and NatSpec comments, the Auction contract should be the owner of the ZENO contract so that it can mint tokens during the auction. In the current deployment, however, the ZENO contract is deployed with an external owner (ZENO_OWNER), and ownership is not transferred to the Auction contract. Consequently, when the Auction contract attempts to mint tokens, the call reverts due to an ownership check, rendering the auction inoperable and causing a denial of service (DoS) for token purchases.

Vulnerability Details

How It Begins

  1. Auction::buy Functionality:
    The buy function in the Auction contract processes a purchase by deducting USDC, updating bid information, and then calling:

    zeno.mint(msg.sender, amount);

    This call is intended to mint new ZENO tokens for the buyer.

  2. ZENO::mint Function Restrictions:
    The mint function in the ZENO contract is defined as:

    function mint(address to, uint256 amount) external onlyOwner {
    if (amount == 0) {
    revert ZeroAmount();
    }
    _mint(to, amount);
    totalZENOMinted += amount;
    }

    The onlyOwner modifier restricts minting to the current owner of the ZENO contract. The NatSpec clearly indicates that only the associated auction (i.e., the Auction contract) should be allowed to mint tokens.

  3. Ownership Mismatch:
    In the deployed system, the ZENO contract is owned by ZENO_OWNER rather than the Auction contract. As a result, when the Auction contract (which is not the owner) calls zeno.mint, the call fails with an "OwnableUnauthorizedAccount" error, leading to a denial of service in the auction process.

Additional Context

  • Deployment Expectations:
    The intended functionality is that the Auction contract should control token minting either by:

    • Having the Auction contract deploy the ZENO contract so that it is the owner from inception, or

    • Transferring ownership of the ZENO contract to the Auction contract immediately after deployment.

  • Current State:
    Since neither of these steps is performed, the Auction contract cannot mint tokens, making both the Auction and ZENO contracts unusable until the ownership issue is resolved.

Proof of Concept

Scenario Walkthrough

  1. Auction Execution Attempt:

    • The Auction contract processes a purchase request via its buy function.

    • It attempts to mint ZENO tokens by calling zeno.mint(msg.sender, amount).

    • This call reverts due to the onlyOwner restriction in the ZENO contract because the Auction contract is not the owner.

  2. Test Suite Demonstration:

// 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("BUSINESS_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 = 10e6;
uint256 reservePrice = 5e6;
uint256 totalAllocated = 20e6;
uint256 zenoMaturityDate = block.timestamp + 10 days;
string zenoName = "ZENO_V1";
string zenoSymbol = "ZENOV1";
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address DEVIL = makeAddr("DEVIL");
function setUp() public {
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 is deployed with ZENO_OWNER as the 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 testDenialOfServiceDueToOwnershipMismatch() public {
vm.warp(block.timestamp + 10 days);
uint256 amountToBuy = 2;
uint256 zenoCurrentBidPrice = auction.getPrice();
uint256 cost = zenoCurrentBidPrice * amountToBuy;
console.log("zenoCurrentBidPrice: ", zenoCurrentBidPrice);
console.log("cost : ", cost);
// Mint USDC to ALICE for buying
usdcMock.mint(ALICE, cost);
vm.startPrank(ALICE);
usdcMock.approve(address(auction), cost);
// This call will revert because Auction is not the owner of the ZENO contract.
vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", address(auction)));
auction.buy(amountToBuy);
vm.stopPrank();
}
}

How to Run the Test

  1. Create a Foundry Project:

    forge init my-foundry-project
  2. Place Contracts:
    Ensure that Auction, ZENO, USDCMock, BusinessMock, and other related contracts are in the src directory.

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

  4. Run the Test:

    forge test --mt testDenialOfServiceDueToOwnershipMismatch -vv
  5. Expected Outcome:
    The test should revert with an "OwnableUnauthorizedAccount" error, confirming that the Auction contract is not authorized to mint ZENO tokens due to the ownership mismatch.

Impact

  • Auction Denial of Service:
    The Auction contract is unable to mint new ZENO tokens because it is not the owner of the ZENO contract, causing all purchase transactions to revert.

  • Operational Breakdown:
    Without the ability to mint tokens, the auction mechanism fails, preventing the sale of ZENO tokens and disrupting the protocol's revenue generation.

  • Economic Disruption:
    The inability to complete auctions can lead to market instability, loss of participant trust, and a potential decline in token value.

  • Governance and Trust Erosion:
    Persistent auction failures undermine confidence in the protocol, negatively impacting governance participation and overall stakeholder engagement.

Tools Used

  • Manual Review

  • Foundry

Recommendations

To resolve this critical vulnerability, it is essential to ensure that the Auction contract is authorized to mint ZENO tokens. We recommend the following corrective actions:

1. Deploy the ZENO Contract Inside the Auction Contract

modify the system design so that the Auction contract deploys the ZENO contract. This ensures that the Auction contract is the owner from the outset.

Recommended Diff

constructor(
- address _zenoAddress,
address _usdcAddress,
address _businessAddress,
+ uint256 _maturityDate,
+ string memory _name,
+ string memory _symbol,
uint256 _startTime,
uint256 _endTime,
uint256 _startingPrice,
uint256 _reservePrice,
uint256 _totalAllocated,
address _initialOwner
) Ownable(_initialOwner) {
- zeno = ZENO(_zenoAddress);
+ if (_usdcAddress == address(0)) revert InvalidUSDCAddress();
+ if (_startTime < block.timestamp || _endTime <= startTime) revert InvalidAuctionTime();
+ // _maturityDate can have a duration threshold also.
+ if (_maturityDate == 0 || _maturityDate <= _endTime) revert InvalidMaturityDate();
+ // other Auction specific issues are addressed explicitly in an another finding.
+ zeno = new ZENO(_usdcAddress, _maturityDate, _name, _symbol, address(this));
usdc = IUSDC(_usdcAddress);
businessAddress = _businessAddress;
state = AuctionState({
startTime: _startTime,
endTime: _endTime,
startingPrice: _startingPrice,
reservePrice: _reservePrice,
totalAllocated: _totalAllocated,
totalRemaining: _totalAllocated,
lastBidTime: 0,
lastBidder: address(0)
});
}

Benefits of This Approach

  • Ownership Integrity:
    By deploying the ZENO contract within the Auction constructor, the Auction contract immediately becomes the owner of ZENO. This resolves the critical ownership mismatch that previously prevented the Auction from minting tokens, ensuring that only authorized functions (i.e., token minting) are callable by the Auction contract.

  • Robust Input Validations:
    The updated constructor validates that the USDC address is non-zero and that the maturity date is set to a future timestamp. This prevents invalid contract initialization and avoids potential runtime failures during auction operations.

  • Improved Dependency Management:
    Internally deploying the ZENO contract makes the Auction contract self-contained and reduces dependency on external deployments. This encapsulation simplifies integration and ensures that the Auction contract always interacts with a correctly configured ZENO instance.

  • Enhanced Security:
    Ensuring the Auction contract is the owner of the ZENO token limits the risk of unauthorized minting and helps enforce the intended minting logic. This reduces the attack surface and enhances the overall security of the auction process.

  • Simplified Deployment Process:
    With the ZENO contract deployed by the Auction contract, deployment scripts become simpler. There is no need for a separate ownership transfer step, reducing the risk of human error and streamlining the deployment workflow.

  • Consistent Protocol Behavior:
    The integrated approach guarantees that all auction-related token minting, pricing, and distribution functions operate under the same ownership context, ensuring predictable and consistent protocol behavior.

  • Factories Elimination:
    Currently there're two factories one for Auctions creation and one for ZENO creation. By implementing this approach, ZENO factory logic would not be required anymore.

2. Transfer Ownership to the Auction Contract

Alternatively, After deploying the ZENO contract, immediately transfer its ownership to the Auction contract so that it can call the mint function.

// In a deployment script or via an administrative function:
vm.prank(ZENO_OWNER);
zeno.transferOwnership(address(auction));
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!