Contents

Candle Auctions on ink!: DIY NFT Sales

The rising NFT market is approaching $12bln in 2021. Despite been touted as a democratizing force for art, relevant researches show that it is still quite far from finding sustainable pricing models. It is exposed to market manipulation and it has an increased entry threshold. To beat all of these shortcomings, let’s Do It Yourself by designing a fair-price Candle Auction smart contract (Github link) to be run on a Polkadot parachain and see how it plays out.

Rationale

New markets of crypto economy, such as NFTs, have combined properties of rapid growing and inefficiency in pricing (Dowling, M 2021b). Not the least source of such an inefficiency lays down in the mechanics of standard auction model used for these new assets sales, which is proven to be exposed to price manipulations via shill bidding and front-running.

Recent studies show that a properly designed dynamic auction with a random closing rule, such as Candle Auction, can reliably mitigate these problems (cf. Häfner and Stewart, 2021).

Last but not the least, increased gas price on Ethereum makes the collectibles market tend to centralize, with top 10% traders perform 85% of transactions. There is a trend to move NFT issuance and trading away from costly Ethereum to other blockchains. E.g. Opensea expands to Polygon, and Dapper Labs is experimenting with its Flow blockchain. I think Polkadot parachains would be among best alternatives here, with the Candle Auction contract we design below.

Auctions of this fashion have been utilized in Polkadot parachain auctions for a while. We look through its codebase as a reference implementation, to build a smart contract which allows to run such an auction on various subjects, including NFTs, domain names, and many others to come, thankfully to its pluggable reward logic feature.

Design

Reinventing the Candle

We can’t fire a candle on a blockchain, hence we implement such a mechanic the following way. Bids are made in a sequence of rounds during a pre-defined set of blocks, i.e. as blocks between $start$ and $end$ block are passing by. After the $end$ block has been added to chain, the decisive round is to be selected randomly in retrospective manner among all bidding rounds. The bidder whose bid was the highest to the end of the decisive round wins. This emulates a candle going out right after that round.

These rounds could be slots of blocks of any pre-defined same length. For the sake of simplicity, we’ll use time slots equal to a single block, effectively making $round == block$, which is cozy. This could be later refined to have a customizable parameter $slot\_duration$.

Next, a little complication is applied to this scheme, as we break our auction into two phases, each one lasts for a customizable number of blocks:

  • $Opening$ period, in which bids are accepted, but candle can’t go out during this period.
  • $Ending$ period, during which bids are accepted in every block, but only blocks up to the decisive round matter for the auction outline.
Example of an auction schedule:
[1][2][3][4][5][6][7][8][9][10][11][12][13]
  | Opening   |        Ending         |

As had been discussed here and here, the randomness of winning round selection during the latter period incentivises bidders to put their highest bid earlier.

The earliest round that can become decisive is the first round (or 1st block) of the $Ending$ period. Moreover, it is the only round that have 100% probability that bids made in it will be accounted in the outline. As bids are made increasingly, and supposing that generated randomness is uniform, then of course there is sufficient chance that the top bid from the 1st round will be out-bidden in some later round which will turn out to be decisive. However, unlike all other rounds in the $Ending$ period, there is no chance that decisive block will appear prior to this 1st block, making some lesser bid win.

In our implementation, we treat the $Opening$ period as round 0, meaning that if there is no bids made in $Ending$ period, then the winner of the $Opening$ period wins the whole auction.

This two-period scheme therefore not only makes participants to probe other bidders valuations during $Opening$ period but also pushes a rational player to submit bids close to her best valuation before this preliminary window ends.

Smart Contract

We will design an ink! (Rust eDSL) smart contract which will be then compiled into a wasm code bundle ready to run on any Substrate runtime with pallet_contracts.

Design Considerations

  1. Auction $Starting$ block, $Opening$ and $Ending$ periods specified in block numbers, and the $Subject$ are set up on contract initialization.

  2. The contract should accept bids as commitments in irreversible manner.
    To that end, special Bidding Scheme is implemented.

  3. Auction should support different subject types, e.g. an NFT token (or collection of tokens) or a domain name.
    This is the feature we call Pluggable Reward Logic

  4. Current auction status, top bid, and all significant subject attributes (ownership in particular) should be open to everyone.
    For that, getters are implemented.

  5. Winner determination and reward processing should be transparent and irreversible.
    Reward logic is executed by cross-contract method invocation.

  6. Candle blowing should be random enough.
    Contract supports customizable sources of randomness.

  7. Contract pays back all participants according to their status, once the auction ended.
    See Payouts section.

Let’s go through specific nuances of implementing these features in our contract.

Roles

We have five participant roles in the process:

  • two roles for contract accounts:

    • our Auction contract, and
    • contract that manages the auction Subject, e.g. ERC721 or DNS;
  • three roles for user accounts:

    • auction Owner, i.e. user who posess the entity being traded and sets up an auction for that,
    • bidder who wins: Winner,
    • bidder who loses: Loser.

Bidding Scheme

In order to make placed bids to become guaranteed commitments to pay the bid amount in case the bid wins, we basically have two options:

  1. Use ReservableCurrency just like this is done in parachain auction, to lock bidded amount on participant account, following by either releasing of this amount in case the bidder loses auction, or transferring this amount to auction owner as a payment for the auction subject, in case the bid wins.

  2. Make the bid() method payable so that bidder simple transfers the bid amount to the contract. Contract records a ledger with balances of all participants. Upon auction end, the contract pays all losers their bidded amounts back, while winner’s amount goes to auction owner.

As currently there is no way to implement variant 1 in Ink! alone, we decide to take variant 2 for the implementation.

For that to be done, we need two things:

  1. Ledger to record bidders balances.
    We use a HashMap which besides effectively presents top bid per user.
  2. When a bidder places her next bid, the amount of her previous bid should be paid back to her (like ‘change’), and her balance record should be updated in storage.

Mastering a Candle

In order to implement the Candle magic, we need:

  1. Special data structure which stores bids-to-rounds mapping.
    We define winning_data as featured StorageVec which holds bids for every bidding round (i.e. block).

  2. Some random() function to select the winning round amoung others insdide winning_data.

Randomness Source

The contract allows you to configure the source of randomness (see entropy module). By default, it uses ink_env::random() function which in turn utilizes randomness-collective-flip module. The latter provides generator of low-influence random values based on the block hashes from the last 81 blocks. It means that when using this particular random function, it is required to wait at least 81 blocks after the last block of $Ending$ period until invoking the function to get a random block inside that period.

Pluggable Reward Logic

What comes as a reward naturally follows from the auction subject. The magic here is that we can ‘plug in’ different reward logics into pay_back() function thankfully to an amazing Rust feature - Function Pointers. Armed with that feature, we can define different rewarding functions, like give_domain() for DNS auction and give_nft() for NFT auction, and map them to subjects using a simple array constant. Yay, Rust is awesome!

Each one of aforementioned rewarding functions invokes an external contract in its own fashion:

  • give_domain() invokes DNS contract’s transfer() method to pass the domain in question to auction winner. The domain being traded should be set up during auction contract initialization as the domain parameter. Every bidder can get the domain being traded with corresponding getter message as well as verify that it’s being owned by the auction contract.

  • give_nft() invokes ERC721 contract’s set_approval_for_all() method, which kills two birds with one stone:

    1. It makes trading of a whole NFT collection just as simple as trading a single NFT: just transfer_from() all positions of a lot to the auction contract account before the bidding starts, and you’re done!
    2. It allows us not to specify particular token ids on contract initialization, which is good.

As reward logic is implemented with cross-contract method invocation, we need some trick to be sure that that external contract ABI is consistent and the message invoked is being dispatched properly. For that purpose, Ink! conveniently provides meta attribute tag #[ink(selector = "...") to ‘freeze’ selector for particular contract message.

On a lower level, ink_env::call::CallBuilder is preferred over ink-as-dependency way, for the sake of keeping our contract loosely coupled with other contracts implementing external logic.

Payouts

Payouts can be claimed once auction ended, on per user basis by payout() method invocation:

  • Winner is paid by specified reward logic
    (e.g. a domain name transferal or an approval to became some NFT tokens operator);
  • Losers are paid by recieving their bidded amounts back;
  • auction Owner is paid by recieving winning bid amount (winner’s balance).

Transparency & Verifability

This is web3, no party should trust the other one in the process. Hence to be able to participate, one should have the ability to

  1. Verify the ownership of the assets being put to the auction. It is clear that the auction contract should posess the assets in question.
  2. Get auction status and winning bids as it goes on.
  3. Check that the contract they are dealing with is truly compiled from the source code we are describing here.

For the first two points contracts involved provide getter methods.

The third one is a basic thing you should know how to accomplish before invoking any smart contract at all. Hence we describe it right away!

Verify Contract

Imagine a malicious auction runner who tweaks our contract a little bit, to trick you and just take both your bid and the prize at the end. This tweak is as easy to implement as changing less than a single line of code in our contract.

So someone had set up a candle auction contract and claims it was built from the very source code you can inspect on Github. Then he gives you the AccountID/Address of that contract and its ABI. The possible attack is that he could give the ABI of the correct contract while still navigating you to the AccountID of a malicious contract. For some reason, PolkadotJS Apps /contracts tab allows you to Add existing contract without verifying that it’s the instance of that exactly code hash. Hopefully, this will be fixed soon (see this issue).

Good news is that you actually can do it yourself! For that, go to PolkadotJS Apps Developer/Chain State/Storage tab, select “contract” state query and “ContractInfoOf” storage field. Specify the AccountID of the contract instance in question. In response you’ll get a contractInfo data with the codeHash value. A smart contract code is stored on chain as a wasm bytecode, and codeHash is its blake2_256 hash. So it’s enough to compare code hashes to effectively vefify that the code being executed by this contract instance on chain is the same you get when compiling this contract yourself. But if for some reason you want to get the bytecode of the contract instance, you could do this as well by querying the PristineCode storage value for that codeHash.

Note that the same Ink! source code built with different compiler versions results in different wasm bytecode. Hence to verify a contract you’ll need to build it using the same compiler version as contract owner did.

Getters

The following getter methods of our contracts make all the sufficient info available to inspect for all participants.

contract method description
candle_auction get_contract() rewarding contract address
^^^ get_status() current auction status
^^^ get_subject() auction subject
^^^ get_winner() auction winner along with the bid
^^^ get_winning() account and bid winning current round
erc721 owner_of(id: TokenId) owner of particular NFT token
^^^ is_approved_for_all(owner: AccountId, operator: AccountId) is operator is approved by the owner
dns get_owner_or_default(name: Hash) TBD: report there's no such fn owner of particular domain name

Conclusion

So here is how we implement Candle Auctions in Ink! smartcontract. I hope this helps you to run your own sale of collectibles! Feel free to reach me out in case of any questions.