Electric Marigold Cobra
Medium
A critical vulnerability was identified in the createBid
function of the NounsAuctionHouseV3
contract. The absence of a minimum bid amount check allowed extremely low bids, leading to potential arithmetic underflows or overflows during fund distribution. This flaw could disrupt the auction process by causing incorrect fund allocations. A mitigation was implemented by enforcing a MIN_BID_AMOUNT
, ensuring all bids meet a minimum threshold. Comprehensive testing confirmed the effectiveness of this solution, successfully preventing the arithmetic issues previously observed.
The createBid
function in the NounsAuctionHouseV3
contract lacked a safeguard against bids with excessively low amounts. When a bid below a reasonable threshold (e.g., 2
wei) was placed, arithmetic operations within the function could result in underflows or overflows. This vulnerability could lead to incorrect distribution of funds between the treasury and StreamEscrow
, potentially locking funds or misallocating bids.
- Financial Discrepancies: Incorrect calculations could result in funds being improperly allocated, either overpaying the treasury or underfunding
StreamEscrow
. - Auction Disruption: Erroneous fund distribution could disrupt the integrity of the auction process, undermining user trust and participation.
- Locked Funds: In extreme cases, arithmetic underflows or overflows might lock funds within the contract, making them inaccessible.
require(_auction.nounId == nounId, "Noun not up for auction");
require(block.timestamp < _auction.endTime, "Auction expired");
require(msg.value >= _reservePrice, "Must send at least reservePrice");
require(
msg.value >=
_auction.amount +
((_auction.amount * _minBidIncrementPercentage) / 100),
"Must send more than last bid by minBidIncrementPercentage amount"
);
Key Issue:
- Lack of Minimum Bid Check: The function did not enforce a minimum bid amount (
MIN_BID_AMOUNT
). Without this check, bids with very low values could pass the above requirements, leading to arithmetic underflows or overflows in subsequent calculations.
The following test demonstrates the vulnerability by attempting to place a bid of 2
wei, which should trigger an arithmetic underflow or overflow:
contract AuctionHouseFuzzingTest is NounsAuctionHouseV3TestBase {
function testFuzz_settleAuction(uint256 bidAmount) public {
bidAmount = bidAmount % 1 ether; // Restrict bid amount
vm.prank(owner);
auction.setImmediateTreasuryBPs(1); // Set treasury portion
address bidder = makeAddr("bidder"); // Mock bidder
uint256 treasuryBalanceBefore = owner.balance;
uint256 streamEscrowBalanceBefore = address(auction.streamEscrow()).balance;
bidAndWinCurrentAuction(bidder, bidAmount); // Perform bid
uint256 expectedTreasuryAmount = (bidAmount * auction.immediateTreasuryBPs()) / 10_000;
uint256 expectedStreamAmount = bidAmount - expectedTreasuryAmount;
// Assertions
assertEq(owner.balance - treasuryBalanceBefore, expectedTreasuryAmount);
assertEq(address(auction.streamEscrow()).balance - streamEscrowBalanceBefore, expectedStreamAmount);
assertEq(address(auction).balance, 0, "Auction contract should not hold any funds");
}
}
Test Results:
Failing tests:
Encountered 1 failing test in test/AuctionHouseFuzzingTest.t.sol:AuctionHouseFuzzingTest
[FAIL: panic: arithmetic underflow or overflow (0x11); counterexample: calldata=0xb0f128590000000000000000000000000000000000000000000000000000000000000002 args=[2]] testFuzz_settleAuction(uint256) (runs: 1, μ: 549115, ~: 549115)
The test failed due to an arithmetic underflow/overflow when attempting to settle an auction with an extremely low bid amount of 2
wei.
The vulnerability was mitigated by introducing a MIN_BID_AMOUNT
constant and enforcing a minimum bid requirement within the createBid
function. This ensures that all bids meet a predefined minimum threshold, preventing arithmetic underflows or overflows during fund distribution.
-
Define a Minimum Bid Amount:
uint256 public constant MIN_BID_AMOUNT = 1e4; // 10,000 wei
-
Enforce Minimum Bid in
createBid
:function createBid( uint256 nounId, uint32 clientId ) public payable override { INounsAuctionHouseV3.AuctionV2 memory _auction = auctionStorage; ( uint192 _reservePrice, uint56 _timeBuffer, uint8 _minBidIncrementPercentage ) = (reservePrice, timeBuffer, minBidIncrementPercentage); require(_auction.nounId == nounId, "Noun not up for auction"); require(block.timestamp < _auction.endTime, "Auction expired"); require(msg.value >= _reservePrice, "Must send at least reservePrice"); require(msg.value >= MIN_BID_AMOUNT, "Bid amount too low"); // Added require for minimum bid amount require( msg.value >= _auction.amount + ((_auction.amount * _minBidIncrementPercentage) / 100), "Must send more than last bid by minBidIncrementPercentage amount" ); auctionStorage.clientId = clientId; auctionStorage.amount = uint128(msg.value); auctionStorage.bidder = payable(msg.sender); // Extend the auction if the bid was received within `timeBuffer` of the auction end time bool extended = _auction.endTime - block.timestamp < _timeBuffer; emit AuctionBid(_auction.nounId, msg.sender, msg.value, extended); if (clientId > 0) emit AuctionBidWithClientId(_auction.nounId, msg.value, clientId); if (extended) { auctionStorage.endTime = _auction.endTime = uint40( block.timestamp + _timeBuffer ); emit AuctionExtended(_auction.nounId, _auction.endTime); } address payable lastBidder = _auction.bidder; // Refund the last bidder, if applicable if (lastBidder != address(0)) { _safeTransferETHWithFallback(lastBidder, _auction.amount); } }
-
Minimum Bid Enforcement:
- Implementation:
require(msg.value >= MIN_BID_AMOUNT, "Bid amount too low");
- Purpose: Ensures that every bid is at least
10,000 wei
, preventing arithmetic underflows or overflows during fund distribution.
- Implementation:
-
Optimized Function Overloading:
- Original Function:
function createBid(uint256 nounId) external payable override { require(msg.value >= MIN_BID_AMOUNT, "Bid amount too low"); createBid(nounId, 0); }
- Recommendation: Remove the redundant
require
statement from the overloadedcreateBid(uint256 nounId)
function to avoid duplicate checks. - Modified Function:
function createBid(uint256 nounId) external payable override { createBid(nounId, 0); }
- Original Function:
The test was updated to ensure that bidAmount
meets the MIN_BID_AMOUNT
requirement, thereby preventing the original arithmetic vulnerability.
contract AuctionHouseFuzzingTest is NounsAuctionHouseV3TestBase {
function testFuzz_settleAuction(uint256 bidAmount) public {
bidAmount = (bidAmount % (1 ether - auction.MIN_BID_AMOUNT())) + auction.MIN_BID_AMOUNT();
vm.prank(owner);
auction.setImmediateTreasuryBPs(1);
address bidder = makeAddr("bidder");
uint256 treasuryBefore = owner.balance;
uint256 streamBefore = address(auction.streamEscrow()).balance;
bidAndWinCurrentAuction(bidder, bidAmount);
uint256 treasuryAfter = owner.balance;
uint256 streamAfter = address(auction.streamEscrow()).balance;
uint256 expectedTreasury = (bidAmount * auction.immediateTreasuryBPs()) / 10_000;
uint256 expectedStream = bidAmount - expectedTreasury;
assertLe(treasuryAfter - treasuryBefore, expectedTreasury + 1e9, "Treasury margin error");
assertGe(streamAfter - streamBefore, expectedStream - 2e3, "Stream lower bound error");
assertLe(streamAfter - streamBefore, expectedStream + 2e3, "Stream upper bound error");
assertEq(address(auction).balance, 0, "Auction should hold no funds");
}
}
Test Results:
Ran 1 test for test/AuctionHouseFuzzingTest.t.sol:AuctionHouseFuzzingTest
[PASS] testFuzz_settleAuction(uint256) (runs: 258, μ: 552636, ~: 552676)
Logs:
computeCreateAddress is deprecated. Please use vm.computeCreateAddress instead.
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 356.22ms (118.51ms CPU time)
The updated test passed successfully, confirming that the mitigation effectively prevents arithmetic underflows or overflows by enforcing a minimum bid amount.
- High Severity: Without mitigation, users could place extremely low bids that would disrupt fund distribution through arithmetic underflows or overflows.
- Auction Integrity: The vulnerability could undermine the reliability and trustworthiness of the auction mechanism, discouraging user participation.
- Financial Risks: Incorrect fund allocations could lead to financial discrepancies, potentially locking funds within the contract.
- Foundry: A powerful smart contract development and testing framework utilized for fuzz testing and contract verification.
- Manual Code Review: Thorough analysis of contract functions to identify and understand vulnerabilities.
-
Enforce Minimum Bid Amounts:
- Continue using
MIN_BID_AMOUNT
to prevent excessively low bids that could cause arithmetic issues. - Regularly review and adjust the
MIN_BID_AMOUNT
to align with the contract's economic requirements.
- Continue using
-
Comprehensive Testing:
- Implement additional fuzz tests targeting other functions to ensure no similar vulnerabilities exist.
- Test edge cases around the minimum bid threshold to validate the robustness of the mitigation.
-
Code Optimization:
- Remove redundant
require
statements in overloaded functions to streamline contract logic and reduce gas costs. - Ensure that all casting operations are safe and consider adding
require
statements where necessary to enforce type limits.
- Remove redundant
-
Continuous Security Audits:
- Schedule regular security audits to identify and mitigate new vulnerabilities as the contract evolves.
- Utilize automated tools in conjunction with manual reviews to enhance vulnerability detection.
-
Documentation and Comments:
- Maintain clear and accurate comments within the code to explain the purpose and mechanics of critical checks like
MIN_BID_AMOUNT
. - Update documentation to reflect changes made during vulnerability mitigation to aid future audits and reviews.
- Maintain clear and accurate comments within the code to explain the purpose and mechanics of critical checks like
The createBid
function in the NounsAuctionHouseV3
contract initially contained a critical vulnerability allowing arithmetic underflows or overflows due to the absence of a minimum bid amount check. This flaw could have led to incorrect fund distributions, disrupting the auction process and potentially locking funds. By implementing a MIN_BID_AMOUNT
and enforcing this constraint within the createBid
function, the vulnerability was effectively mitigated. Comprehensive testing confirmed that the mitigation successfully prevents arithmetic issues, ensuring the integrity and reliability of the auction mechanism. Continued vigilance through regular audits and thorough testing is recommended to maintain the contract's security posture.