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.
New markets of cryptoeconomy, such as NFTs, have combined properties of rapid growing and ineffiency 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.
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 decesive 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:  | Opening | Ending |
As had been discussed here and here, the randomness of winning round selection during the latter period incetivises 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 outbidden 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.
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.
Auction $Starting$ block, $Opening$ and $Ending$ periods specified in block numbers, and the $Subject$ are set up on contract initialization.
The contract should accept bids as commitments in irreversible manner.
To that end, special Bidding Scheme is implemented.
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
Current auction status, top bid, and all significant subject attributes (ownership in particular) should be open to everyone.
For that, getters are implemented.
Winner determination and reward processing should be transparent and irreversible.
Reward logic is executed by cross-contract method invocation.
Candle blowing should be random enough.
Contract supports customizable sources of randomness.
Contract pays back all participants accornding to their status, once the auction ended.
See Payouts section.
Let’s go through specific nuances of imlementing these features in our contract.
We have five participant roles in the process:
- two roles for contract accounts:
- 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.
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:
ReservableCurrencyjust 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.
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:
- Ledger to record bidders balances.
We use a HashMap which besides effectively presents top bid per user.
- 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:
Special data structure which stores bids-to-rounds mapping.
winning_dataas featured StorageVec which holds bids for every bidding round (i.e. block).
random()function to select the winning round amoung others insdide
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
domainparameter. 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:
- 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!
- It allows us not to specify particular token ids on contract initialization, which is good.
- It makes trading of a whole NFT collection just as simple as trading a single NFT: just
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 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 transferral 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
- Verify the ownership of the assets being put to the auction. It is clear that the auction contract should posess the assets in question.
- Get auction status and winning bids as it goes on.
- Check that the contract they are dealing with is trully compiled from the source code we are descibing here.
For the first two points contracts ivolved 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 descibe it right away!
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 quering the
PristineCode storage value for that
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.
The following getter methods of our contracts make all the sufficient info available to inspect for all participants.
||rewarding contract address|
||current auction status|
||auction winner along with the bid|
||account and bid winning current round|
||owner of particular NFT token|
||is operator is approved by the owner|
||owner of particular domain name|
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.