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
Rank: 83/88
Findings: 1
Award: $5.60
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Trust
Also found by: 0x1f8b, 0xdapper, HE1M, KIntern_NA, Lambda, Picodes, RaymondFam, RedOneN, TomJ, V_B, __141345__, c7e7eff, chaduke, codexploder, corerouter, cryptonue, fs0c, gz627, hihen, joestakey, ktg, ladboy233, minhtrng, rvierdiiev, simon135, skyle, slowmoses, wagmi, yixxas
5.604 USDC - $5.60
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
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()
.
Medium
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):
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
.
Manual Analysis
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