Buying back tokens from liqudity pool

As a motivating example, let’s consider a stablecoin protocol, which is using an oracle pool to get reserve asset value to ensure peg (such as SigUSD or Dexy). Security of the protocol is based on security of the oracle pool, and as the latter depends on oracles incentivization, it would be desirable to redirect part of fees (e.g. minting fee) to oracles.

In oracle pools 2.0 rewards are done in custom reward tokens, let’s call it GORT (gold oracle reward token). Let’s assume that there is ErgoDEX(Spectrum) liquidity pool with a contract from EIP draft.

Then we can use a contract to buy GORT tokens back from the liquidity pool and send them back to oracles pool (pools everywhere :grinning:) . The stablecoin minting contract pays part of a minting fee to the contract.

However, similar contract may be useful in different scenarios to buy tokens back from liquidity pool.

Contract is

{
  // GORT (Gold Oracle Reward Token) buyback script
  //
  // It is accepting ERGs (via top-up action), swapping them in ERG/GORT LP pool to get GORT, and gives GORT back
  // to oracle pool. See detailed description of the actions below.
  //
  // Tokens:
  //  - buyback NFT
  //  - gort
  //
  //  Registers:
  //    None
  //
  // Swap:
  //
  //   Input         |  Output        |   Data-Input
  // -----------------------------------------------
  // 0 LP            |  LP            |
  // 1 BuyBack       |  BuyBack       |
  //
  // Top-up:
  //
  //   Input         |  Output        |   Data-Input
  // -----------------------------------------------
  // 0 mint action   |  mint action   |
  // 1 Bank          |  Bank          |
  // 2 BuyBack       |  BuyBack       |
  //
  // Give back:
  //
  //   Input         |  Output        |   Data-Input
  // -----------------------------------------------
  // 0 Pool          |  Pool          |
  // 1 Refresh       |  Refresh       |
  // 2 BuyBack       |  BuyBack       |

  val action = getVar[Int](0).get

  if (action == 0) {
    // swap path
    // the contract is buying back GORTs from ERG/GORT LP here
    val buybackNft = SELF.tokens(0)._1

    // checking that swap inputs provided
    val poolInput = INPUTS(0)
    val lpCorrect = poolInput.tokens(0)._1 == fromBase64("$gortLpNFT")

    // checking that gort tokens are in LP and buyback outputs only
    // we consider other outputs are fee and maybe change,
    // and change output could not have tokens, offchain logic needs to ensure that
    def noTokens(b: Box) = b.tokens.size == 0
    val outputsCorrect = OUTPUTS.slice(2, OUTPUTS.size).forall(noTokens)

    val selfOut = OUTPUTS(1)
    val selfOutCorrect = selfOut.tokens(0)._1 == buybackNft &&
                          selfOut.tokens(1)._1 == fromBase64("$gortId")

    val price = poolInput.value / poolInput.tokens(2)._2
    val gortObtained = selfOut.tokens(1)._2 - SELF.tokens(1)._2
    val maxErgDelta = price * gortObtained / 100 * 105 // max slippage 5%
    val ergDelta = SELF.value - selfOut.value
    val poolOutput = OUTPUTS(0)
    val swapCorrect = gortObtained > 0 && ergDelta <= maxErgDelta && poolOutput.value - poolInput.value == ergDelta

    sigmaProp(lpCorrect && outputsCorrect && selfOutCorrect && swapCorrect)
  } else if(action == 1) {
    // top-up path
    // we allow to add Ergs while preserving contract and tokens
    val selfOut = OUTPUTS(2)
    val topUp = selfOut.tokens == SELF.tokens &&
                selfOut.propositionBytes == SELF.propositionBytes &&
                SELF.value < selfOut.value
    sigmaProp(topUp)
  } else {
    // return path
    // we allow to return GORT tokens to oracle pool
    // hovewer, oracle pool contract does not have dedicated top-up action,
    // but it allows to add tokens when paying rewards to oracles.
    // Thus we need to copy reward logic from oracle pool contract here to be sure the contract
    // is receiving all the tokens deducted from this box

    // starting copying from oracle pool contract
    val minStartHeight = HEIGHT - $epochLength
    val poolIn = INPUTS(0)

    def isValidDataPoint(b: Box) = if (b.R6[Long].isDefined) {
        b.creationInfo._1    >= minStartHeight &&
        b.tokens(0)._1       == fromBase64("$oracleTokenId") &&
        b.R5[Int].get        == poolIn.R5[Int].get
    }  else false

    val dataPoints = INPUTS.filter(isValidDataPoint)
    val rewardEmitted = dataPoints.size * 2
    // finishing copying from oracle pool contract

    val selfGort = SELF.tokens(1)._2
    val properGiving =  poolIn.tokens(0)._1 == fromBase64("$oracleNFT") &&
                        OUTPUTS(0).tokens(1)._2 >= poolIn.tokens(1)._2 + selfGort - rewardEmitted

    val giveback = OUTPUTS(2).tokens(0) == SELF.tokens(0) &&
                   OUTPUTS(2).propositionBytes == SELF.propositionBytes &&
                   SELF.value == OUTPUTS(2).value &&
                   properGiving

    sigmaProp(giveback)
  }
}

, and testing transactions can be found in https://github.com/kushti/dexy-stable/blob/master/src/test/scala/dexy/BuybackSpec.scala

5 Likes