Home

Awesome

<br /> <p align="center"> <img width="250" src="https://i.imgur.com/nn7LMNV.png"/> </p> <br />

Summary

In January the Bonfida team found a critical vulnerability in the Metaplex auction smart contract which is used for most of Solana's NFT marketplaces, which exposed the funds of the Metaplex auction program to the risk of being stolen. This document is a short write-up explaining the mechanism behind the exploit and how it was fixed.

Exploit

When using the place_bid instruction, users had little constraint over what bidder_pot_token account they supplied (although it had to be owned by the auction). This means that they could supply a bidder_pot_token account which was already in use for this auction (for instance the one of another bidder). The funds for both bids could therefore be merged into the same token account. Then, the cancel_bid instruction would empty the entire token account into the supplied destination token account, thus enabling one user to steal the bid from another user (depending on the context, the funds are stolen from the bidder themselves or the auction seller).

Fix

The fix consisted of requiring the bidder pot token account to be uninitialized on bidder pot account creation (i.e. the first bid), with the added security that the token account is a PDA uniquely tied to the bidder pot account.

Detailed explanation

How does on-chain bidding even work?

You can think of Solana as a big key-value database in which you can store any data you need for your program. Elements of this database are called accounts. The Metaplex auction logic requires two Solana accounts to bid:

pub struct BidderPot {
    /// Points at actual pot that is a token account
    pub bidder_pot: Pubkey,
    /// Originating bidder account
    pub bidder_act: Pubkey,
    /// Auction metadata account
    pub auction_act: Pubkey,
    /// emptied or not
    pub emptied: bool,
}

As we can see above the bidder pot contains the public key of the bidder pot token account (bidder_pot field). The main issue came from the fact that the program was relying on the user to create the bidder pot token account before invoking the place bid instruction. Let's look at the code to see why the checks were not sufficient.

Original code

The original code can be found in the old.rs file. The instruction only performed the following checks:

These checks do not prevent an attacker from passing a bidder pot token account already in use by someone else. Let's look at the following example:

  1. Alice places a bid:

    • Alice has to create the bidder pot token account before invoking the place_bid instruction of Metaplex. She assigns the auction as the owner of this account.
    • Alice invokes the place_bid instruction, all checks pass, and the bid is placed. Her bid funds are then stored in the above bidder pot token account.
  2. Bob steals Alice's bid:

    • Bob sees Alice's bid.
    • Bob invokes the place_bid instruction and passes Alice's bidder pot token account. All the checks pass by because Alice's bidder pot token account passed the checks when she executed a place_bid, and nothing has changed to affect the results of those checks. Bob's bid funds are therefore added to Alice's bidder pot token account
    • Bob now has a valid bidder account which shares Alice's bidder pot token account. Bob invokes a cancel_bid instruction, which dumps the entirety of the bidder pot token account's balance into one of his own token accounts. Bob has therefore acquired Alice's bid in addition to recovering his own.

Using a very simple script to go through all active bidder pot accounts in the Metaplex extended ecosystem (any program using the Metaplex auction program would have been a collateral target), then performing the above, an attacker could then steal all bids.

New code

Commit 94cfc600d570cfc6b880d73ee64206a83b572ca3 fixed the vulnerability by requiring the bidder pot token to be uninitialized and a PDA.

Bounty

Metaplex bounty program can be found here, for this vulnerability Metaplex offered a bounty of 100k.

TL;DR

Whenever an account is passed by the user it should be considered unsafe and should be checked. In many cases using PDAs will reduce the surface of attack of your program as it guarantees that the initialization of the account is made by the program itself. We strongly recommend reading Neodyme's blog to learn more about common security pitfalls in Solana programming.