The NFTLiquidator contract provides a mechanism for users to “buy back” liquidated NFTs at a premium (110% of the debt). The buyback function computes the required payment solely as 110% of the debt, independent of the ongoing auction state. Because the auction state (stored in tokenData) is mutable and can be influenced by early bids, a malicious actor can manipulate this state through a race condition to capture an NFT at a cost far below its true market value.
Static Price Calculation:
The buyback price is computed as:
This calculation does not incorporate any market dynamics or the current highest bid.
Mutable Auction State:
At auction initiation, tokenData is set with an initial debt and no bids (i.e. highestBid is zero). A malicious bidder can then submit a minimal bid (e.g. 1 wei) to become the highest bidder without affecting the debt value.
Race Condition During Buyback:
Because the auction state remains mutable until the buyback function is successfully executed, the attacker can call buyBackNFT while the state still reflects the low or negligible bid. In effect, the attacker (or a colluding party) triggers the buyback, forcing the contract to delete the auction state and transfer the NFT at a fixed price (110% of debt) that is independent of the artificially low bid.
Arbitrage Opportunity:
If the fair market value of the NFT is significantly higher than 110% of the debt, the attacker can immediately resell the NFT for a profit, capturing a substantial arbitrage gain. The vulnerability thereby subverts the intended liquidation outcome and can result in protocol capital loss.
Liquidation Initiation:
The StabilityPool liquidates an NFT by calling liquidateNFT, initializing tokenData with a given debt (D) and starting an auction with no bid (highestBid = 0).
Malicious Minimal Bid:
The attacker places a negligible bid (e.g. 1 wei) on the auction. This bid is sufficient to record them as the highestBidder without raising the effective auction price.
Race to Buyback:
Before any honest bidder can place a competitive bid, the attacker quickly calls buyBackNFT. Since the auction state still reflects the minimal bid and the debt remains unchanged, the buyback price is calculated as 110% of D.
State Deletion and Execution:
The buyback function then:
Refunds the minimal bid (which costs almost nothing),
Deletes the auction state,
Transfers the NFT to the attacker, and
Forwards the 110% payment to the StabilityPool.
Arbitrage Profit:
The attacker now controls the NFT at an effective cost of 110% of the debt. If the NFT’s fair market value exceeds this price, the attacker can resell it for a profit—exploiting the race condition and the static buyback pricing.
Incorporate Auction Dynamics into Price Calculation:
Rather than relying solely on a fixed 110% multiplier, adjust the buyback price to consider the current highest bid. For instance, require that the buyback price be at least the maximum of (110% of debt) and (highest bid plus a minimum increment). This would prevent an attacker from setting an artificially low bid to manipulate the buyback price.
State Locking and Reentrancy Guard:
Add a reentrancy guard (using OpenZeppelin’s nonReentrant modifier) to the buyBackNFT function. This ensures that concurrent calls cannot interfere with the auction state, mitigating race conditions.
Pull-based Refunds:
Replace the push-based refund (using transfer) with a pull-based refund mechanism. Store the refundable amount in a mapping so that bidders can withdraw their funds in a separate transaction. This decouples refund logic from the buyback execution and prevents state manipulation during the race.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.