Skip to content

Latest commit

 

History

History
174 lines (135 loc) · 5.82 KB

025.md

File metadata and controls

174 lines (135 loc) · 5.82 KB

Bright Felt Bird

High

bidder will lose funds and ETH can be locked by bidding after auction settlement

Summary

Lack of strict timestamp validation in auction settlement will cause loss of bidder funds as bidders can place bids after auction is settled, resulting in trapped ETH in the contract.

Root Cause

In NounsAuctionHouseV3.sol, the timestamp checks allow both settlement and bidding at block.timestamp = _auction.endTime:

require(block.timestamp >= _auction.endTime, "Auction hasn't completed"); // settlement
require(block.timestamp < _auction.endTime, 'Auction expired'); // bidding

Internal pre-conditions

  1. Auction reaches exact endTime
  2. Settlement function is callable
  3. Bidding still possible at endTime
  • this is the code Snippet :

createBid _settleAuction

External pre-conditions

  1. Multiple transactions can be included in same block

Attack Path

  1. Auction reaches endTime
  2. Settlement transaction executes
  3. Bidder submits bid in same block
  4. ETH from bid becomes trapped in contract

Impact

Bidders can lose ETH by placing bids which make Funds become permanently locked in the contract.

PoC

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;

import { Test } from 'forge-std/Test.sol';
import { NounsAuctionHouseV3 } from '../../contracts/NounsAuctionHouseV3.sol';
import { StreamEscrow } from '../../contracts/StreamEscrow.sol';
import { ERC721Mock } from './helpers/ERC721Mock.sol';
import 'forge-std/console.sol';

contract NounsAuctionHouseV3Test is Test {
    NounsAuctionHouseV3 auction;
    ERC721Mock nounsToken;
    StreamEscrow escrow;
    address weth;
    uint256 duration = 24 hours;
    
    function setUp() public {
        nounsToken = new ERC721Mock();
        weth = makeAddr("weth");
        address treasury = makeAddr("treasury");
        
        escrow = new StreamEscrow(
            treasury,
            treasury,
            treasury,
            address(nounsToken),
            address(this),
            24 hours
        );
        
        auction = new NounsAuctionHouseV3(nounsToken, weth, duration);
        auction.initialize(0.1 ether, 1 hours, 10, 5000, 100, address(escrow));
        auction.unpause();
        
        vm.prank(address(auction));
        nounsToken.mint(address(auction), 0);
    }

    function testCompleteAuctionScenario() public {
        // Setup bidders
        address bidder1 = makeAddr("bidder1");
        address bidder2 = makeAddr("bidder2");
        address bidder3 = makeAddr("bidder3");
        
        vm.deal(bidder1, 5 ether);
        vm.deal(bidder2, 5 ether);
        vm.deal(bidder3, 5 ether);

        // Record initial balances
        uint256 initialBalance1 = bidder1.balance;
        uint256 initialBalance2 = bidder2.balance;
        uint256 initialBalance3 = bidder3.balance;
        
        console.log("Initial balances:");
        console.log("Bidder1:", initialBalance1);
        console.log("Bidder2:", initialBalance2);
        console.log("Bidder3:", initialBalance3);

        // First bid
        vm.prank(bidder1);
        auction.createBid{value: 1 ether}(0);
        console.log("After first bid, bidder1 balance:", bidder1.balance);

        // Second bid
        vm.prank(bidder2);
        auction.createBid{value: 2 ether}(0);
        console.log("After second bid, bidder2 balance:", bidder2.balance);
        console.log("Bidder1 refunded balance:", bidder1.balance);

        // Advance to endTime
        uint40 endTime = auction.auction().endTime;
        vm.warp(endTime);

        // Settle auction
        auction.settleAuction();
        console.log("Auction settled, winning bidder:", auction.auction().bidder);

        // Third bidder attempts to bid after settlement
        vm.prank(bidder3);
        auction.createBid{value: 3 ether}(0);

        // Final state verification
        console.log("\nFinal state:");
        console.log("Bidder1 final balance:", bidder1.balance);
        console.log("Bidder2 final balance:", bidder2.balance);
        console.log("Bidder3 final balance:", bidder3.balance);
        console.log("Contract balance (locked funds):", address(auction).balance);
        console.log("Noun token owner:", nounsToken.ownerOf(0));

        // Assertions
        assertEq(bidder1.balance, initialBalance1, "Bidder1 should be fully refunded");
        assertEq(bidder2.balance, initialBalance2 - 2 ether, "Bidder2 should have paid 2 ether");
        assertEq(bidder3.balance, initialBalance3 - 3 ether, "Bidder3's funds should be trapped");
        assertEq(address(auction).balance, 3 ether, "Contract should hold trapped funds");
        assertEq(nounsToken.ownerOf(0), bidder2, "Bidder2 should own the Noun");
    }
}

run the test: forge test --match-test testCompleteAuctionScenario -vvv Output:

Running 1 test for NounsAuctionHouseV3Test
[PASS] testCompleteAuctionScenario()
Logs:
  Initial balances:
  Bidder1: 5000000000000000000
  Bidder2: 5000000000000000000
  Bidder3: 5000000000000000000
  After first bid, bidder1 balance: 4000000000000000000
  After second bid, bidder2 balance: 3000000000000000000
  Bidder1 refunded balance: 5000000000000000000
  Auction settled, winning bidder: 0x2000000000000000000000000000000000000000

  Final state:
  Bidder1 final balance: 5000000000000000000
  Bidder2 final balance: 3000000000000000000
  Bidder3 final balance: 2000000000000000000
  Contract balance (locked funds): 3000000000000000000
  Noun token owner: 0x2000000000000000000000000000000000000000

Test result: ok. 1 passed; 0 failed; 0 skipped

Mitigation

No response