SIZE contest - joestakey's results

An on-chain sealed bid auction protocol.

General Information

Platform: Code4rena

Start Date: 04/11/2022

Pot Size: $42,500 USDC

Total HM: 9

Participants: 88

Period: 4 days

Judge: 0xean

Total Solo HM: 2

Id: 180

League: ETH

SIZE

Findings Distribution

Researcher Performance

Rank: 83/88

Findings: 1

Award: $5.60

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

5.604 USDC - $5.60

Labels

bug
2 (Med Risk)
satisfactory
duplicate-237

External Links

Lines of code

https://github.com/code-423n4/2022-11-size/blob/706a77e585d0852eae6ba0dca73dc73eb37f8fb6/src/SizeSealed.sol#L153 https://github.com/code-423n4/2022-11-size/blob/706a77e585d0852eae6ba0dca73dc73eb37f8fb6/src/SizeSealed.sol#L280

Vulnerability details

To finalize an auction, the seller has to call reveal(), which first reveals the private key of the auction then calls finalize(), which fills bidders in a descending price order.

The gas cost of this call is hence proportional to the number of bids - as it loops through the bids.

Malicious users can increase the gas cost by calling bid() with a baseAmount high enough that will make the check in finalize() go through without filling the order. They can then retrieve their quoteToken by calling refund().

Impact

Medium

Proof Of Concept

Take an example Auction made by Alice.

  • Bob calls bid(), specifying quoteAmount so that it is greater than a.params.minimumBidQuote. He however maliciously choses a baseAmount large enough, so that quotePerBase = FixedPointMathLib.mulDivDown(b.quoteAmount, type(uint128).max, baseAmount) is smaller than auctionParams.reserveQuotePerBase. Because baseAmount is encrypted in encryptedMessage, the call goes through and the bid is made.

  • When the auction is finished, Alice calls reveal, which triggers finalize(), filling all the bids in descending order.

  • When the loop reaches Bob's bid, this check will trigger the code to skip the remaining block of code and start the next iteration of the loop - ie the next bid.

  • Bob's b.filledBaseAmount is still 0, he can call refund() to get his quoteToken back.

The issue here is that the function call processed Bob's bid during this entire block of code (line 245 - 280), adding extra gas to the call made by Alice, despite Bob's bid being invalid.

As bidIndices.length needs to be the same as a.bids.length (check here), attackers can grieve an auction seller by making these 'ghost' bids, inflating the gas cost of a reveal() call.

The cost of the attack is two calls: one to bid(), another to refund(). Total cost is (numbers taken from the forge gas report):

  • 193477 + 23000 + 3120 + 23000 = 242,597 gas

At a gas price of 14 gwei, this mean the cost of the attack is roughly 0.003 ETH.

There is no profit motive for attackers, but it does grieve the seller.

Tools Used

Manual Analysis

Mitigation

Two amendments can be done:

1 - add extra validation in bid() to check what baseAmount is encrypted in the encryptedMessage, so that bids below reserve price cannot be made.

2 - change the continue to a break when checking the price in finalize(): this way, the code will not waste gas iterating over invalid bids.

-280           if (quotePerBase < data.reserveQuotePerBase) continue;
+280           if (quotePerBase < data.reserveQuotePerBase) break;

#0 - c4-judge

2022-11-09T19:22:03Z

0xean marked the issue as duplicate

#1 - 0xean

2022-11-09T19:22:26Z

generally a duplicate of the bid stuffing problem

#2 - c4-judge

2022-12-06T00:22:48Z

0xean marked the issue as satisfactory

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter