Recently, there’s been a lot of frustration around NFT marketplaces (and, of course, aggregators like Flip) forcing users to sign one message per listing. If you want to list 10 NFTs at once, not only do you have to approve each separate NFT collection for trading on each marketplace you’d like to list on, but you may have to sign 10 different messages to get each one properly listed.
Obviously, this isn’t ideal. It wastes users’ time, and it can be scary, as most users are already wary of signing messages in the first place. This is due to how marketplaces are set up; as we’ll soon see, when listing on marketplaces like Opensea, LooksRare, and X2Y2, there’s just no way around this given how these marketplaces are currently designed.
That said, this is changing quickly, as bulk listing (one signature to list multiple items) is finally going mainstream: Blur already enables this, and Seaport (the protocol Opensea uses for NFT trading) is implementing it in version 1.2 (coming soon). LooksRare is also working on upgraded smart contracts, and I’d be surprised to see them not feature bulk listing as well.
In this article, we’ll dive into:
- Why most NFT marketplaces have historically required one signature per listing (and why we at Flip, sadly, can’t alleviate this issue until protocols support bulk listings)
- How to get around this via bulk listings
- How creating bulk listings works
- Two examples of using bulk listings in the wild, Seaport 1.2 and Blur, including a real-life example of a Bored Ape fumbled for 9 ETH
Listing on Orderbook-Based NFT Marketplaces
To understand single or bulk listing, it helps to understand how NFT orderbooks work in the first place. Orderbook-based NFT marketplaces like Opensea, LooksRare, and X2Y2 all enable “gas-less listing” by allowing users to sign listing orders with their wallets instead of sending a costly on-chain transaction.
The basic process for selling an NFT on orderbook-based exchanges is:
- You want to list an NFT for sale at some price. You go to the exchange, select your NFT and price you want, and maybe approve the NFT collection for trading if you haven’t listed it there before.
- In the background, the exchange builds you a sell order.
- You get prompted to sign this sell order via a message in your wallet.
- You sign the order. By signing this order, you’re saying, “Hey, I’m validating this sell order, anyone (or a specific user, if it’s a private sale) can come fulfill it now.” Here, “fulfill” = “buy the NFT”.
- The exchange takes your order and your signature (created when you sign the order) and stores it in their database. They expose the signed order via their interface (and API) for users to come look at and, if they want to, fulfill.
Fairly simple. All you’re doing is using your wallet to sign the order, thereby validating it, and the exchange takes care of showing that now-valid order to the world.
The Classic: One Signature Per Listing
Up until recently, most NFT marketplaces have operated using the “one listing, one signed message” method described above. You sign your order, which includes some parameters that are laid out, at least on most marketplaces, nicely in your wallet. If you’re selling an NFT, the order probably includes sections specifying the seller (you), the NFT you’re trying to sell, what token you’re trying to sell it for and how much of that token you want, and maybe a specific buyer address if it’s a private sale.
Importantly, when you sign this order in step three above, what you’re actually signing here is a hash of the order. You can think of this “order hash” as a unique representation of your order, as it comes from taking the entire order and running it through a hashing algorithm to get a unique string that represents it. For instance, this order hash may look like 0x43f74063f760d655ff96df27af0b694043402ab7a1a586b0efa7c5ef423538e4, which is a real order hash for a random, now-canceled Opensea listing that I found. The key thing to note here is that, when you sign the message, in this case your order hash, the result is a single signature. One order, one signature.
So…Why Not Just Sign More Orders At Once?
If you’re clever, you may be thinking, “Well, then why can’t aggregators like Flip just let you sign multiple order hashes at the same time, cutting down on the number of times you have to sign with your wallet?” Wait, why didn’t I think of that, I’ll be right back…
Kidding. Sadly, it’s not that easy, because most marketplaces don’t support this. Remember, when you sign a message in your wallet, the result is a single signature. When a marketplace receives your listing order, they expect to receive your signature for that specific order. If you had signed multiple orders at once, then the resulting signature wouldn’t line up with the expected, specific order.
Even if you signed multiple orders at once and the marketplace did accept it, when someone later comes to fulfill just one of the orders, the exchange’s smart contracts have to match the seller’s (your) signature with the exact order the buyer wants to fulfill. As we noted above, they wouldn’t match, since your signature matches the combination of all the orders you signed, not just the one this buyer wants to fulfill.
This is why many marketplaces do support bulk listing, but it comes with the catch that the buyer has to buy everything in the batch. The buyer couldn’t just buy part of it, because the order they’re fulfilling needs to match what was signed by the seller, and what was signed by the seller was everything contained in the batch.
Due to this lack of marketplace support, marketplaces and aggregators have had no choice but to force you to sign one order at a time. So, everyone settled on this “one listing, one signed message” method, and, for a while, it worked well enough.
A Better Idea: Bulk Listing
However, as the NFT market grew, it became apparent that users would want to list multiple NFTs at once, with each being a separately-fillable listing. Requiring users to sign one message per listing, as was always done in the past, made this a very painful, tedious user experience, and it opened up an opportunity for a marketplace to differentiate itself by offering proper, separately-fillable, single-signature bulk listings. How can this be done? Through the magic of merkle trees.
Rather than going off on a tangent about how merkle trees work, let’s learn by looking at an example. Then, once we understand how to build separately-fillable bulk listings, we’ll look at some examples of how these are now being used in practice. Off we go…
Building Bulk Listings
Say we want to list a Bored Ape for 100 ETH, a Pudgy Penguin for 8 ETH, and a Tubby Cat for 2 ETH (obviously we’d never voluntarily list a Tubby Cat, but this is just an example). Just as before, the first step is to build the listing order for each of them, and then the order hash for each of those orders. So far, so good; same process as before.
This time, however, we aren’t going to sign each order hash separately. We also know that we can’t just sign all the order hashes concatenated together. Instead, we’re going to build a merkle tree using the hashes!
For this example, let’s use short, simple, fake hashes. We’ll give the Tubby Cat listing order a hash of 5dc6c6, the Pudgy Penguin listing order a hash of 102be0, and the BAYC listing order a hash of 32e425. We can arrange the order hashes into a tree, like this:
In real life, these hashes would be longer (like 32 bytes long) and use keccak256 as their hashing algorithm, but this works for our example
To briefly explain this, af470d is the hash of 5dc6c and 102be0, and bbe693 is the hash of af470d and 32e425. All we’re doing is recursively hashing the merkle leaves (the hashes at the bottom of the tree) until we get to a merkle root (the hash at the top of the tree).
Now that we’ve built this merkle tree, the user just signs the merkle root! The exchange still stores each separate listing order, but, rather than storing a separate signature for each order, it now stores the (a) the full merkle tree, and (b) just one signature: the signed merkle root. Now, all three of these listings are valid, and users can come to the marketplace to buy them.
This is how creating bulk listings works in practice. But, as we saw earlier, classic marketplace smart contracts expect the sell order’s signature to line up with whatever order is being purchased. If the signature here is just a signed merkle root, how is the smart contract supposed to allow a user to just buy one of these listings? Some smart contract changes to support this are needed.
To see how the process of actually buying one of these listings works, let’s get a bit more technical and check out a couple examples: Seaport 1.2 and Blur.
Example 1: Seaport 1.2
Seaport 1.2, an upgrade to the Seaport 1.1 protocol that Opensea currently uses to run their orderbook exchange, is coming soon (the audit process starts on January 13th). Thankfully, one prominent new feature of Seaport 1.2 is bulk listings! To see how these work, we can consult the code directly, since it’s open source.
Let’s take our example from above, and assume that “User A” has listed a Bored Ape, a Pudgy Penguin, and a Tubby Cat for sale using a Seaport 1.2 bulk listing. We’d like to, as “User B”, purchase the fine Tubby Cat for 2 ETH, since it’s obviously a bargain. Keep in mind that User A’s bulk listing was built specifically for Seaport, so that’s where we have to execute our buy as well (for example, we couldn’t buy it using LooksRare’s smart contracts).
We place our buy order, and the Seaport smart contract begins executing and verifying things: that the order is formatted correctly, that we’ve paid the 2 ETH, and so on. At some point, it comes time to verify that the signature for the sell order is actually valid and allows us to execute a purchase of just the Tubby Cat listing rather than the entire bulk listing. After all, given recent market conditions, there’s no way we can afford a Penguin or Bored Ape. We hit this function for verifying the sell order’s signature:
Using Seaport’s reference implementation, a less-verbose version of the production code
Okay…we’ve got some unpacking to do here, which may or may not include a fair amount of glossing over of the very low-level code contained in Seaport (Seaport, as one of the most-used protocols on Ethereum, uses some nice-but-complex optimizations to reduce gas fees for its users - very kind).
Let’s skip down to line 21 (I said we’re glossing over stuff, so just go with it). Here, Seaport looks at the signature length, and, given that we’re buying a listing from a batch listing, it’s going to be different than a typical signature length (a signature for just a single-NFT listing). Why? Because, when we’re buying this order from a bulk listing, Seaport actually requires us to provide two things at once in the signature field:
- The original signature, which we know is the signed merkle root of the merkle tree containing all three listings.
- A merkle proof to show that the Tubby Cat listing is indeed a part of that merkle tree! We’re not going to go deep into exactly how this works, but long story short, a merkle proof is just a list of all hashes in the tree that need to be combined to go from the leaf to the merkle root. The key here is that it’s possible to recreate a merkle root using a merkle proof. Remember that, it’s important. Anyways, for this above example, the circled hashes would need to be provided in this proof (remember, the Tubby Cat listing is “5dc6c6”):
Obviously, these two components combine to become much longer than a signature for a single listing’s order hash, so we enter the “if“ statement on line 23. Inside, we run straight into the “_computeBulkOrderProof“ function.
This function is going to take our provided merkle proof and use it to, just as we now know is possible, calculate the merkle root! With the result of “_computeBulkOrderProof“, we’re basically saying, “Hey, this Tubby Cat listing (‘5dc6c6’) is a valid member of a merkle tree with merkle root ‘bbe693’. We calculated this merkle root using our merkle path, the circled hashes above.”
Since we provided the correct merkle proof, our merkle root that we get from “computeBulkOrderProof“ should be “bbe693”. Now we’ve got the merkle root for the merkle tree that we _claim this listing is a part of. But that claim still needs to be verified.
On line 27, we just do some formatting, which we can skip over. Then, on line 34, we enter the most important step: verifying that our merkle root (“bbe693”, which we claim this listing is a part of) MATCHES the merkle root that was signed by the lister. If they match, we know for certain that this Tubby Cat listing was a valid part of the bulk listing that User A signed, and we can complete our purchase! So how do we verify that they match?
I’m going to keep this short, because signature verification could easily be its own article, but it IS important to at least touch on. Basically, what we do here is take (a) our merkle root (“bbe693”) and (b) the sell order’s signature (which we know is a signed merkle root of the Tubby Cat, Pudgy Penguin, and Bored Ape listings), and, via some cool cryptography, determine if our merkle root matches what User A actually signed! If it does, we’ve proved that this listing was correctly signed off on by User A via a bulk listing.
Since our merkle root we calculated was “bbe693”, which is the same merkle root that was signed by User A, this check passes and we get our NFT. Nothing else left to do other than wait for the Tubby floor to hit 100 ETH.
Example 2: Blur
Blur already has bulk listings working in production, and their method for making them work is very similar to Seaport’s. Let’s take a look at these using a real-life transaction where Franklinisbored accidentally sold a Bored Ape for 9 ETH. The egregiously mispriced 9 ETH sale wasn’t due to technical issues (instead, it was a classic case of “fat-fingers” or mis-clicking), and for our purposes, it provides an engaging example of exactly how Blur’s bulk listing mechanism functions, since Franklin had listed this Ape as part of a bulk listing.
The buyer, on this lucky day, was able to fire off a transaction to buy Franklin’s Bored Ape #7303 for 9 ETH. The smart contract process begins, and eventually we hit “_validateSignature“, which needs to ensure that the bulk listing Franklin signed (more specifically, the merkle root that he signed) does indeed contain a 9 ETH listing for Bored Ape #7303. If it did not, then we obviously can’t buy this Ape for 9 ETH. Here’s that function:
The “if“ statements on lines 7-24 don’t trigger, so we’ll skip them. Let’s jump right to what we really care about: “_validateUserAuthorization“ on line 28. This function is going to validate that Franklin approved the 9 ETH listing, and here it is:
So, we enter this function looking to verify that Franklin really did sign off on this listing. The “signatureVersion“ parameter here is going to be 1, meaning that this was a Bulk listing signature, so we skip the first “if” statement on line 11 but enter the “else if” statement on line 14. The first thing we do in here, on line 16, is grab the merkle proof (called merklePath here, but same thing) from the “extraSignature” parameter. Our “extraSignature“ parameter for this transaction looks like this:
Which probably looks like nonsense to you, but running line 16’s “abi.decode(extraSignature, (bytes32))” on it gives us a bytes32 array that looks like this:
The last 5 lines above make up the merkle proof for the 9 ETH listing! For our next step, just like we did in Seaport’s “_computeBulkOrderProof“ function, we need to derive a merkle root from this 5-line merkle proof. This gets done on line 18’s “_computeRoot“ function.
With the result of “_computeRoot”, we’re walking around with a merkle root, let’s call this one “abc123”, saying “We claim that the listing order we want to purchase (Franklin’s 9 ETH Bored Ape) is a valid member of a merkle tree with merkle root ‘abc123’. We calculated this merkle root using the merkle proof provided in the ‘extraSignature’ parameter.”
Line 19 then (again, just like in Seaport) does some formatting work that we’ll skip over. Now, we’re ready for the most important step, which happens on line 22.
Line 22 is where we verify that our merkle root (“abc123”, which we claim this listing is a part of) MATCHES the merkle root that was signed by the lister. This uses the same cryptography tricks mentioned earlier to verify the match, and the check passes (well, not with “abc123”, but it passes using the real merkle root in the real transaction).
We’ve now confirmed that Franklin correctly signed this listing via a bulk listing, and his Bored Ape gets snagged for 9 ETH!
Bulk listings, as we can see, are obviously possible to implement. Sure, they require some slightly-complex smart contract changes, as well as changes to what the marketplace must store for bulk orders (the full merkle tree must be stored), but the user benefits are well worth the extra work. There’s no real tradeoffs being made here; orderbook-based NFT exchanges store orders in a centralized way to begin with, and the same thing happens here. It’s just a bit of extra data.
For now, when you’re listing on exchanges like Opensea (Seaport 1.1), LooksRare, and X2Y2, you’re still going to have to sign one message per listing. There’s just no way around it. But, soon, most exchanges should begin supporting bulk listings, removing this headache. And Flip, of course, will support this as well.