Skip to content

EIP-0022: Auction Contract#

🔗 From EIP-0022:

Motivation#

Decentralized auctioning of any kind of tokens (artwork, share tokens, etc.) is an important part of any blockchain. This EIP is proposing the auction contract with various features listed in the Design section

Design#

This proposed contract allows any kind of tokens to be auctioned while supporting the following features: - Any token as the auction's currency alongside ERG - "Buy it now" which allows a buyer to instantly buy the auctioned token by paying the predefined amount in the auction's currency - Auction duration as timestamp which is much more precise than block height and is independent of the network difficulty - Auto extending the duration near the end of the auction based on a global config - Minimum bid step - each bidder has to increase the previous bid at least by this amount - Royalty - The original owner (whomever issued the token) gets a share of the auction every time his/her token is auctioned using a global config

The contract#

{
  // R4: The seller's ergo tree
  // R5: Current bidder's ergo tree
  // R6: (Minimum bid, minimum step)
  // R7: The auction's end time in timestamp
  // R8: The auction's "Buy it now" amount. -1 if it is not enabled.
  // R9: Auction info that is needed for UI/UX purposes - stringfied json encoded as Coll[Byte]:
  //     - initialBid: The auction's initial bid
  //     - startTime: The auction's start time
  //     - description: The auction's description
  //     - Any other info needed in the future
  //
  // tokens(0): auctioned token
  // tokens(1): current bid for non-ERG auctions - doesn't exist otherwise
  //
  // dataInputs(0): Auction house configuration - contains configs like UI fee and artist fee
  //     - R4: UI fee in thousand, e.g. 10 for 1% or 5 for 0.5%
  //     - R5: UI implementor's ergo tree
  //     - R6: Maximum miner fee for withdrawal
  //     - R7: Extend configuration as a Coll[Long] with two values (extendThreshold, extendNum)
  //             e.g., (30 * 60 * 1000L, 40 * 60 * 1000L) to extend the duration for 40min if 30min is left when the bid is placed

  // originalIssuanceBox: Spent box with ID equal to the NFT ID
  //     - R4: Royalty percentage - 0 if empty
  //     - R5: Artist address

  val seller = SELF.R4[Coll[Byte]].get
  val currBidder = SELF.R5[Coll[Byte]].get
  val minBid = SELF.R6[Coll[Long]].get(0)
  val minStep = SELF.R6[Coll[Long]].get(1)
  val endTime = SELF.R7[Long].get
  val buyItNowAmount = SELF.R8[Long].get

  // auction currency can be any token like SigUSD
  val auctionCurrency = if (SELF.tokens.size > 1) SELF.tokens(1)._1
                        else Coll[Byte]()
  val isCurrencyERG = auctionCurrency.size == 0
  val getBoxVal = {(b: Box) => {
     if (isCurrencyERG) b.value
     else {
       if (b.tokens.size == 1 && b.tokens(0)._1 == auctionCurrency) b.tokens(0)._2
       else if (b.tokens.size == 2 && b.tokens(1)._1 == auctionCurrency) b.tokens(1)._2
       else 0L
     }
  }}

  val currBid = getBoxVal(SELF)

  val auctionConfig = CONTEXT.dataInputs(0)

  // auction is not finished, a bid is being placed
  val isNextBid = {
     val extendConfig = auctionConfig.R7[Coll[Long]]
     val extendThreshold = extendConfig.get(0)
     val extendNum = extendConfig.get(1)
     // we extend the auction end time by extendNum if the bid is placed near the very end (extendThreshold)
     val newEndTime = if (endTime - CONTEXT.preHeader.timestamp <= extendThreshold) endTime + extendNum
                      else endTime


     val newSelf = OUTPUTS(0) // new auction box
     val oldBidRefundBox = OUTPUTS(1) // refund box

     val isUsingBuyItNow = buyItNowAmount != -1 && getBoxVal(newSelf) >= buyItNowAmount

     // as a general check, ERG value can not be less than the current
     newSelf.value >= SELF.value &&
     // auction is not finished
     CONTEXT.preHeader.timestamp < endTime &&
     // preserve auctioned tokens
     newSelf.tokens(0) == SELF.tokens(0) &&
     // correct value and contract for the new box
     (getBoxVal(newSelf) >= getBoxVal(SELF) + minStep || getBoxVal(SELF) < minBid || isUsingBuyItNow) &&
     getBoxVal(newSelf) >= minBid &&
     newSelf.propositionBytes == SELF.propositionBytes &&
     // shouldn't be able to add tokens - will change the currency from ERG to a worthless token
     SELF.tokens.size == newSelf.tokens.size &&
     // currency must be the same
     // refund the previous bidder
     oldBidRefundBox.propositionBytes == currBidder &&
     getBoxVal(oldBidRefundBox) >= currBid &&
     // preserve the auction config
     newSelf.R4[Coll[Byte]].get == seller &&
     // just making sure that the new R5's type is Coll[Byte]
     newSelf.R5[Coll[Byte]].get.size > 0 &&
     newSelf.R6[Coll[Long]].get.size == 2 &&
     newSelf.R6[Coll[Long]].get(0) == minBid &&
     newSelf.R6[Coll[Long]].get(1) == minStep &&
     newSelf.R7[Long].get == newEndTime &&
     newSelf.R8[Long].get == buyItNowAmount &&
     newSelf.R9[Coll[Byte]] == SELF.R9[Coll[Byte]]

  }

  // either auction has ended due to time or "Buy it now" is being used
  val isFinishedWithBid = {
     val winnerBox = OUTPUTS(0)
     val auctionFeeBox = OUTPUTS(1)
     val sellerBox = if (OUTPUTS.size < 3) SELF
                         else OUTPUTS(2)
     val artistRoyalty = if (OUTPUTS.size < 4) SELF
                         else OUTPUTS(3)
     val originalIssuanceBox = if (auctionFeeBox.R4[Box].isDefined) auctionFeeBox.R4[Box].get
                               else OUTPUTS(OUTPUTS.size - 1)

     val auctionFee = (currBid * auctionConfig.R4[Int].get) / 1000
     val auctionFeeTo = auctionConfig.R5[Coll[Byte]].get // ui implementor's ergo tree

     val artistSharePerc = if (originalIssuanceBox.R4[Int].isDefined) max(originalIssuanceBox.R4[Int].get, 0)
                           else 0
     val artistShare = (currBid * artistSharePerc) / 1000

     val maxFee = auctionConfig.R6[Long].get
     // if currency is not ERG, then nothing has to be deducted from it for miner fee. Otherwise, 2 * maxFee will be deducted
     val minerFeeInCurrency = if (isCurrencyERG) maxFee * 2 // one maxFee for the miner fee and one for the seller box
                              else 0L

     val artistGetsHisShare = {
       artistShare == 0L || {
         blake2b256(originalIssuanceBox.bytes) == SELF.tokens(0)._1 && // the same ID as the NFT - the integrity of the box is also ensured with this line
         getBoxVal(artistRoyalty) >= artistShare && // gets at least the percentage defined in the auction config box
         artistRoyalty.propositionBytes == originalIssuanceBox.propositionBytes // goes to the artist
       }
     }

     val buyItNow = (currBid >= buyItNowAmount && buyItNowAmount != -1)

     val outSize = if (artistShare == 0L) 4
                   else 5

     // either auction is finished or "Buy it now" is used
     (CONTEXT.preHeader.timestamp >= endTime || buyItNow) &&
     getBoxVal(auctionFeeBox) >= auctionFee &&
     auctionFeeBox.propositionBytes == auctionFeeTo &&
     winnerBox.tokens(0) == SELF.tokens(0) &&
     winnerBox.propositionBytes == currBidder &&
     getBoxVal(sellerBox) >= currBid - auctionFee - artistShare - minerFeeInCurrency &&
     sellerBox.propositionBytes == seller &&
     artistGetsHisShare &&
     OUTPUTS.size == outSize
  }

  // in this case, there is no winner and the seller doesn't need to pay any fee to the auction house or the artist
  val isFinishedWithoutBid = {
     val maxFee = auctionConfig.R6[Long].get
     val minerFeeInCurrency = if (isCurrencyERG) maxFee
                              else 0L
     currBid < minBid &&
     CONTEXT.preHeader.timestamp >= endTime &&
     OUTPUTS.size == 2 &&
     OUTPUTS(0).tokens(0) == SELF.tokens(0) &&
     OUTPUTS(0).propositionBytes == seller &&
     getBoxVal(OUTPUTS(0)) >= currBid - minerFeeInCurrency
  }

  val validConfig = auctionConfig.tokens(0)._1 == AUCTION_CONFIG_TOKEN_ID
  sigmaProp((isNextBid || isFinishedWithBid || isFinishedWithoutBid) && validConfig)
}