NFTs Part Two: Auctions, Royalties, Mints, Generative, Game Items

Day 22: Rubin's Bitcoin Advent Calendar

on December 19, 2021

This post is syndicated from rubin.io.

Welcome to day 22 of my Bitcoin Advent Calendar. You can see an index of all the posts here or subscribe at judica.org/join to get new posts in your inbox

I promised you a few things a few days ago:

  1. We’d see how to do royalties in a sale
  2. We’d see how to do a Dutch auction
  3. We’d see how to do batch mints
  4. We’d see how to make generative art

and one thing I didn’t

  1. In game items

Let’d get it done, son.

Royalties and Dutch Auction:

A Dutch Auction is a theoretically beautiful form on an auction that is great for sellers.

The way it works is that if you want to sell a piece, you start selling it at price that you think no one could buy it for, and then slowly lower the price.

For example, suppose I have a car that the blue book value is $10,000 for. I start by offerring it at $15,000k, and then drop it by $10 per second until someone buys it. After about 10 minutes, the price will be $9,000, so a pretty good deal. But before that, the price will be all prices between $9k and $15k. So if a buyer thinks the car is actually a pretty good deal at $11k, and a great deal at $10.5k, they would want to bid (assuming lots of bidders) at $11k lest someone else buy it first.

Thus Dutch Auctions are very favorable to sellers, so natually, sellers like them.

Let’s patch our earlier NFT System to support Dutch Auctions! While we’re at it let’s toss in royalties too!

First, we need to clean up a couple things about our NFT Definitions. These are sorta trivial changes – really if I had planned better I’d have included them from the get-go.

To our Minting trait we’re going to add a few fields:

  1. A key for the creator
  2. A ‘royalty’ percent (0 to disable)
/// # Trait for a Mintable NFT
#[derive(Serialize, JsonSchema, Deserialize, Clone)]
pub struct Mint_NFT_Trait_Version_0_1_0 {
    /// # Creator Key
    pub creator: bitcoin::PublicKey,
    /// # Initial Owner
    /// The key that will own this NFT
    pub owner: bitcoin::PublicKey,
    /// # Locator
    /// A piece of information that will instruct us where the NFT can be
    /// downloaded -- e.g. an IPFs Hash
    pub locator: String,
    /// # Minting Module
    /// If a specific sub-module is to be used / known -- when in doubt, should
    /// be None.
    pub minting_module: Option<SapioHostAPI<Mint_NFT_Trait_Version_0_1_0>>,
    /// how much royalty, should be paid, as a percent
    pub royalty: f64,
}

Next, we’re going to add to our Sale trait a start time (e.g. blockheight).

/// # NFT Sale Trait
/// A trait for coordinating a sale of an NFT
#[derive(Serialize, JsonSchema, Deserialize, Clone)]
pub struct NFT_Sale_Trait_Version_0_1_0 {
    /// # Owner
    /// The key that will own this NFT
    pub sell_to: bitcoin::PublicKey,
    /// # Price
    /// The price in Sats
    pub price: AmountU64,
    /// # NFT
    /// The NFT's Current Info
    pub data: Mint_NFT_Trait_Version_0_1_0,
    /// # Sale Time
    /// When the sale should be possible after
    pub sale_time: AbsHeight,
    /// # Extra Information
    /// Extra information required by this contract, if any.
    /// Must be Optional for consumer or typechecking will fail.
    /// Usually None unless you know better!
    pub extra: Option<Value>,
}

These fields could have gone into the extra data, but since it was probably a mistake to not have them from the get-go we’ll allow it this time without increasing our version numbers (nothings been released yet!).

Next, we’ll go ahead and create a new plugin module for our Dutch auction.

First we define some data that we have to have for a Dutch auction:

/// # Dutch Auction Data
/// Additional information required to initiate a dutch auction
#[derive(JsonSchema, Serialize, Deserialize)]
struct DutchAuctionData {
    /// How often should we decreate the price, in blocks
    period: u16,
    /// what price should we start at?
    start_price: AmountU64,
    /// what price should we stop at?
    min_price: AmountU64,
    /// how many price decreases should we do?
    updates: u64,
}

Then we define how to translate that into a schedule of sale prices:

impl DutchAuctionData {
    /// # Create a Schedule for Sale
    /// computes, based on a start time, the list of heights and prices
    fn create_schedule(
        &self,
        start_height: AbsHeight,
    ) -> Result<Vec<(AbsHeight, AmountU64)>, CompilationError> {
        let mut start: Amount = self.start_price.into();
        let stop: Amount = self.min_price.into();
        let inc = (start - stop) / self.updates;
        let mut h: u32 = start_height.get();
        let mut sched = vec![(start_height, self.start_price)];
        for _ in 1..self.updates {
            h += self.period as u32;
            start -= inc;
            sched.push((AbsHeight::try_from(h)?, start.into()));
        }
        Ok(sched)
    }

Finally, we want to be able to derive this data with some default choices in case a user wants to not select specific parameters. Hope you liked what we pick!

    /// derives a default auction where the price drops every 6
    /// blocks (1 time per hour), from 10x to 1x the sale price specified,
    /// spanning a month of blocks.
    fn derive_default(main: &NFT_Sale_Trait_Version_0_1_0) -> Self {
        DutchAuctionData {
            // every 6 blocks
            period: 6,
            start_price: (Amount::from(main.price) * 10u64).into(),
            min_price: main.price,
            // 144 blocks/day
            updates: 144 * 30 / 6,
        }
    }
}

With the parameters for a Dutch Auction out of the way, now we can implement the contract logic. First, the boring stuff:

#[derive(JsonSchema, Serialize, Deserialize)]
pub struct NFTDutchAuction {
    /// This data can be specified directly, or default derived from main
    extra: DutchAuctionData,
    /// The main trait data
    main: NFT_Sale_Trait_Version_0_1_0,
}

/// # Versions Trait Wrapper
#[derive(Serialize, Deserialize, JsonSchema)]
enum Versions {
    /// Use the Actual Trait API
    NFT_Sale_Trait_Version_0_1_0(NFT_Sale_Trait_Version_0_1_0),
    /// Directly Specify the Data
    Exact(DutchAuctionData, NFT_Sale_Trait_Version_0_1_0),
}
impl Contract for NFTDutchAuction {
    declare! {updatable<()>, Self::transfer}
}
fn default_coerce<T>(_: T) -> Result<(), CompilationError> {
    Ok(())
}

impl TryFrom<Versions> for NFTDutchAuction {
    type Error = CompilationError;
    fn try_from(v: Versions) -> Result<NFTDutchAuction, Self::Error> {
        Ok(match v {
            Versions::NFT_Sale_Trait_Version_0_1_0(main) => {
                // attempt to get the data from the JSON:
                // - if extra data, must deserialize
                //   - return any errors?
                // - if no extra data, derive.
                let extra = main
                    .extra
                    .clone()
                    .map(serde_json::from_value)
                    .transpose()
                    .map_err(|_| CompilationError::TerminateCompilation)?
                    .unwrap_or_else(|| DutchAuctionData::derive_default(&main));
                NFTDutchAuction { main, extra }
            }
            Versions::Exact(extra, main) => {
                if extra.start_price < extra.min_price || extra.period == 0 || extra.updates == 0{
                    // Nonsense
                    return Err(CompilationError::TerminateCompilation);
                }
                NFTDutchAuction { main, extra },
            }
        })
    }
}

REGISTER![[NFTDutchAuction, Versions], "logo.png"];

Now, the fun part! Implementing it. This is basically the same as our NFTs from the other day, but we just do sales along the schedule we generated:

impl NFTDutchAuction {
    /// # signed
    /// sales must be signed by the current owner
    #[guard]
    fn signed(self, ctx: Context) {
        Clause::Key(self.main.data.owner.clone())
    }
    /// # transfer
    /// transfer exchanges the NFT for cold hard Bitcoinz
    #[continuation(guarded_by = "[Self::signed]", web_api, coerce_args = "default_coerce")]
    fn transfer(self, base_ctx: Context, u: ()) {
        let mut ret = vec![];
        let schedule = self.extra.create_schedule(self.main.sale_time)?;
        let mut base_ctx = base_ctx;
        // the main difference is we iterate over the schedule here
        for (nth, sched) in schedule.iter().enumerate() {
            let ctx = base_ctx.derive_num(nth as u64)?;
            let amt = ctx.funds();
            // first, let's get the module that should be used to 're-mint' this NFT
            // to the new owner
            let key = self
                .main
                .data
                .minting_module
                .clone()
                .ok_or(CompilationError::TerminateCompilation)?
                .key;
            // let's make a copy of the old nft metadata..
            let mut mint_data = self.main.data.clone();
            // and change the owner to the buyer
            mint_data.owner = self.main.sell_to;
            // let's now compile a new 'mint' of the NFT
            let new_nft_contract = Ok(CreateArgs {
                context: ContextualArguments {
                    amount: ctx.funds(),
                    network: ctx.network,
                    effects: unsafe { ctx.get_effects_internal() }.as_ref().clone(),
                },
                arguments: mint_impl::Versions::Mint_NFT_Trait_Version_0_1_0(mint_data),
            })
            .and_then(serde_json::to_value)
            .map(|args| create_contract_by_key(&key, args, Amount::from_sat(0)))
            .map_err(|_| CompilationError::TerminateCompilation)?
            .ok_or(CompilationError::TerminateCompilation)?;
            // Now for the magic:
            // This is a transaction that creates at output 0 the new nft for the
            // person, and must add another input that pays sufficiently to pay the
            // prior owner an amount.

            // todo: we also could use cut-through here once implemented
            // todo: change seem problematic here? with a bit of work, we could handle it
            // cleanly if the buyer identifys an output they are spending before requesting
            // a purchase.
            let price: Amount = sched.1.into();
            ret.push(Ok(ctx
                .template()
                .add_output(amt, &new_nft_contract, None)?
                .add_amount(price)
                .add_sequence()
                // Pay Sale to Seller
                .add_output(
                    Amount::from_btc(price.as_btc() * (1.0 - self.main.data.royalty))?,
                    &self.main.data.owner,
                    None,
                )?
                // Pay Royalty to Creator
                .add_output(
                    Amount::from_btc(price.as_btc() as f64 * self.main.data.royalty)?,
                    &self.main.data.creator,
                    None,
                )?
                // only active at the set time
                .set_lock_time(sched.0.into())?
                .into()))
        }
        Ok(Box::new(ret.into_iter()))
    }
}

What’s interesting is that this contract is technically just a helper on-top of our earlier Sale definition. Granted, we really ought to have had the royalty and timelock before, but we could emulate a dutch auction by just calling the regular Sale contract n times with different locktimes and prices. So we didn’t really have to implement a standalone system for this. However, for more advanced or bespoke things (like sales that also mint an NFT comemorating the Sale itself) we’d want a bespoke module. Plus, the module makes it simple to ensure that the type of auction and rate of change in price is well understood.

If desired, the DutchAuctionData could also have different sorts of logic for different price curves (e.g. Geometric, Linear, S-Curve, Custom).

Fun!

Abstract Client Verifier Auction

After an auction closes, in order for them to be able to prove to a future party it was made correctly, they would need to run the identical Sapio code and generate all possible execution price transactions.

This is not just computationally annoying, it’s also not very “lightweight”. And it can lead to bugs like some bozo writing a contract which does not do what it says it does (and pays no royalties).

An Abstract Client Verifier Auction could be set up as a postcondition on the transactions generated by a Sale that they all be able to be re-generated by a specialized template builder that just checks basic properties like “was a royalty paid”.

We won’t go into detail on this here, but you could imagine patching Sell as follows:

/// # Sell Instructions
#[derive(Serialize, Deserialize, JsonSchema)]
pub enum Sell {
    /// # Hold
    /// Don't transfer this NFT
    Hold,
    /// # MakeSale
    /// Transfer this NFT
    MakeSale {
        /// # Which Sale Contract to use?
        /// Specify a hash/name for a contract to generate the sale with.
        which_sale: SapioHostAPI<NFT_Sale_Trait_Version_0_1_0>,
        /// # The information needed to create the sale
        sale_info: NFT_Sale_Trait_Version_0_1_0,
    },
    VerifySale {
        txn: Bitcoin::Transaction 
    }
}

and the NFT can verify that the Sale transaction was valid according to it’s choice of rule (or maybe even an artist selected Verifier module).

This might not be a huge deal / worth doing given that the Cross-Module-Call results for client-side validation are cacheable.

Batch Mints

Batch mints are important because they allow an artist to fairly and easily distribute their art. It’s really important for batch mints that the artist be able to disseminate a single Output + Contract info and sign it per collection. Even if the artist/their server has to be online to sell the work, users should be able to unambiguously see who got which art.

Conceptually speaking – no code for now – Batch Mints can be done several ways. It really depends what the artist wants:

Single Transaction

Do a single transaction whereby every minted NFT has an output.

Annuity of NFTs

Embed the mint contract into an Annuity where the successful auction of the ith NFT starts the auction of the ith+1.

Congestion Control Tree of NFTs

Generative Art:

This concept is actually… pretty simple!

If you want to automatic generative art, essentially all you need to do is give your NFT Contract (or your NFT Minting contract) some piece of state and a function to convert the metadata description of the NFT + a pointer to the transaction’s location and then you can generate a random seed for generating that piece via your generate_art function.

struct MyNFT;

impl MyNFT {
    fn generate_art(&self, b: BlockHash, offset: u64) -> String {
        /*
            Make your artz here
        */
    }
}

This can be fun for things like creating the entropy for input to e.g. a machine learning model.

Bonus: Updatable NFTs

Imagine you have a rare sword NFT for a videogame.

struct Sword {
    sharpness: u64,
    kills: u64
}

Every 10 kills you -1 sharpness, and every time you sharpen it you get +100 sharpness.

impl Sword {
    #[continuation = "[Self::signed]"]
    fn sharpen(self, ctx: Context, times: u64) {
        /*
            Pay 1000 sats to the game dev  per time sharpened
        */
    }
    #[continuation = "[Self::signed]"]
    fn register_kills(self, ctx: Context, headcount: u64) {
        /*
            update the metadata with a commitment to v
        */
    }
}

These state transitions would be verified by anyone playing the game with you, using Bitcoin as the Database.

bbbbbuttt on-chain load

Not to sweat – simply build in to the continuation logic the ability to load in an attestation chain (remember those?) of lightning invoices of you paying the game developer over LN.

The attestation chain means that cheating would be duly punishable by loss of bonds. You can also log things like ‘kills’ by publishing your game record through the attestation chain with a signature from the other player you killed.

Any time you move or sell your NFT you can checkpoint into the metadata a copy of the attestation chain “sealing” those actions. One tweak we can make to the attestation chains is to require a regular “heartbeat” attestation from players as well as a freeze attestation. This helps ensure that players buying an NFT that they have all the latest state of the item loaded and other players can check that there’s nothing missing.

galaxy brain: what if you bake into your NFT an attestation chain spec and the thing you lose for lying is the item itself? And then you can do a special HTLC-like contract whereby you have to prove you didn’t cheat for 2 weeks before getting the payment from your counterparty, else they get a refund.

Overall I hope this post has opened your mind up wildly about the possibilities with Bitcoin NFTs…

I apologize I didn’t have more code ready and the post is late, but writing these posts is hard and I’ve been focusing on the end of the series too :)


comments powered by Disqus