Core Contracts

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

Auction will not work due to the not transfering Ownership to Auction contract.

Summary

The Protocol Implements functionality in the Auction contract, any user with usdc holding will be use buy function to enter the auction with given period and time but it will revert the If the ZENO contract initialOwner not transfer the ownership to the perticular Auction then user can't buy the zeno.

Vulnerability Details

The Issue Arises when Auction ready and set up for the users to enter the Auction and buy zeno for USDC token. and that time user will send amount for zeno token, then we can see that auction contract will mint the zeno token to the current user. that time if the ZENO contract, check for the current msg.sender SLOC#34 is the initialOwner but ZENO owner forgot to transfer the ownership to the perticular auction, auction startTime user cannot mint the zeno token from Auction Contract.

// File: contracts/zeno/Auction.sol
zeno.mint(msg.sender, amount); // <@ call ZENO for mint

However this zeno contract not transfering to the ownership to auction will leading to the user can't buy zeno from running auction.

// File: contracts/zeno/ZENO.sol
/**
CAN BE CALLED ONLY BY ASSOCIATED AUCTION CONTRACT (THE OWNER)
*/
function mint(address to, uint256 amount) external onlyOwner { // <@ POC here
if (amount == 0) {
revert ZeroAmount();
}
_mint(to, amount);
totalZENOMinted += amount;
}

Impact

Impact: The Auctoins buy zeno functionality is non-operational, but theres no risk of the funds loss.

Likelihood: The Auctions buy function fails to excute as intended if the owner of the zeno is not the auction contract address.

Steps to Reproduce

Step 1: ZENO FACTORY Initial Owner Create the new ZENO token from the ZENO Factory.

Step 2: Auction FACTORY initial owner create the new Auction from the Auction FActory.

Step 3: Now Auction Starts and Users who wants buy zeno got to the current running auction and execute the Auction::buy.

/**
Bid on the ZENO auction
User will able to buy ZENO tokens in exchange for USDC
*/
function buy(uint256 amount) external whenActive {
require(amount <= state.totalRemaining, "Not enough ZENO remaining");
uint256 price = getPrice();
uint256 cost = price * amount;
require(usdc.transferFrom(msg.sender, businessAddress, cost), "Transfer failed");
bidAmounts[msg.sender] += amount;
state.totalRemaining -= amount;
state.lastBidTime = block.timestamp;
state.lastBidder = msg.sender;
zeno.mint(msg.sender, amount); // <@ call zeno for mint
emit ZENOPurchased(msg.sender, amount, price);
}

Step 4: Users can't buy ZENO token from auction due to ZENO contract function ZENO::mint has the crucial check onlyOwner which is deployer of the ZENO Token contract not the Auction Contract.

/**
CAN BE CALLED ONLY BY ASSOCIATED AUCTION CONTRACT (THE OWNER)
*/
function mint(address to, uint256 amount) external onlyOwner { // <@ POC Here
if (amount == 0) {
revert ZeroAmount();
}
_mint(to, amount);
totalZENOMinted += amount;
}

Step 5: Users Can't buy Zeno Due to constantly Failing till the ZENO Factory initial Owner not transfering Ownership to the Auction contract.

Foundry POC

Step 1: Clone the github Repo from Codehawks github link.

Step 2: Navigate to the repo root directory. cd 2025-02-raac

Step 3: Integrate foundry in this project link with this command npm i npm i --save-dev @nomicfoundation/hardhat-foundry.

Step 4: Add require("@nomicfoundation/hardhat-foundry"); to the top of your hardhat.config.cjs file.

Step 5: Comment Out Your Network Section in the hardhat.config.cjs file.

networks: {
// hardhat: {
// mining: {
// auto: true,
// interval: 0
// },
// forking: {
// url: process.env.BASE_RPC_URL,
// },
// chainId: 8453,
// gasPrice: 50000000000, // 50 gwei
// allowBlocksWithSameTimestamp: true
// },
// devnet: {
// url: "http://0.0.0.0:8545",
// chainId: 8453,
// },
// ...deploymentNetworks
},

Step 6: Navigate to the test/libraries/ReserveLibraryMock.sol replace this file name with this test/libraries/ReserveLibraryMock.sol.bak so at the time of compilation of contract tester dont get errors.

Step 7: Now Inside the test Directory Create Test File NoBuyTransferFrom.t.sol. and copy paste the following POC below for running test.

Step 8: Test POC with this command forge test --match-path test/NoBuyDuetoNotTransferingOwnership.t.sol -vvvv

POC

Foundry Solidity Test

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;
import {Test, console} from "forge-std/Test.sol";
import {ZENO} from "../contracts/zeno/ZENO.sol";
import {ZENOFactory} from "../contracts/zeno/ZENOFactory.sol";
import {Auction} from "../contracts/zeno/Auction.sol";
import {AuctionFactory} from "../contracts/zeno/AuctionFactory.sol";
import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract USDCMOCK is ERC20 {
constructor() ERC20("USDC", "USDC") {}
function mint(address recipient, uint256 amount) public {
_mint(recipient, amount);
}
function decimals() public view override returns (uint8) {
return 6;
}
}
contract NoBuyDuetoNotTransferingOwnership is Test {
AuctionFactory auctionFactory;
ZENOFactory zenoFactory;
Auction auction;
ZENO zeno;
address ALICE = makeAddr("alice");
address BOB = makeAddr("bob");
address OWNER_ZENO_FACTORY = makeAddr("owner_zeno");
address OWNER_AUCTION_FACTORY = makeAddr("owner_auction");
address BUISNESS_ADDRESS = makeAddr("business_address");
uint256 MATURITY_DATE = 3 days;
uint256 AUCTIONS_START_TIME = 1 hours;
uint256 AUCTION_END_TIME = AUCTIONS_START_TIME + 1 days;
uint256 startingPrice = 100;
uint256 reservePrice = 10;
uint256 TOTAL_ZENO_ALLCATED = 10;
USDCMOCK USDC_MOCK;
function setUp() public {
USDC_MOCK = new USDCMOCK(); // deploy mock usdc
// DEPLOY ZENO FACTORY
zenoFactory = new ZENOFactory(OWNER_ZENO_FACTORY); // deploy zeno factory
// DEPLOY AUCTION FACTORY
auctionFactory = new AuctionFactory(OWNER_AUCTION_FACTORY); // deploy auction factory
USDC_MOCK.mint(ALICE, 100000); // mint usdc for alice
}
function testFail_NooneCanBuyFromZenoAuction() public {
// NEW ZENO
vm.startPrank(OWNER_ZENO_FACTORY); // startPrank as the owner of the zeno factory
zenoFactory.createZENOContract(address(USDC_MOCK), MATURITY_DATE); // create new zeno contract
vm.stopPrank(); // stopPrank
ZENO.ZENODetails memory zeno_details = zenoFactory.getZENODetails(0); // get the zeno address
console.log("Zeno Address: ", address(zeno_details.zenoAddress)); // log the zeno address
// NEW AUCTION
vm.startPrank(OWNER_AUCTION_FACTORY); // startPrank as the owner of the auction factory
auctionFactory.createAuction(
address(zeno_details.zenoAddress),
address(USDC_MOCK),
BUISNESS_ADDRESS,
AUCTIONS_START_TIME,
AUCTION_END_TIME,
startingPrice,
reservePrice,
TOTAL_ZENO_ALLCATED
); // create auction
vm.stopPrank(); // stopPrank
Auction.AuctionDetails memory auction_details = auctionFactory.getAuctionDetails(0); // get the auction contract address
console.log("Auction Contract Address: ", address(auction_details.auctionAddress)); // log the auction contract address
// Users can't buy zeno from the current auction
vm.startPrank(ALICE); // startPrank as Alice
console.log("USDC BALANCE of Alice: ", USDC_MOCK.balanceOf(address(this))); // display balance of usdc for alice
USDC_MOCK.approve(address(auction_details.auctionAddress), 500); // approve 500 to auction contract
console.log("Approved balance: ", USDC_MOCK.allowance(ALICE, address(auction_details.auctionAddress))); // display allowance from alice to auction
vm.warp(AUCTIONS_START_TIME + 20); // set time stamp
Auction(address(auction_details.auctionAddress)).buy(1); // buy zeno will revert
vm.stopPrank(); // stopPrank
}
}

POC Result

Ran 1 test for test/NoBuyDuetoNotTransferingOwnership.t.sol:NoBuyDuetoNotTransferingOwnership
[PASS] testFail_NooneCanBuyFromZenoAuction() (gas: 1952146)
Logs:
Zeno Address: 0xffD4505B3452Dc22f8473616d50503bA9E1710Ac
Auction Contract Address: 0x4f81992FCe2E1846dD528eC0102e6eE1f61ed3e2
USDC BALANCE of Alice: 0
Approved balance: 500
Traces:
[1952146] NoBuyDuetoNotTransferingOwnership::testFail_NooneCanBuyFromZenoAuction()
├─ [0] VM::startPrank(owner_zeno: [0xA16BF0791a22419D925CCDd34BEcac3b887AFcDc])
│ └─ ← [Return]
├─ [982477] ZENOFactory::createZENOContract(USDCMOCK: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], 259200 [2.592e5])
│ ├─ [876996] → new ZENO@0xffD4505B3452Dc22f8473616d50503bA9E1710Ac
│ │ ├─ emit OwnershipTransferred(previousOwner: 0x0000000000000000000000000000000000000000, newOwner: ZENOFactory: [0x2e234DAe75C793f67A35089C9d99245E1C58470b])
│ │ └─ ← [Return] 3919 bytes of code
│ ├─ emit ZENOCreated(zenoAddress: ZENO: [0xffD4505B3452Dc22f8473616d50503bA9E1710Ac], maturityDate: 259200 [2.592e5])
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [5900] ZENOFactory::getZENODetails(0) [staticcall]
│ ├─ [2791] ZENO::getDetails() [staticcall]
│ │ └─ ← [Return] ZENODetails({ zenoAddress: 0xffD4505B3452Dc22f8473616d50503bA9E1710Ac, maturityDate: 259200 [2.592e5], name: "ZENO Bond 1", symbol: "ZENO1" })
│ └─ ← [Return] ZENODetails({ zenoAddress: 0xffD4505B3452Dc22f8473616d50503bA9E1710Ac, maturityDate: 259200 [2.592e5], name: "ZENO Bond 1", symbol: "ZENO1" })
├─ [0] console::log("Zeno Address: ", ZENO: [0xffD4505B3452Dc22f8473616d50503bA9E1710Ac]) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::startPrank(owner_auction: [0xc09882dBdE929E7F0C6B5B28b9F9fa35504c9e62])
│ └─ ← [Return]
├─ [779576] AuctionFactory::createAuction(ZENO: [0xffD4505B3452Dc22f8473616d50503bA9E1710Ac], USDCMOCK: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], business_address: [0x6d5AE8abD3Fb993A9e8607BDbA3cc687f9A70443], 3600, 90000 [9e4], 100, 10, 10)
│ ├─ [698468] → new Auction@0x4f81992FCe2E1846dD528eC0102e6eE1f61ed3e2
│ │ ├─ emit OwnershipTransferred(previousOwner: 0x0000000000000000000000000000000000000000, newOwner: AuctionFactory: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a])
│ │ └─ ← [Return] 2680 bytes of code
│ ├─ emit AuctionCreated(auctionAddress: Auction: [0x4f81992FCe2E1846dD528eC0102e6eE1f61ed3e2])
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [7485] AuctionFactory::getAuctionDetails(0) [staticcall]
│ ├─ [4692] Auction::getDetails() [staticcall]
│ │ └─ ← [Return] AuctionDetails({ auctionAddress: 0x4f81992FCe2E1846dD528eC0102e6eE1f61ed3e2, zenoAddress: 0xffD4505B3452Dc22f8473616d50503bA9E1710Ac, businessAddress: 0x6d5AE8abD3Fb993A9e8607BDbA3cc687f9A70443, auctionEndTime: 90000 [9e4], startingPrice: 100, reservePrice: 10, auctionStartTime: 3600, totalZENOAllocated: 10, totalZENORemaining: 10, lastBidTime: 0, lastBidder: 0x0000000000000000000000000000000000000000, lastBidAmount: 0, price: 100 })
│ └─ ← [Return] AuctionDetails({ auctionAddress: 0x4f81992FCe2E1846dD528eC0102e6eE1f61ed3e2, zenoAddress: 0xffD4505B3452Dc22f8473616d50503bA9E1710Ac, businessAddress: 0x6d5AE8abD3Fb993A9e8607BDbA3cc687f9A70443, auctionEndTime: 90000 [9e4], startingPrice: 100, reservePrice: 10, auctionStartTime: 3600, totalZENOAllocated: 10, totalZENORemaining: 10, lastBidTime: 0, lastBidder: 0x0000000000000000000000000000000000000000, lastBidAmount: 0, price: 100 })
├─ [0] console::log("Auction Contract Address: ", Auction: [0x4f81992FCe2E1846dD528eC0102e6eE1f61ed3e2]) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::startPrank(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6])
│ └─ ← [Return]
├─ [2562] USDCMOCK::balanceOf(NoBuyDuetoNotTransferingOwnership: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("USDC BALANCE of Alice: ", 0) [staticcall]
│ └─ ← [Stop]
├─ [24739] USDCMOCK::approve(Auction: [0x4f81992FCe2E1846dD528eC0102e6eE1f61ed3e2], 500)
│ ├─ emit Approval(owner: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], spender: Auction: [0x4f81992FCe2E1846dD528eC0102e6eE1f61ed3e2], value: 500)
│ └─ ← [Return] true
├─ [814] USDCMOCK::allowance(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], Auction: [0x4f81992FCe2E1846dD528eC0102e6eE1f61ed3e2]) [staticcall]
│ └─ ← [Return] 500
├─ [0] console::log("Approved balance: ", 500) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::warp(3620)
│ └─ ← [Return]
├─ [97187] Auction::buy(1)
│ ├─ [30866] USDCMOCK::transferFrom(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], business_address: [0x6d5AE8abD3Fb993A9e8607BDbA3cc687f9A70443], 100)
│ │ ├─ emit Transfer(from: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], to: business_address: [0x6d5AE8abD3Fb993A9e8607BDbA3cc687f9A70443], value: 100)
│ │ └─ ← [Return] true
│ ├─ [615] ZENO::mint(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], 1)
│ │ └─ ← [Revert] OwnableUnauthorizedAccount(0x4f81992FCe2E1846dD528eC0102e6eE1f61ed3e2)
│ └─ ← [Revert] OwnableUnauthorizedAccount(0x4f81992FCe2E1846dD528eC0102e6eE1f61ed3e2)
└─ ← [Revert] OwnableUnauthorizedAccount(0x4f81992FCe2E1846dD528eC0102e6eE1f61ed3e2)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.44ms (1.28ms CPU time)
Ran 1 test suite in 1.74s (3.44ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Tools Used

  • Manual Review

  • Foundry

Recommended Mitigation

It is advisable to check current ownership of the zeno contract matches the current auction contract address.

// File: contracts/zeno/Auction.sol
.
.
.
+ modifier checkZENOOwnerShip() {
+ require(address(this) == ZENO(zeno).owner(), "Auction Not the Owner");
+ _;
+}
.
.
.
- function buy(uint256 amount) external whenActive {
+ function buy(uint256 amount) external whenActive checkZENOOwnerShip {
require(amount <= state.totalRemaining, "Not enough ZENO remaining");
uint256 price = getPrice();
uint256 cost = price * amount;
require(usdc.transferFrom(msg.sender, businessAddress, cost), "Transfer failed");
bidAmounts[msg.sender] += amount;
state.totalRemaining -= amount;
state.lastBidTime = block.timestamp;
state.lastBidder = msg.sender;
zeno.mint(msg.sender, amount);
emit ZENOPurchased(msg.sender, amount, price);
}
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!