Bright Felt Bird
High
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.
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
- Auction reaches exact
endTime
- Settlement function is callable
- Bidding still possible at
endTime
- this is the code Snippet :
- Multiple transactions can be included in same block
- Auction reaches endTime
- Settlement transaction executes
- Bidder submits bid in same block
- ETH from bid becomes trapped in contract
Bidders can lose ETH by placing bids which make Funds become permanently locked in the contract.
// 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
No response