Skip to content

Latest commit

 

History

History
257 lines (189 loc) · 11 KB

eip-0015.md

File metadata and controls

257 lines (189 loc) · 11 KB

SigmaUSD Contracts Standard

  • Author: kushti, scalahub, Robert Kornacki
  • Status: Proposed
  • Created: 12-Mar-2021
  • Last edited: 12-Mar-2021
  • License: CC0
  • Track: Applications, Standards

Motivation

This Ergo Improvement Proposal defines SigmaUSD contracts actually deployed on the blockchain and used via known user interfaces. Thus in case of bank script update this EIP is to updated as well.

Bank Script

    { // this box
      // R4: Number of stable-coins in circulation
      // R5: Number of reserve-coins in circulation

      val feePercent = 2 // in percent, so 2% fee

      // Base-64 version of ERG/USD oracle pool NFT 011d3364de07e5a26f0c4eef0852cddb387039a921b7154ef3cab22c6eda887f
      // UI at https://explorer.ergoplatform.com/en/oracle-pool-state/ergusd
      // Got via http://tomeko.net/online_tools/hex_to_base64.php
      val oraclePoolNFT = fromBase64("AR0zZN4H5aJvDE7vCFLN2zhwOakhtxVO88qyLG7aiH8=") 
      
      // Base-64 version of bank update NFT 239c170b7e82f94e6b05416f14b8a2a57e0bfff0e3c93f4abbcd160b6a5b271a
      // Got via http://tomeko.net/online_tools/hex_to_base64.php
      val updateNFT = fromBase64("I5wXC36C+U5rBUFvFLiipX4L//DjyT9Ku80WC2pbJxo=")
      
      val coolingOffHeight = 460000  

      val minStorageRent = 10000000L 
      val minReserveRatioPercent = 400L // percent
      val defaultMaxReserveRatioPercent = 800L // percent
      val INF = 1000000000L
      val LongMax = 9223372036854775807L 
      val rcDefaultPrice = 1000000L
    
      val isExchange = if (CONTEXT.dataInputs.size > 0) {
    
        val dataInput = CONTEXT.dataInputs(0)
        val validDataInput = dataInput.tokens(0)._1 == oraclePoolNFT
   
        val bankBoxIn = SELF
        val bankBoxOut = OUTPUTS(0)
   
        val rateBox = dataInput
        val receiptBox = OUTPUTS(1)
   
        val rate = rateBox.R4[Long].get / 100
   
        val scCircIn = bankBoxIn.R4[Long].get
        val rcCircIn = bankBoxIn.R5[Long].get
        val bcReserveIn = bankBoxIn.value
   
        val scTokensIn = bankBoxIn.tokens(0)._2
        val rcTokensIn = bankBoxIn.tokens(1)._2
   
        val scCircOut = bankBoxOut.R4[Long].get
        val rcCircOut = bankBoxOut.R5[Long].get
        val bcReserveOut = bankBoxOut.value
   
        val scTokensOut = bankBoxOut.tokens(0)._2
        val rcTokensOut = bankBoxOut.tokens(1)._2
   
        val totalScIn = scTokensIn + scCircIn
        val totalScOut = scTokensOut + scCircOut
   
        val totalRcIn = rcTokensIn + rcCircIn
        val totalRcOut = rcTokensOut + rcCircOut
   
        val rcExchange = rcTokensIn != rcTokensOut
        val scExchange = scTokensIn != scTokensOut
   
        val rcExchangeXorScExchange = (rcExchange || scExchange) && !(rcExchange && scExchange)
   
        val circDelta = receiptBox.R4[Long].get
        val bcReserveDelta = receiptBox.R5[Long].get
   
        val rcCircDelta = if (rcExchange) circDelta else 0L
        val scCircDelta = if (rcExchange) 0L else circDelta
   
        val validDeltas = (scCircIn + scCircDelta == scCircOut) &&
                           (rcCircIn + rcCircDelta == rcCircOut) &&
                           (bcReserveIn + bcReserveDelta == bcReserveOut) &&
                           scCircOut >= 0 && rcCircOut >= 0
   
        val coinsConserved = totalRcIn == totalRcOut && totalScIn == totalScOut
   
        val tokenIdsConserved = bankBoxOut.tokens(0)._1 == bankBoxIn.tokens(0)._1 && // also ensures that at least one token exists
                                bankBoxOut.tokens(1)._1 == bankBoxIn.tokens(1)._1 && // also ensures that at least one token exists
                                bankBoxOut.tokens(2)._1 == bankBoxIn.tokens(2)._1    // also ensures that at least one token exists
   
        val mandatoryRateConditions = rateBox.tokens(0)._1 == oraclePoolNFT
        val mandatoryBankConditions = bankBoxOut.value >= minStorageRent &&
                                      bankBoxOut.propositionBytes == bankBoxIn.propositionBytes &&
                                      rcExchangeXorScExchange &&
                                      coinsConserved &&
                                      validDeltas &&
                                      tokenIdsConserved
   
        // exchange equations
        val bcReserveNeededOut = scCircOut * rate
        val bcReserveNeededIn = scCircIn * rate
        val liabilitiesIn = max(min(bcReserveIn, bcReserveNeededIn), 0)
        val maxReserveRatioPercent = if (HEIGHT > coolingOffHeight) defaultMaxReserveRatioPercent else INF
   
        val reserveRatioPercentOut = if (bcReserveNeededOut == 0) maxReserveRatioPercent else bcReserveOut * 100 / bcReserveNeededOut
   
        val validReserveRatio = if (scExchange) {
          if (scCircDelta > 0) {
            reserveRatioPercentOut >= minReserveRatioPercent
          } else true
        } else {
          if (rcCircDelta > 0) {
            reserveRatioPercentOut <= maxReserveRatioPercent
          } else {
            reserveRatioPercentOut >= minReserveRatioPercent
          }
        }
   
        val brDeltaExpected = if (scExchange) { // sc
          val liableRate = if (scCircIn == 0) LongMax else liabilitiesIn / scCircIn
          val scNominalPrice = min(rate, liableRate)
          scNominalPrice * scCircDelta
        } else { // rc
          val equityIn = bcReserveIn - liabilitiesIn
          val equityRate = if (rcCircIn == 0) rcDefaultPrice else equityIn / rcCircIn
          val rcNominalPrice = if (equityIn == 0) rcDefaultPrice else equityRate
          rcNominalPrice * rcCircDelta
        }
   
        val fee = brDeltaExpected * feePercent / 100
        val actualFee = if (fee < 0) {-fee} else fee
     
        // actualFee is always positive, irrespective of brDeltaExpected
        val brDeltaExpectedWithFee = brDeltaExpected + actualFee
   
        mandatoryRateConditions &&
         mandatoryBankConditions &&
         bcReserveDelta == brDeltaExpectedWithFee &&
         validReserveRatio &&
         validDataInput
      } else false

      val isUpdate = INPUTS(0).tokens(0)._1 == updateNFT
   
      sigmaProp(isExchange || isUpdate)
    }

Update Script

{ // This box (update box):
  // Registers empty 
  // 
  // ballot boxes (Inputs)
  // R4 the pub key of voter [GroupElement] (not used here)
  // R5 dummy int due to AOTC non-lazy evaluation (from the line marked **** below)
  // R6 the box id of this box [Coll[Byte]]
  // R7 the value voted for [Coll[Byte]]

  // Base-64 version of the stablecoin bank NFT 7d672d1def471720ca5782fd6473e47e796d9ac0c138d9911346f118b2f6d9d9 
  // Issued in https://explorer.ergoplatform.com/en/transactions/134db72906d84ea8f6d5b4dc0bbfeaed880836f36dffc4bda8254071b519000a
  // Got via http://tomeko.net/online_tools/hex_to_base64.php
  val bankNFT = fromBase64("fWctHe9HFyDKV4L9ZHPkfnltmsDBONmRE0bxGLL22dk=")

  // Base-64 version of the ballot token ID f7995f212216fcf21854f56df7a9a0a9fc9b7ae4c0f1cc40f5b406371286a5e0
  // Issued in https://explorer.ergoplatform.com/en/transactions/744f7080e0373c754f9c8174989c1307e92f4a3937799f15628b1d434d70afb9
  // Got via http://tomeko.net/online_tools/hex_to_base64.php
  val ballotTokenId = fromBase64("95lfISIW/PIYVPVt96mgqfybeuTA8cxA9bQGNxKGpeA=")

  val minVotes = 3

  // collect and update in one step
  val updateBoxOut = OUTPUTS(0) // copy of this box is the 1st output
  val validUpdateIn = SELF.id == INPUTS(0).id // this is 1st input

  val bankBoxIn = INPUTS(1) // bank box is 2nd input
  val bankBoxOut = OUTPUTS(1) // copy of bank box is the 2nd output

  // compute the hash of the bank output box. This should be the value voted for
  val bankBoxOutHash = blake2b256(bankBoxOut.propositionBytes)

  val validBankIn = bankBoxIn.tokens.size == 3 && bankBoxIn.tokens(2)._1 == bankNFT
  val validBankOut = bankBoxIn.tokens == bankBoxOut.tokens &&
                     bankBoxIn.value == bankBoxOut.value &&
                     bankBoxIn.R4[Long].get == bankBoxOut.R4[Long].get &&
                     bankBoxIn.R5[Long].get == bankBoxOut.R5[Long].get 


  val validUpdateOut = SELF.tokens == updateBoxOut.tokens && 
                       SELF.propositionBytes == updateBoxOut.propositionBytes &&
                       updateBoxOut.value >= SELF.value

  def isValidBallot(b:Box) = {
    b.tokens.size > 0 && 
    b.tokens(0)._1 == ballotTokenId &&
    b.R6[Coll[Byte]].get == SELF.id && // ensure vote corresponds to this box ****
    b.R7[Coll[Byte]].get == bankBoxOutHash // check value voted for
  }

  val ballotBoxes = INPUTS.filter(isValidBallot)

  val votesCount = ballotBoxes.fold(0L, {(accum: Long, b: Box) => accum + b.tokens(0)._2})

  sigmaProp(validBankIn && validBankOut && validUpdateIn && validUpdateOut && votesCount >= minVotes)
}

Ballot Script

{ // This box (ballot box):
  // R4 the group element of the owner of the ballot token [GroupElement]
  // R5 dummy Int due to AOTC non-lazy evaluation (since bank box has Long at R5). Due to the line marked ****
  // R6 the box id of the update box [Coll[Byte]]
  // R7 the value voted for [Coll[Byte]]
  
  // Base-64 version of bank update NFT 239c170b7e82f94e6b05416f14b8a2a57e0bfff0e3c93f4abbcd160b6a5b271a
  // Got via http://tomeko.net/online_tools/hex_to_base64.php
  val updateNFT = fromBase64("I5wXC36C+U5rBUFvFLiipX4L//DjyT9Ku80WC2pbJxo=")
      
  val pubKey = SELF.R4[GroupElement].get
  
  val index = INPUTS.indexOf(SELF, 0)
  
  val output = OUTPUTS(index)
  
  val isBasicCopy = output.R4[GroupElement].get == pubKey && 
                    output.propositionBytes == SELF.propositionBytes &&
                    output.tokens == SELF.tokens && 
                    output.value >= SELF.value
                    
  val castVote = proveDlog(pubKey) 
   
  val notVoted = output.R7[Coll[Byte]].isDefined == false
  
  val update = INPUTS(0).tokens(0)._1 == updateNFT && notVoted
  
  // Note: We want that during an update, a new valid vote should not be generated
  // notVoted ensures that the update action does not generate a new vote.
  // This is already prevented by having R6 contain the id of the update box,
  // This guarantees that R6 can never contain this box Id (because it depends on the outputs)
  // However, notVoted is used for additional security 
  
  sigmaProp(
    isBasicCopy && (castVote || update)
  )
}