From fd3b7c3181580244ffabc984b716b58c3e5c47a5 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Wed, 20 Apr 2022 02:18:27 +0530 Subject: [PATCH 1/8] Add Dexy stablecoin EIP (#30) --- README.md | 1 + eip-0030.md | 329 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 eip-0030.md diff --git a/README.md b/README.md index bab35b9f..a7b57959 100644 --- a/README.md +++ b/README.md @@ -19,3 +19,4 @@ Please check out existing EIPs, such as [EIP-1](eip-0001.md), to understand the | [EIP-0022](eip-0022.md) | Auction Contract | | [EIP-0024](eip-0024.md) | Artwork contract | | [EIP-0025](eip-0025.md) | Payment Request URI | +| [EIP-0030](eip-0030.md) | Dexy StableCoin design | diff --git a/eip-0030.md b/eip-0030.md new file mode 100644 index 00000000..b2951ccf --- /dev/null +++ b/eip-0030.md @@ -0,0 +1,329 @@ +# Dexy StableCoin + +* Author: @kushti, @scalahub, @code-for-uss +* Status: Proposed +* Created: 20-April-2022 +* License: CC0 +* Forking: not needed + +## Description + +This EIP defines a design for a stablecoin called "Dexy", first proposed by @kushti. +Dexy uses a combination of oracle-pool and a liquidity pool. + +Below are the main aspects of Dexy. + +1. **One-way tethering**: There is a minting (or "emission") contract that emits Dexy tokens (example DexyUSD) in a one-way swap using the oracle pool rate. + The swap is one-way in the sense that we can only buy Dexy tokens by selling ergs to the box. We cannot do the reverse swap. + +2. **Liquidify Pool**: The reverse swap, selling of Dexy tokens, is done via a Liquidity Pool (LP) which also permits buying Dexy tokens. The LP + primarily uses the logic of Uniswap V2. The difference is that the LP also takes as input the oracle pool rate and uses that to modify certain logic. In particular, + redeeming of LP tokens is not allowed when the oracle pool rate is below a certain percent (say 90%) of the oracle pool rate. + +3. In case the oracle pool rate is higher than LP rate, then traders can do arbitrage by minting Dexy tokens from the emission box and + selling them to the LP. + +4. In case the oracle pool rate is lower than LP rate, then the Ergs collected in the emission box can be used to bring the rate back up by performing a swap. + We call this the "top-up swap". + +The swap logic is encoded in a **swapping** contract. + +There is another contract, the **tracking** contract that is responsible for tracking the LP's state. In particular, this contract +tracks the block at which the "top-up-swap" is initiated. The swap can be initiated when the LP rate falls below 90%. +Once initiated, if the LP rate remains below the oracle pool rate for a certain threshold number of blocks, the swap can be compleded. +On the other hand, if before the threshold the rate goes higher than oracle pool then the swap must be aborted. + +The LP uses a "cross-counter" to keep count of the number of times the LP rate has crossed the oracle pool rate (from below or above) in a swap transaction. +If the cross-counter is preserved at swap initiation and completion then swap is valid, else it is aborted. This logic is present in the swapping box. + +## Emission Contract + +```scala +{ + // This box: (dexyUSD emission box) + // tokens(0): emissionNFT identifying the box + // tokens(1): dexyUSD tokens to be emitted + + val selfOutIndex = getVar[Int](0).get + + val oraclePoolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify oracle pool box + val swappingNFT = fromBase64("Fho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify swapping box for future use + + val validEmission = { + val oraclePoolBox = CONTEXT.dataInputs(0) // oracle-pool (v1 and v2) box containing rate in R4 + + val validOP = oraclePoolBox.tokens(0)._1 == oraclePoolNFT + + val oraclePoolRate = oraclePoolBox.R4[Long].get // can assume always > 0 (ref oracle pool contracts) NanoErgs per USD + + val selfOut = OUTPUTS(selfOutIndex) + + val validSelfOut = selfOut.tokens(0) == SELF.tokens(0) && // emissionNFT and quantity preserved + selfOut.propositionBytes == SELF.propositionBytes && // script preserved + selfOut.tokens(1)._1 == SELF.tokens(1)._1 && // dexyUSD tokenId preserved + selfOut.value > SELF.value // can only purchase dexyUSD, not sell it + + val inTokens = SELF.tokens(1)._2 + val outTokens = selfOut.tokens(1)._2 + + val deltaErgs = selfOut.value - SELF.value // deltaErgs must be (+)ve because ergs must increase + + val deltaTokens = inTokens - outTokens // outTokens must be < inTokens (see below) + + val validDelta = deltaErgs >= deltaTokens * oraclePoolRate // deltaTokens must be (+)ve, since both deltaErgs and oraclePoolRate are (+)ve + + validOP && validSelfOut && validDelta + } + + val validTopping = INPUTS(0).tokens(0)._1 == swappingNFT + + sigmaProp(validEmission || validTopping) +} +``` +## Liquidity Pool Contract + +```scala +{ + // Notation: + // + // X is the primary token + // Y is the secondary token + // When using Erg-USD oracle v1, X is NanoErg and Y is USD + + // This box: (LP box) + // R1 (value): X tokens in NanoErgs + // R4: How many LP in circulation (long). This can be non-zero when bootstrapping, to consider the initial token burning in UniSwap v2 + // R5: Cross-counter. A counter to track how many times the rate has "crossed" the oracle pool rate. That is the oracle pool rate falls in between the before and after rates + // Tokens(0): LP NFT to uniquely identify NFT box. (Could we possibly do away with this?) + // Tokens(1): LP tokens + // Tokens(2): Y tokens (Note that X tokens are NanoErgs (the value) + // + // Data Input #0: (oracle pool box) + // R4: Rate in units of X per unit of Y + // Token(0): OP NFT to uniquely identify Oracle Pool + + // constants + val feeNum = 3 // 0.3 % + val feeDenom = 1000 + val minStorageRent = 10000000L // this many number of nanoErgs are going to be permanently locked + + val successor = OUTPUTS(0) // copy of this box after exchange + val oraclePoolBox = CONTEXT.dataInputs(0) // oracle pool box + val validOraclePoolBox = oraclePoolBox.tokens(0)._1 == fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify oracle pool box + + val lpNFT0 = SELF.tokens(0) + val reservedLP0 = SELF.tokens(1) + val tokenY0 = SELF.tokens(2) + + val lpNFT1 = successor.tokens(0) + val reservedLP1 = successor.tokens(1) + val tokenY1 = successor.tokens(2) + + val supplyLP0 = SELF.R4[Long].get // LP tokens in circulation in input LP box + val supplyLP1 = successor.R4[Long].get // LP tokens in circulation in output LP box + + val validSuccessorScript = successor.propositionBytes == SELF.propositionBytes + + val preservedLpNFT = lpNFT1 == lpNFT0 + val validLP = reservedLP1._1 == reservedLP0._1 + val validY = tokenY1._1 == tokenY0._1 + val validSupplyLP1 = supplyLP1 >= 0 + + // since tokens can be repeated, we ensure for sanity that there are no more tokens + val noMoreTokens = successor.tokens.size == 3 + + val validStorageRent = successor.value > minStorageRent + + val reservesX0 = SELF.value + val reservesY0 = tokenY0._2 + val reservesX1 = successor.value + val reservesY1 = tokenY1._2 + + val oraclePoolRateXY = oraclePoolBox.R4[Long].get + val lpRateXY0 = reservesX0 / reservesY0 // we can assume that reservesY0 > 0 (since at least one token must exist) + val lpRateXY1 = reservesX1 / reservesY1 // we can assume that reservesY1 > 0 (since at least one token must exist) + val isCrossing = (lpRateXY0 - oraclePoolRateXY) * (lpRateXY1 - oraclePoolRateXY) < 0 // if (and only if) oracle pool rate falls in between, then this will be negative + + val crossCounterIn = SELF.R5[Int].get + val crossCounterOut = successor.R5[Int].get + + val validCrossCounter = crossCounterOut == {if (isCrossing) crossCounterIn + 1 else crossCounterIn} + + val validRateForRedeemingLP = oraclePoolRateXY > lpRateXY0 * 9 / 10 // lpRate must be >= 0.9 oraclePoolRate // these parameters need to be tweaked + // Do we need above if we also have the tracking contract? + + val deltaSupplyLP = supplyLP1 - supplyLP0 + val deltaReservesX = reservesX1 - reservesX0 + val deltaReservesY = reservesY1 - reservesY0 + + // LP formulae below using UniSwap v2 (with initial token burning by bootstrapping with positive R4) + val validDepositing = { + val sharesUnlocked = min( + deltaReservesX.toBigInt * supplyLP0 / reservesX0, + deltaReservesY.toBigInt * supplyLP0 / reservesY0 + ) + deltaSupplyLP <= sharesUnlocked + } + + val validRedemption = { + val _deltaSupplyLP = deltaSupplyLP.toBigInt + // note: _deltaSupplyLP, deltaReservesX and deltaReservesY are negative + deltaReservesX.toBigInt * supplyLP0 >= _deltaSupplyLP * reservesX0 && deltaReservesY.toBigInt * supplyLP0 >= _deltaSupplyLP * reservesY0 + } && validRateForRedeemingLP + + val validSwap = + if (deltaReservesX > 0) + reservesY0.toBigInt * deltaReservesX * feeNum >= -deltaReservesY * (reservesX0.toBigInt * feeDenom + deltaReservesX * feeNum) + else + reservesX0.toBigInt * deltaReservesY * feeNum >= -deltaReservesX * (reservesY0.toBigInt * feeDenom + deltaReservesY * feeNum) + + val validAction = + if (deltaSupplyLP == 0) + validSwap + else + if (deltaReservesX > 0 && deltaReservesY > 0) validDepositing + else validRedemption + + sigmaProp( + validSupplyLP1 && + validSuccessorScript && + validOraclePoolBox && + preservedLpNFT && + validLP && + validY && + noMoreTokens && + validAction && + validStorageRent && + validCrossCounter + ) +} +``` +## Tracking Contract + +```scala +{ + // Tracking box + // R4: Crossing Counter of LP box + // tokens(0): TrackingNFT + + val thresholdPercent = 90 // 90% or less value (of LP in terms of OraclePool) will trigger action (ensure less than 100) + val errorMargin = 3 // number of blocks where tracking error is allowed + + val lpNFT = fromBase64("Nho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify LP box for future use + val oraclePoolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify oracle pool box + + val lpBox = CONTEXT.dataInputs(0) + val oraclePoolBox = CONTEXT.dataInputs(1) + + val validLpBox = lpBox.tokens(0)._1 == lpNFT + val validOraclePoolBox = oraclePoolBox.tokens(0)._1 == oraclePoolNFT + + val tokenY = lpBox.tokens(2) + + val reservesX = lpBox.value + val reservesY = tokenY._2 + + val lpRateXY = reservesX / reservesY // we can assume that reservesY > 0 (since at least one token must exist) + + val oraclePoolRateXY = oraclePoolBox.R4[Long].get + + val crossCounter = lpBox.R5[Int].get // stores how many times LP rate has crossed oracle pool rate (by cross, we mean going from above to below or vice versa) + + val successor = OUTPUTS(0) + + val validThreshold = lpRateXY * 100 < thresholdPercent * oraclePoolRateXY + + val validSuccessor = successor.propositionBytes == SELF.propositionBytes && + successor.tokens == SELF.tokens && + successor.value >= SELF.value + + val validTracking = successor.R4[Int].get == crossCounter && + successor.creationInfo._1 > (HEIGHT - errorMargin) + + sigmaProp( + validLpBox && + validOraclePoolBox && + validThreshold && + validSuccessor && + validTracking + ) +} +``` + +## Swapping Contract + +```scala +{ + val waitingPeriod = 20 // blocks after which a trigger swap event can be completed, provided rate has not crossed oracle pool rate + val emissionNFT = fromBase64("Bho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify LP box for future use + val lpNFT = fromBase64("Nho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify LP box for future use + val trackingNFT = fromBase64("Jho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify LP box for future use + val oraclePoolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify oracle pool box + + val thresholdPercent = 90 // 90% or less value (of LP in terms of OraclePool) will trigger action (ensure less than 100) + + val oraclePoolBox = CONTEXT.dataInputs(0) + val trackingBox = CONTEXT.dataInputs(1) + + val lpBoxIn = INPUTS(0) + val emissionBoxIn = INPUTS(1) + + val lpBoxOut = OUTPUTS(0) + val emissionBoxOut = OUTPUTS(1) + + val successor = OUTPUTS(2) // SELF should be INPUTS(2) + + val tokenYIn = lpBoxIn.tokens(2) + val tokenYOut = lpBoxOut.tokens(2) + + val reservesXIn = lpBoxIn.value + val reservesYIn = tokenYIn._2 + + val reservesXOut = lpBoxOut.value + val reservesYOut = tokenYOut._2 + + val lpRateXYIn = reservesXIn / reservesYIn // we can assume that reservesYIn > 0 (since at least one token must exist) + val lpRateXYOut = reservesXOut / reservesYOut // we can assume that reservesYOut > 0 (since at least one token must exist) + + val oraclePoolRateXY = oraclePoolBox.R4[Long].get + + val validThreshold = lpRateXYIn * 100 < thresholdPercent * oraclePoolRateXY + + val validTrackingBox = trackingBox.tokens(0)._1 == trackingNFT + val validOraclePoolBox = oraclePoolBox.tokens(0)._1 == oraclePoolNFT + val validLpBox = lpBoxIn.tokens(0)._1 == lpNFT + + val validSuccessor = successor.propositionBytes == SELF.propositionBytes && + successor.tokens == SELF.tokens && + successor.value == SELF.value + + val validEmissionBoxIn = emissionBoxIn.tokens(0)._1 == emissionNFT + val validEmissionBoxOut = emissionBoxOut.tokens(0) == emissionBoxIn.tokens(0) && + emissionBoxOut.tokens(1)._1 == emissionBoxIn.tokens(1)._1 + + val deltaEmissionTokens = emissionBoxOut.tokens(1)._2 - emissionBoxIn.tokens(1)._2 + val deltaEmissionErgs = emissionBoxIn.value - emissionBoxOut.value + val deltaLpX = reservesXOut - reservesXIn + val deltaLpY = reservesYIn - reservesYOut + + val validLpIn = lpBoxIn.R5[Int].get == trackingBox.R4[Int].get && // no change in cross-counter + trackingBox.creationInfo._1 < HEIGHT - waitingPeriod // at least waitingPeriod blocks have passed since the tracking started + + val lpRateXYOutTimes100 = lpRateXYOut * 100 + + val validSwap = lpRateXYOutTimes100 >= oraclePoolRateXY * 105 && // new rate must be >= 1.05 times oracle rate + lpRateXYOutTimes100 <= oraclePoolRateXY * 110 && // new rate must be <= 1.1 times oracle rate + deltaEmissionErgs <= deltaLpX && // ergs reduced in emission box must be <= ergs gained in LP + deltaEmissionTokens >= deltaLpY && // tokens gained in emission box must be >= tokens reduced in LP + validEmissionBoxIn && + validEmissionBoxOut && + validSuccessor && + validLpBox && + validOraclePoolBox && + validTrackingBox && + validThreshold && + validLpIn + + sigmaProp(validSwap) +} +``` \ No newline at end of file From 9d7282c213faef03e841dd0f16e5ee70fa45053e Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Thu, 28 Apr 2022 20:32:33 +0530 Subject: [PATCH 2/8] Fix LP redeeming logic typo in description --- eip-0030.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eip-0030.md b/eip-0030.md index b2951ccf..2dfc1cb3 100644 --- a/eip-0030.md +++ b/eip-0030.md @@ -18,7 +18,7 @@ Below are the main aspects of Dexy. 2. **Liquidify Pool**: The reverse swap, selling of Dexy tokens, is done via a Liquidity Pool (LP) which also permits buying Dexy tokens. The LP primarily uses the logic of Uniswap V2. The difference is that the LP also takes as input the oracle pool rate and uses that to modify certain logic. In particular, - redeeming of LP tokens is not allowed when the oracle pool rate is below a certain percent (say 90%) of the oracle pool rate. + redeeming of LP tokens is not allowed when the oracle pool rate is below a certain percent (say 90%) of the LP rate. 3. In case the oracle pool rate is higher than LP rate, then traders can do arbitrage by minting Dexy tokens from the emission box and selling them to the LP. From cea90c2922dbb62ba1b0e67c6c0e894125c96482 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Wed, 18 May 2022 00:53:42 +0530 Subject: [PATCH 3/8] Fix typos, address kushti's comments --- eip-0030.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/eip-0030.md b/eip-0030.md index 2dfc1cb3..1f4af8a1 100644 --- a/eip-0030.md +++ b/eip-0030.md @@ -26,11 +26,13 @@ Below are the main aspects of Dexy. 4. In case the oracle pool rate is lower than LP rate, then the Ergs collected in the emission box can be used to bring the rate back up by performing a swap. We call this the "top-up swap". -The swap logic is encoded in a **swapping** contract. +The swap logic is encoded in a **swapping** contract. The swap logic allows anyone to swap Ergs for DexyUSD (i.e., it allows the swapping contract to buy DexyUSD from the LP). +The swap is possible only under certain conditions: (1) The oracle pool rate is lower than LP rate, and (2) It remains so for a certain number of blocks. This is done with the +help of tracking contract (see below). There is another contract, the **tracking** contract that is responsible for tracking the LP's state. In particular, this contract tracks the block at which the "top-up-swap" is initiated. The swap can be initiated when the LP rate falls below 90%. -Once initiated, if the LP rate remains below the oracle pool rate for a certain threshold number of blocks, the swap can be compleded. +Once initiated, if the LP rate remains below the oracle pool rate for a certain threshold number of blocks, the swap can be completed. On the other hand, if before the threshold the rate goes higher than oracle pool then the swap must be aborted. The LP uses a "cross-counter" to keep count of the number of times the LP rate has crossed the oracle pool rate (from below or above) in a swap transaction. @@ -54,14 +56,14 @@ If the cross-counter is preserved at swap initiation and completion then swap is val validOP = oraclePoolBox.tokens(0)._1 == oraclePoolNFT - val oraclePoolRate = oraclePoolBox.R4[Long].get // can assume always > 0 (ref oracle pool contracts) NanoErgs per USD + val oraclePoolRate = oraclePoolBox.R4[Long].get // can assume always > 0 (ref oracle pool contracts) NanoErgs per USD cent val selfOut = OUTPUTS(selfOutIndex) - val validSelfOut = selfOut.tokens(0) == SELF.tokens(0) && // emissionNFT and quantity preserved + val validSelfOut = selfOut.tokens(0) == SELF.tokens(0) && // emissionNFT preserved selfOut.propositionBytes == SELF.propositionBytes && // script preserved selfOut.tokens(1)._1 == SELF.tokens(1)._1 && // dexyUSD tokenId preserved - selfOut.value > SELF.value // can only purchase dexyUSD, not sell it + selfOut.value > SELF.value // users can only purchase dexyUSD, not sell it val inTokens = SELF.tokens(1)._2 val outTokens = selfOut.tokens(1)._2 @@ -86,9 +88,9 @@ If the cross-counter is preserved at swap initiation and completion then swap is { // Notation: // - // X is the primary token - // Y is the secondary token - // When using Erg-USD oracle v1, X is NanoErg and Y is USD + // X is the primary token (NanoErgs) + // Y is the secondary token (USD cents) + // When using Erg-USD oracle v1, X is NanoErg and Y is USD cent // This box: (LP box) // R1 (value): X tokens in NanoErgs From 547a2d270854570908e74c3f80571a7ccb067601 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Wed, 18 May 2022 00:56:07 +0530 Subject: [PATCH 4/8] Explain purpose of swapping contract logic --- eip-0030.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eip-0030.md b/eip-0030.md index 1f4af8a1..eee0ead5 100644 --- a/eip-0030.md +++ b/eip-0030.md @@ -28,7 +28,8 @@ Below are the main aspects of Dexy. The swap logic is encoded in a **swapping** contract. The swap logic allows anyone to swap Ergs for DexyUSD (i.e., it allows the swapping contract to buy DexyUSD from the LP). The swap is possible only under certain conditions: (1) The oracle pool rate is lower than LP rate, and (2) It remains so for a certain number of blocks. This is done with the -help of tracking contract (see below). +help of tracking contract (see below), which tracks when a swap was initiated, etc. The remaining part of the contract replicates the LP's swap logic to ensure that the correct +amount is exchanged. There is another contract, the **tracking** contract that is responsible for tracking the LP's state. In particular, this contract tracks the block at which the "top-up-swap" is initiated. The swap can be initiated when the LP rate falls below 90%. From a2e3466eaf4f58c98add204f320bb3af43ac75f1 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Fri, 3 Jun 2022 03:13:41 +0530 Subject: [PATCH 5/8] Don't use cross counter to track rate crossing --- eip-0030.md | 87 +++++++++++------------------------------------------ 1 file changed, 18 insertions(+), 69 deletions(-) diff --git a/eip-0030.md b/eip-0030.md index eee0ead5..180a2610 100644 --- a/eip-0030.md +++ b/eip-0030.md @@ -96,7 +96,7 @@ If the cross-counter is preserved at swap initiation and completion then swap is // This box: (LP box) // R1 (value): X tokens in NanoErgs // R4: How many LP in circulation (long). This can be non-zero when bootstrapping, to consider the initial token burning in UniSwap v2 - // R5: Cross-counter. A counter to track how many times the rate has "crossed" the oracle pool rate. That is the oracle pool rate falls in between the before and after rates + // R5: Stores the height where oracle pool rate becomes lower than LP rate. Reset to Long.MaxValue when rate crossed back. Called crossToggle below // Tokens(0): LP NFT to uniquely identify NFT box. (Could we possibly do away with this?) // Tokens(1): LP tokens // Tokens(2): Y tokens (Note that X tokens are NanoErgs (the value) @@ -146,11 +146,17 @@ If the cross-counter is preserved at swap initiation and completion then swap is val lpRateXY0 = reservesX0 / reservesY0 // we can assume that reservesY0 > 0 (since at least one token must exist) val lpRateXY1 = reservesX1 / reservesY1 // we can assume that reservesY1 > 0 (since at least one token must exist) val isCrossing = (lpRateXY0 - oraclePoolRateXY) * (lpRateXY1 - oraclePoolRateXY) < 0 // if (and only if) oracle pool rate falls in between, then this will be negative - - val crossCounterIn = SELF.R5[Int].get - val crossCounterOut = successor.R5[Int].get - - val validCrossCounter = crossCounterOut == {if (isCrossing) crossCounterIn + 1 else crossCounterIn} + + val crossToggleIn = SELF.R5[Int].get + val crossToggleOut = successor.R5[Int].get + + val validCrossCounter = { + if (isCrossing) { + if (lpRateXY1 > oraclePoolRateXY) { + crossToggleOut >= HEIGHT - threshold + } else crossToggleOut == 9223372036854775807L // Long.MaxValue + } else crossToggleOut == crossToggleIn + } val validRateForRedeemingLP = oraclePoolRateXY > lpRateXY0 * 9 / 10 // lpRate must be >= 0.9 oraclePoolRate // these parameters need to be tweaked // Do we need above if we also have the tracking contract? @@ -201,72 +207,18 @@ If the cross-counter is preserved at swap initiation and completion then swap is ) } ``` -## Tracking Contract - -```scala -{ - // Tracking box - // R4: Crossing Counter of LP box - // tokens(0): TrackingNFT - - val thresholdPercent = 90 // 90% or less value (of LP in terms of OraclePool) will trigger action (ensure less than 100) - val errorMargin = 3 // number of blocks where tracking error is allowed - - val lpNFT = fromBase64("Nho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify LP box for future use - val oraclePoolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify oracle pool box - - val lpBox = CONTEXT.dataInputs(0) - val oraclePoolBox = CONTEXT.dataInputs(1) - - val validLpBox = lpBox.tokens(0)._1 == lpNFT - val validOraclePoolBox = oraclePoolBox.tokens(0)._1 == oraclePoolNFT - - val tokenY = lpBox.tokens(2) - - val reservesX = lpBox.value - val reservesY = tokenY._2 - - val lpRateXY = reservesX / reservesY // we can assume that reservesY > 0 (since at least one token must exist) - - val oraclePoolRateXY = oraclePoolBox.R4[Long].get - - val crossCounter = lpBox.R5[Int].get // stores how many times LP rate has crossed oracle pool rate (by cross, we mean going from above to below or vice versa) - - val successor = OUTPUTS(0) - - val validThreshold = lpRateXY * 100 < thresholdPercent * oraclePoolRateXY - - val validSuccessor = successor.propositionBytes == SELF.propositionBytes && - successor.tokens == SELF.tokens && - successor.value >= SELF.value - - val validTracking = successor.R4[Int].get == crossCounter && - successor.creationInfo._1 > (HEIGHT - errorMargin) - - sigmaProp( - validLpBox && - validOraclePoolBox && - validThreshold && - validSuccessor && - validTracking - ) -} -``` - ## Swapping Contract ```scala { val waitingPeriod = 20 // blocks after which a trigger swap event can be completed, provided rate has not crossed oracle pool rate - val emissionNFT = fromBase64("Bho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify LP box for future use - val lpNFT = fromBase64("Nho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify LP box for future use - val trackingNFT = fromBase64("Jho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify LP box for future use - val oraclePoolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify oracle pool box + val emissionNFT = fromBase64("Bho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") + val lpNFT = fromBase64("Nho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") + val oraclePoolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") val thresholdPercent = 90 // 90% or less value (of LP in terms of OraclePool) will trigger action (ensure less than 100) val oraclePoolBox = CONTEXT.dataInputs(0) - val trackingBox = CONTEXT.dataInputs(1) val lpBoxIn = INPUTS(0) val emissionBoxIn = INPUTS(1) @@ -292,7 +244,6 @@ If the cross-counter is preserved at swap initiation and completion then swap is val validThreshold = lpRateXYIn * 100 < thresholdPercent * oraclePoolRateXY - val validTrackingBox = trackingBox.tokens(0)._1 == trackingNFT val validOraclePoolBox = oraclePoolBox.tokens(0)._1 == oraclePoolNFT val validLpBox = lpBoxIn.tokens(0)._1 == lpNFT @@ -308,10 +259,9 @@ If the cross-counter is preserved at swap initiation and completion then swap is val deltaEmissionErgs = emissionBoxIn.value - emissionBoxOut.value val deltaLpX = reservesXOut - reservesXIn val deltaLpY = reservesYIn - reservesYOut - - val validLpIn = lpBoxIn.R5[Int].get == trackingBox.R4[Int].get && // no change in cross-counter - trackingBox.creationInfo._1 < HEIGHT - waitingPeriod // at least waitingPeriod blocks have passed since the tracking started - + + val validLpIn = lpBoxIn.R5[Int].get < HEIGHT - waitingPeriod // at least waitingPeriod blocks have passed since the tracking started + val lpRateXYOutTimes100 = lpRateXYOut * 100 val validSwap = lpRateXYOutTimes100 >= oraclePoolRateXY * 105 && // new rate must be >= 1.05 times oracle rate @@ -323,7 +273,6 @@ If the cross-counter is preserved at swap initiation and completion then swap is validSuccessor && validLpBox && validOraclePoolBox && - validTrackingBox && validThreshold && validLpIn From f706d6d4cf866a7061c7d84e8b36dd38ce150de4 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Sat, 4 Jun 2022 00:02:02 +0530 Subject: [PATCH 6/8] Update Dexy-USD text with new LP tracking logic --- eip-0030.md | 257 ++++++++++++++++++++++++++-------------------------- 1 file changed, 130 insertions(+), 127 deletions(-) diff --git a/eip-0030.md b/eip-0030.md index 180a2610..7068f880 100644 --- a/eip-0030.md +++ b/eip-0030.md @@ -27,60 +27,62 @@ Below are the main aspects of Dexy. We call this the "top-up swap". The swap logic is encoded in a **swapping** contract. The swap logic allows anyone to swap Ergs for DexyUSD (i.e., it allows the swapping contract to buy DexyUSD from the LP). -The swap is possible only under certain conditions: (1) The oracle pool rate is lower than LP rate, and (2) It remains so for a certain number of blocks. This is done with the -help of tracking contract (see below), which tracks when a swap was initiated, etc. The remaining part of the contract replicates the LP's swap logic to ensure that the correct -amount is exchanged. +The swap is possible only under certain conditions: (1) The oracle pool rate is lower than LP rate, and (2) It remains so for a certain number of blocks. +This is done with the help of register R5 of the LP box (see below), which stores the height at which the oracle rate dropped below the LP rate. +The remaining part of the contract replicates the LP's swap logic to ensure that the correct amount is exchanged. -There is another contract, the **tracking** contract that is responsible for tracking the LP's state. In particular, this contract -tracks the block at which the "top-up-swap" is initiated. The swap can be initiated when the LP rate falls below 90%. -Once initiated, if the LP rate remains below the oracle pool rate for a certain threshold number of blocks, the swap can be completed. -On the other hand, if before the threshold the rate goes higher than oracle pool then the swap must be aborted. +The LP uses a "cross-tracker" to keep track of the height at which the oracle pool rate dropped below the LP rate after a swap, that is +the height at which the oracle pool rate was above the LP-box in rate but was below the LP-box out rate. +When this even happens, R5 of the LP box will store the height (within an error margin) at which this happened. +If the oracle pool rate again becomes higher than the LP rate, the register is set to Long.MaxValue (9223372036854775807). +This register can only be changed when the oracle pool and LP rates cross each other during an exchange. At all other times +this register must be preserved. -The LP uses a "cross-counter" to keep count of the number of times the LP rate has crossed the oracle pool rate (from below or above) in a swap transaction. -If the cross-counter is preserved at swap initiation and completion then swap is valid, else it is aborted. This logic is present in the swapping box. +The swap contract looks at R5 of the LP box and if the value is below a certain threshold (say 50 blocks) then the swap is allowed. +This implies that a swap is valid only when the oracle pool rate falls below the LP rate and stays below for at least 50 blocks. ## Emission Contract ```scala -{ - // This box: (dexyUSD emission box) - // tokens(0): emissionNFT identifying the box - // tokens(1): dexyUSD tokens to be emitted - - val selfOutIndex = getVar[Int](0).get - - val oraclePoolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify oracle pool box - val swappingNFT = fromBase64("Fho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify swapping box for future use - - val validEmission = { - val oraclePoolBox = CONTEXT.dataInputs(0) // oracle-pool (v1 and v2) box containing rate in R4 - - val validOP = oraclePoolBox.tokens(0)._1 == oraclePoolNFT - - val oraclePoolRate = oraclePoolBox.R4[Long].get // can assume always > 0 (ref oracle pool contracts) NanoErgs per USD cent - - val selfOut = OUTPUTS(selfOutIndex) - - val validSelfOut = selfOut.tokens(0) == SELF.tokens(0) && // emissionNFT preserved - selfOut.propositionBytes == SELF.propositionBytes && // script preserved - selfOut.tokens(1)._1 == SELF.tokens(1)._1 && // dexyUSD tokenId preserved - selfOut.value > SELF.value // users can only purchase dexyUSD, not sell it - - val inTokens = SELF.tokens(1)._2 - val outTokens = selfOut.tokens(1)._2 - - val deltaErgs = selfOut.value - SELF.value // deltaErgs must be (+)ve because ergs must increase - - val deltaTokens = inTokens - outTokens // outTokens must be < inTokens (see below) - - val validDelta = deltaErgs >= deltaTokens * oraclePoolRate // deltaTokens must be (+)ve, since both deltaErgs and oraclePoolRate are (+)ve - - validOP && validSelfOut && validDelta - } - - val validTopping = INPUTS(0).tokens(0)._1 == swappingNFT - - sigmaProp(validEmission || validTopping) +{ + // This box: (dexyUSD emission box) + // tokens(0): emissionNFT identifying the box + // tokens(1): dexyUSD tokens to be emitted + + val selfOutIndex = getVar[Int](0).get + + val oraclePoolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify oracle pool box + val swappingNFT = fromBase64("Fho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify swapping box for future use + + val validEmission = { + val oraclePoolBox = CONTEXT.dataInputs(0) // oracle-pool (v1 and v2) box containing rate in R4 + + val validOP = oraclePoolBox.tokens(0)._1 == oraclePoolNFT + + val oraclePoolRate = oraclePoolBox.R4[Long].get // can assume always > 0 (ref oracle pool contracts) NanoErgs per USD + + val selfOut = OUTPUTS(selfOutIndex) + + val validSelfOut = selfOut.tokens(0) == SELF.tokens(0) && // emissionNFT and quantity preserved + selfOut.propositionBytes == SELF.propositionBytes && // script preserved + selfOut.tokens(1)._1 == SELF.tokens(1)._1 && // dexyUSD tokenId preserved + selfOut.value > SELF.value // can only purchase dexyUSD, not sell it + + val inTokens = SELF.tokens(1)._2 + val outTokens = selfOut.tokens(1)._2 + + val deltaErgs = selfOut.value - SELF.value // deltaErgs must be (+)ve because ergs must increase + + val deltaTokens = inTokens - outTokens // outTokens must be < inTokens (see below) + + val validDelta = deltaErgs >= deltaTokens * oraclePoolRate // deltaTokens must be (+)ve, since both deltaErgs and oraclePoolRate are (+)ve + + validOP && validSelfOut && validDelta + } + + val validTopping = INPUTS(0).tokens(0)._1 == swappingNFT + + sigmaProp(validEmission || validTopping) } ``` ## Liquidity Pool Contract @@ -89,14 +91,14 @@ If the cross-counter is preserved at swap initiation and completion then swap is { // Notation: // - // X is the primary token (NanoErgs) - // Y is the secondary token (USD cents) - // When using Erg-USD oracle v1, X is NanoErg and Y is USD cent + // X is the primary token + // Y is the secondary token + // When using Erg-USD oracle v1, X is NanoErg and Y is USD // This box: (LP box) // R1 (value): X tokens in NanoErgs // R4: How many LP in circulation (long). This can be non-zero when bootstrapping, to consider the initial token burning in UniSwap v2 - // R5: Stores the height where oracle pool rate becomes lower than LP rate. Reset to Long.MaxValue when rate crossed back. Called crossToggle below + // R5: Stores the height where oracle pool rate becomes lower than LP rate. Reset to Long.MaxValue when rate crossed back. Called crossTracker below // Tokens(0): LP NFT to uniquely identify NFT box. (Could we possibly do away with this?) // Tokens(1): LP tokens // Tokens(2): Y tokens (Note that X tokens are NanoErgs (the value) @@ -105,7 +107,8 @@ If the cross-counter is preserved at swap initiation and completion then swap is // R4: Rate in units of X per unit of Y // Token(0): OP NFT to uniquely identify Oracle Pool - // constants + // constants + val threshold = 3 // error threshold in crossTracker val feeNum = 3 // 0.3 % val feeDenom = 1000 val minStorageRent = 10000000L // this many number of nanoErgs are going to be permanently locked @@ -146,17 +149,17 @@ If the cross-counter is preserved at swap initiation and completion then swap is val lpRateXY0 = reservesX0 / reservesY0 // we can assume that reservesY0 > 0 (since at least one token must exist) val lpRateXY1 = reservesX1 / reservesY1 // we can assume that reservesY1 > 0 (since at least one token must exist) val isCrossing = (lpRateXY0 - oraclePoolRateXY) * (lpRateXY1 - oraclePoolRateXY) < 0 // if (and only if) oracle pool rate falls in between, then this will be negative - - val crossToggleIn = SELF.R5[Int].get - val crossToggleOut = successor.R5[Int].get - + + val crossTrackerIn = SELF.R5[Int].get + val crossTrackerOut = successor.R5[Int].get + val validCrossCounter = { if (isCrossing) { if (lpRateXY1 > oraclePoolRateXY) { - crossToggleOut >= HEIGHT - threshold - } else crossToggleOut == 9223372036854775807L // Long.MaxValue - } else crossToggleOut == crossToggleIn - } + crossTrackerOut >= HEIGHT - threshold + } else crossTrackerOut == 9223372036854775807L + } else crossTrackerOut == crossTrackerIn + } val validRateForRedeemingLP = oraclePoolRateXY > lpRateXY0 * 9 / 10 // lpRate must be >= 0.9 oraclePoolRate // these parameters need to be tweaked // Do we need above if we also have the tracking contract? @@ -210,72 +213,72 @@ If the cross-counter is preserved at swap initiation and completion then swap is ## Swapping Contract ```scala -{ - val waitingPeriod = 20 // blocks after which a trigger swap event can be completed, provided rate has not crossed oracle pool rate - val emissionNFT = fromBase64("Bho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") - val lpNFT = fromBase64("Nho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") - val oraclePoolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") - - val thresholdPercent = 90 // 90% or less value (of LP in terms of OraclePool) will trigger action (ensure less than 100) - - val oraclePoolBox = CONTEXT.dataInputs(0) - - val lpBoxIn = INPUTS(0) - val emissionBoxIn = INPUTS(1) +{ + val waitingPeriod = 20 // blocks after which a trigger swap event can be completed, provided rate has not crossed oracle pool rate + val emissionNFT = fromBase64("Bho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") + val lpNFT = fromBase64("Nho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") + val oraclePoolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") - val lpBoxOut = OUTPUTS(0) - val emissionBoxOut = OUTPUTS(1) - - val successor = OUTPUTS(2) // SELF should be INPUTS(2) - - val tokenYIn = lpBoxIn.tokens(2) - val tokenYOut = lpBoxOut.tokens(2) - - val reservesXIn = lpBoxIn.value - val reservesYIn = tokenYIn._2 - - val reservesXOut = lpBoxOut.value - val reservesYOut = tokenYOut._2 - - val lpRateXYIn = reservesXIn / reservesYIn // we can assume that reservesYIn > 0 (since at least one token must exist) - val lpRateXYOut = reservesXOut / reservesYOut // we can assume that reservesYOut > 0 (since at least one token must exist) - - val oraclePoolRateXY = oraclePoolBox.R4[Long].get - - val validThreshold = lpRateXYIn * 100 < thresholdPercent * oraclePoolRateXY - - val validOraclePoolBox = oraclePoolBox.tokens(0)._1 == oraclePoolNFT - val validLpBox = lpBoxIn.tokens(0)._1 == lpNFT - - val validSuccessor = successor.propositionBytes == SELF.propositionBytes && - successor.tokens == SELF.tokens && - successor.value == SELF.value - - val validEmissionBoxIn = emissionBoxIn.tokens(0)._1 == emissionNFT - val validEmissionBoxOut = emissionBoxOut.tokens(0) == emissionBoxIn.tokens(0) && - emissionBoxOut.tokens(1)._1 == emissionBoxIn.tokens(1)._1 - - val deltaEmissionTokens = emissionBoxOut.tokens(1)._2 - emissionBoxIn.tokens(1)._2 - val deltaEmissionErgs = emissionBoxIn.value - emissionBoxOut.value - val deltaLpX = reservesXOut - reservesXIn - val deltaLpY = reservesYIn - reservesYOut - - val validLpIn = lpBoxIn.R5[Int].get < HEIGHT - waitingPeriod // at least waitingPeriod blocks have passed since the tracking started - - val lpRateXYOutTimes100 = lpRateXYOut * 100 - - val validSwap = lpRateXYOutTimes100 >= oraclePoolRateXY * 105 && // new rate must be >= 1.05 times oracle rate - lpRateXYOutTimes100 <= oraclePoolRateXY * 110 && // new rate must be <= 1.1 times oracle rate - deltaEmissionErgs <= deltaLpX && // ergs reduced in emission box must be <= ergs gained in LP - deltaEmissionTokens >= deltaLpY && // tokens gained in emission box must be >= tokens reduced in LP - validEmissionBoxIn && - validEmissionBoxOut && - validSuccessor && - validLpBox && - validOraclePoolBox && - validThreshold && - validLpIn - - sigmaProp(validSwap) + val thresholdPercent = 90 // 90% or less value (of LP in terms of OraclePool) will trigger action (ensure less than 100) + + val oraclePoolBox = CONTEXT.dataInputs(0) + + val lpBoxIn = INPUTS(0) + val emissionBoxIn = INPUTS(1) + + val lpBoxOut = OUTPUTS(0) + val emissionBoxOut = OUTPUTS(1) + + val successor = OUTPUTS(2) // SELF should be INPUTS(2) + + val tokenYIn = lpBoxIn.tokens(2) + val tokenYOut = lpBoxOut.tokens(2) + + val reservesXIn = lpBoxIn.value + val reservesYIn = tokenYIn._2 + + val reservesXOut = lpBoxOut.value + val reservesYOut = tokenYOut._2 + + val lpRateXYIn = reservesXIn / reservesYIn // we can assume that reservesYIn > 0 (since at least one token must exist) + val lpRateXYOut = reservesXOut / reservesYOut // we can assume that reservesYOut > 0 (since at least one token must exist) + + val oraclePoolRateXY = oraclePoolBox.R4[Long].get + + val validThreshold = lpRateXYIn * 100 < thresholdPercent * oraclePoolRateXY + + val validOraclePoolBox = oraclePoolBox.tokens(0)._1 == oraclePoolNFT + val validLpBox = lpBoxIn.tokens(0)._1 == lpNFT + + val validSuccessor = successor.propositionBytes == SELF.propositionBytes && + successor.tokens == SELF.tokens && + successor.value == SELF.value + + val validEmissionBoxIn = emissionBoxIn.tokens(0)._1 == emissionNFT + val validEmissionBoxOut = emissionBoxOut.tokens(0) == emissionBoxIn.tokens(0) && + emissionBoxOut.tokens(1)._1 == emissionBoxIn.tokens(1)._1 + + val deltaEmissionTokens = emissionBoxOut.tokens(1)._2 - emissionBoxIn.tokens(1)._2 + val deltaEmissionErgs = emissionBoxIn.value - emissionBoxOut.value + val deltaLpX = reservesXOut - reservesXIn + val deltaLpY = reservesYIn - reservesYOut + + val validLpIn = lpBoxIn.R5[Int].get < HEIGHT - waitingPeriod // at least waitingPeriod blocks have passed since the tracking started + + val lpRateXYOutTimes100 = lpRateXYOut * 100 + + val validSwap = lpRateXYOutTimes100 >= oraclePoolRateXY * 105 && // new rate must be >= 1.05 times oracle rate + lpRateXYOutTimes100 <= oraclePoolRateXY * 110 && // new rate must be <= 1.1 times oracle rate + deltaEmissionErgs <= deltaLpX && // ergs reduced in emission box must be <= ergs gained in LP + deltaEmissionTokens >= deltaLpY && // tokens gained in emission box must be >= tokens reduced in LP + validEmissionBoxIn && + validEmissionBoxOut && + validSuccessor && + validLpBox && + validOraclePoolBox && + validThreshold && + validLpIn + + sigmaProp(validSwap) } ``` \ No newline at end of file From e4b30e7cb860a5772ba59b6ba88cfc7df5d1c4f0 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Tue, 12 Jul 2022 00:46:50 +0530 Subject: [PATCH 7/8] Update contract nomenclature based on paper --- eip-0030.md | 326 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 252 insertions(+), 74 deletions(-) diff --git a/eip-0030.md b/eip-0030.md index 7068f880..3b608861 100644 --- a/eip-0030.md +++ b/eip-0030.md @@ -13,21 +13,22 @@ Dexy uses a combination of oracle-pool and a liquidity pool. Below are the main aspects of Dexy. -1. **One-way tethering**: There is a minting (or "emission") contract that emits Dexy tokens (example DexyUSD) in a one-way swap using the oracle pool rate. +1. **One-way tethering**: There is a Bank that emits Dexy tokens (example DexyUSD) in a one-way swap using the oracle pool rate. The swap is one-way in the sense that we can only buy Dexy tokens by selling ergs to the box. We cannot do the reverse swap. + This one-way swap is called a "mint" operation, and is captured using the *Free Mint* contract. -2. **Liquidify Pool**: The reverse swap, selling of Dexy tokens, is done via a Liquidity Pool (LP) which also permits buying Dexy tokens. The LP - primarily uses the logic of Uniswap V2. The difference is that the LP also takes as input the oracle pool rate and uses that to modify certain logic. In particular, +2. **Liquidity Pool**: The reverse swap, selling of Dexy tokens, is done via a Liquidity Pool (LP) which also permits buying Dexy tokens. The LP + primarily uses the logic of UniSwap V2. The difference is that the LP also takes as input the oracle pool rate and uses that to modify certain logic. In particular, redeeming of LP tokens is not allowed when the oracle pool rate is below a certain percent (say 90%) of the LP rate. 3. In case the oracle pool rate is higher than LP rate, then traders can do arbitrage by minting Dexy tokens from the emission box and - selling them to the LP. + selling them to the LP. This is captured using the *Arbitrage Mint* contract. 4. In case the oracle pool rate is lower than LP rate, then the Ergs collected in the emission box can be used to bring the rate back up by performing a swap. - We call this the "top-up swap". + We call this the operation an *Intervention*. -The swap logic is encoded in a **swapping** contract. The swap logic allows anyone to swap Ergs for DexyUSD (i.e., it allows the swapping contract to buy DexyUSD from the LP). -The swap is possible only under certain conditions: (1) The oracle pool rate is lower than LP rate, and (2) It remains so for a certain number of blocks. +The intervention logic is encoded in a **intervention** contract. This logic allows anyone to swap Ergs for DexyUSD (i.e., it allows the intervention contract to buy DexyUSD from the LP). +The intervention is possible only under certain conditions: (1) The oracle pool rate is lower than LP rate, and (2) It remains so for a certain number of blocks. This is done with the help of register R5 of the LP box (see below), which stores the height at which the oracle rate dropped below the LP rate. The remaining part of the contract replicates the LP's swap logic to ensure that the correct amount is exchanged. @@ -38,53 +39,207 @@ If the oracle pool rate again becomes higher than the LP rate, the register is s This register can only be changed when the oracle pool and LP rates cross each other during an exchange. At all other times this register must be preserved. -The swap contract looks at R5 of the LP box and if the value is below a certain threshold (say 50 blocks) then the swap is allowed. +The intervention contract looks at R5 of the LP box and if the value is below a certain threshold (say 50 blocks) then the swap is allowed. This implies that a swap is valid only when the oracle pool rate falls below the LP rate and stays below for at least 50 blocks. -## Emission Contract +## Bank Contract ```scala { - // This box: (dexyUSD emission box) - // tokens(0): emissionNFT identifying the box + // This box: (dexyUSD bank box) emits DexyUsd in exchange for Ergs + // tokens(0): bankNFT identifying the box // tokens(1): dexyUSD tokens to be emitted - val selfOutIndex = getVar[Int](0).get + // Usually bank box will be spent as follows - val oraclePoolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify oracle pool box - val swappingNFT = fromBase64("Fho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify swapping box for future use + // Arbitrage Mint + // Input | Output | Data-Input + // ------------------------------------- + // 0 Bank | Bank | Oracle + // 1 ArbitrageMint | ArbitrageMint | LP - val validEmission = { - val oraclePoolBox = CONTEXT.dataInputs(0) // oracle-pool (v1 and v2) box containing rate in R4 + // Free Mint + // Input | Output | Data-Input + // ------------------------------------- + // 0 Bank | Bank | Oracle + // 1 FreeMint | FreeMit | LP - val validOP = oraclePoolBox.tokens(0)._1 == oraclePoolNFT + // Intervention + // Input | Output | Data-Input + // ------------------------------------- + // 0 LP | LP | Oracle + // 1 Bank | Bank | - val oraclePoolRate = oraclePoolBox.R4[Long].get // can assume always > 0 (ref oracle pool contracts) NanoErgs per USD + val selfOutIndex = getVar[Int](0).get - val selfOut = OUTPUTS(selfOutIndex) + val interventionNFT = fromBase64("Fho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify intervention box for future use + val freeMintNFT = fromBase64("Bho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") + val arbitrageMintNFT = fromBase64("lho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") - val validSelfOut = selfOut.tokens(0) == SELF.tokens(0) && // emissionNFT and quantity preserved - selfOut.propositionBytes == SELF.propositionBytes && // script preserved - selfOut.tokens(1)._1 == SELF.tokens(1)._1 && // dexyUSD tokenId preserved - selfOut.value > SELF.value // can only purchase dexyUSD, not sell it + val selfOut = OUTPUTS(selfOutIndex) + val validSelfOut = selfOut.tokens(0) == SELF.tokens(0) && // bankNFT and quantity preserved + selfOut.propositionBytes == SELF.propositionBytes && // script preserved + selfOut.tokens(1)._1 == SELF.tokens(1)._1 // dexyUSD tokenId preserved - val inTokens = SELF.tokens(1)._2 - val outTokens = selfOut.tokens(1)._2 + val validMint = INPUTS(1).tokens(0)._1 == freeMintNFT || + INPUTS(1).tokens(0)._1 == arbitrageMintNFT - val deltaErgs = selfOut.value - SELF.value // deltaErgs must be (+)ve because ergs must increase + val validIntervention = INPUTS(2).tokens(0)._1 == interventionNFT - val deltaTokens = inTokens - outTokens // outTokens must be < inTokens (see below) + sigmaProp(validSelfOut && (validMint || validIntervention)) +} +``` - val validDelta = deltaErgs >= deltaTokens * oraclePoolRate // deltaTokens must be (+)ve, since both deltaErgs and oraclePoolRate are (+)ve +## Free Mint Contract +```scala +{ // + // this box: (free-mint box) + // tokens(0): Free-mint NFT + // + // R4: (Int) height at which counter will reset + // R5: (Long) remaining stablecoins available to be purchased before counter is reset + + val oracleNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify oracle pool box + val bankNFT = fromBase64("hho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") + val lpNFT = fromBase64("Nho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") + val t_free = 100 + + val feeNum = 10 + val feeDenom = 1000 + // actual fee ratio is feeNum / feeDenom + // example if feeNum = 10 and feeDenom = 1000 then fee = 0.01 = 1 % + + val oracleBox = CONTEXT.dataInputs(0) // oracle-pool (v1 and v2) box containing rate in R4 + val lpBox = CONTEXT.dataInputs(1) + val bankBoxIn = INPUTS(0) + + val selfOutIndex = getVar[Int](0).get + val bankOutIndex = getVar[Int](1).get + + val selfOut = OUTPUTS(selfOutIndex) + val bankBoxOut = OUTPUTS(bankOutIndex) + + val selfInR4 = SELF.R4[Int].get + val selfInR5 = SELF.R5[Long].get + val selfOutR4 = selfOut.R4[Int].get + val selfOutR5 = selfOut.R5[Long].get - validOP && validSelfOut && validDelta - } + val isCounterReset = HEIGHT > selfInR4 + + val oracleRateWithoutFee = oracleBox.R4[Long].get // can assume always > 0 (ref oracle pool contracts) NanoErgs per USD + val oracleRate = oracleRateWithoutFee * (feeNum + feeDenom) / feeDenom - val validTopping = INPUTS(0).tokens(0)._1 == swappingNFT + val lpReservesX = lpBox.value + val lpReservesY = lpBox.tokens(2)._2 // dexyReserves + val lpRate = lpReservesX / lpReservesY + + val validRateFreeMint = 98 * lpRate < oracleRate * 100 && + oracleRate * 100 < 102 * lpRate + + val dexyMinted = bankBoxIn.tokens(1)._2 - bankBoxOut.tokens(1)._2 + val ergsAdded = bankBoxOut.value - bankBoxIn.value + val validDelta = ergsAdded >= dexyMinted * oracleRate && ergsAdded > 0 // dexyMinted must be (+)ve, since both ergsAdded and oracleRate are (+)ve + + val maxAllowedIfReset = lpReservesY / 100 + + val availableToMint = if (isCounterReset) maxAllowedIfReset else selfInR5 + + val validAmount = dexyMinted <= availableToMint + + val validSelfOutR4 = selfOutR4 == (if (isCounterReset) HEIGHT + t_free else selfInR4) + val validSelfOutR5 = selfOutR5 == availableToMint - dexyMinted + + val validBankBoxInOut = bankBoxIn.tokens(0)._1 == bankNFT && bankBoxOut.tokens(0)._1 == bankNFT + val validLpBox = lpBox.tokens(0)._1 == lpNFT + val validOracleBox = oracleBox.tokens(0)._1 == oracleNFT + val validSelfOut = selfOut.tokens == SELF.tokens && // NFT preserved + selfOut.propositionBytes == SELF.propositionBytes && // script preserved + selfOut.value > SELF.value && validSelfOutR5 && validSelfOutR4 + + sigmaProp(validAmount && validBankBoxInOut && validLpBox && validOracleBox && validSelfOut && validDelta && validRateFreeMint) +} +``` + +## Arbitrage Mint Contract +```scala +{ + // this box: (arbitrage-mint box) + // tokens(0): Arbitrage-mint NFT + // + // R4: (Int) height at which counter will reset + // R5: (Long) remaining stablecoins available to be purchased before counter is reset + + val oracleNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify oracle pool box + val bankNFT = fromBase64("hho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") + val lpNFT = fromBase64("Nho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") + val T_arb = 30 // 30 blocks = 1 hour + val thresholdPercent = 101 // 101% or more value (of LP in terms of OraclePool) will trigger action + + val feeNum = 5 + val feeDenom = 1000 + // actual fee ratio is feeNum / feeDenom + // example if feeNum = 5 and feeDenom = 1000 then fee = 0.005 = 0.5 % + + val oracleBox = CONTEXT.dataInputs(0) // oracle-pool (v1 and v2) box containing rate in R4 + val lpBox = CONTEXT.dataInputs(1) + val bankBoxIn = INPUTS(0) + + val selfOutIndex = getVar[Int](0).get + val bankOutIndex = getVar[Int](1).get + + val selfOut = OUTPUTS(selfOutIndex) + val bankBoxOut = OUTPUTS(bankOutIndex) + + val selfInR4 = SELF.R4[Int].get + val selfInR5 = SELF.R5[Long].get + val selfOutR4 = selfOut.R4[Int].get + val selfOutR5 = selfOut.R5[Long].get - sigmaProp(validEmission || validTopping) + val isCounterReset = HEIGHT > selfInR4 + + val oracleRateWithoutFee = oracleBox.R4[Long].get // can assume always > 0 (ref oracle pool contracts) NanoErgs per USD + val oracleRate = oracleRateWithoutFee * (feeNum + feeDenom) / feeDenom + + val lpReservesX = lpBox.value + val lpReservesY = lpBox.tokens(2)._2 // dexyReserves + val lpRate = lpReservesX / lpReservesY + + val dexyMinted = bankBoxIn.tokens(1)._2 - bankBoxOut.tokens(1)._2 + val ergsAdded = bankBoxOut.value - bankBoxIn.value + val validDelta = ergsAdded >= dexyMinted * oracleRate && ergsAdded > 0 // dexyMinted must be (+)ve, since both ergsAdded and oracleRate are (+)ve + + val maxAllowedIfReset = (lpReservesX - oracleRate * lpReservesY) / oracleRate + + // above formula: + // Before mint rate is lpReservesX / lpReservesY, which should be greater than oracleRate + // After mint rate is lpReservesX / (lpReservesY + dexyMinted), which should be same or less than than oracleRate + // Thus: + // lpReservesX / lpReservesY > oracleRate + // lpReservesX / (lpReservesY + dexyMinted) <= oracleRate + // above gives min value of dexyMinted = (lpReservesX - oracleRate * lpReservesY) / oracleRate + + val availableToMint = if (isCounterReset) maxAllowedIfReset else selfInR5 + + val validAmount = dexyMinted <= availableToMint + + val validSelfOutR4 = selfOutR4 == (if (isCounterReset) HEIGHT + T_arb else selfInR4) + val validSelfOutR5 = selfOutR5 == availableToMint - dexyMinted + + val validBankBoxInOut = bankBoxIn.tokens(0)._1 == bankNFT && bankBoxOut.tokens(0)._1 == bankNFT + val validLpBox = lpBox.tokens(0)._1 == lpNFT + val validOracleBox = oracleBox.tokens(0)._1 == oracleNFT + val validSelfOut = selfOut.tokens == SELF.tokens && // NFT preserved + selfOut.propositionBytes == SELF.propositionBytes && // script preserved + selfOut.value > SELF.value && validSelfOutR5 && validSelfOutR4 + + val validDelay = lpBox.R5[Int].get < HEIGHT - T_arb // at least T_arb blocks have passed since the tracking started + val validThreshold = lpRate * 100 > thresholdPercent * oracleRate + + sigmaProp(validDelay && validThreshold && validAmount && validBankBoxInOut && validLpBox && validOracleBox && validSelfOut && validDelta) } ``` + + ## Liquidity Pool Contract ```scala @@ -98,7 +253,8 @@ This implies that a swap is valid only when the oracle pool rate falls below the // This box: (LP box) // R1 (value): X tokens in NanoErgs // R4: How many LP in circulation (long). This can be non-zero when bootstrapping, to consider the initial token burning in UniSwap v2 - // R5: Stores the height where oracle pool rate becomes lower than LP rate. Reset to Long.MaxValue when rate crossed back. Called crossTracker below + // R5: Stores the height where oracle pool rate becomes lower than LP rate. Reset to Long.MaxValue when rate crossed back. Called crossTrackerLow + // R6: Stores the height where oracle pool rate becomes higher than LP rate. Reset to Long.MaxValue when rate crossed back. Called crossTrackerHigh // Tokens(0): LP NFT to uniquely identify NFT box. (Could we possibly do away with this?) // Tokens(1): LP tokens // Tokens(2): Y tokens (Note that X tokens are NanoErgs (the value) @@ -108,14 +264,18 @@ This implies that a swap is valid only when the oracle pool rate falls below the // Token(0): OP NFT to uniquely identify Oracle Pool // constants - val threshold = 3 // error threshold in crossTracker - val feeNum = 3 // 0.3 % + val threshold = 3 // error threshold in crossTrackerLow + val feeNum = 3 // 0.3 % if feeDenom is 1000 val feeDenom = 1000 + + // the value feeNum / feeDenom is the fraction of fee + // for example if feeNum = 3 and feeDenom = 1000 then fee is 0.003 = 0.3% + val minStorageRent = 10000000L // this many number of nanoErgs are going to be permanently locked val successor = OUTPUTS(0) // copy of this box after exchange - val oraclePoolBox = CONTEXT.dataInputs(0) // oracle pool box - val validOraclePoolBox = oraclePoolBox.tokens(0)._1 == fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify oracle pool box + val oracleBox = CONTEXT.dataInputs(0) // oracle pool box + val validOraclePoolBox = oracleBox.tokens(0)._1 == fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify oracle pool box val lpNFT0 = SELF.tokens(0) val reservedLP0 = SELF.tokens(1) @@ -130,8 +290,8 @@ This implies that a swap is valid only when the oracle pool rate falls below the val validSuccessorScript = successor.propositionBytes == SELF.propositionBytes - val preservedLpNFT = lpNFT1 == lpNFT0 - val validLP = reservedLP1._1 == reservedLP0._1 + val preservedLpNFT = lpNFT1 == lpNFT0 + val validLpBox = reservedLP1._1 == reservedLP0._1 val validY = tokenY1._1 == tokenY0._1 val validSupplyLP1 = supplyLP1 >= 0 @@ -145,23 +305,33 @@ This implies that a swap is valid only when the oracle pool rate falls below the val reservesX1 = successor.value val reservesY1 = tokenY1._2 - val oraclePoolRateXY = oraclePoolBox.R4[Long].get + val oracleRateXY = oracleBox.R4[Long].get val lpRateXY0 = reservesX0 / reservesY0 // we can assume that reservesY0 > 0 (since at least one token must exist) val lpRateXY1 = reservesX1 / reservesY1 // we can assume that reservesY1 > 0 (since at least one token must exist) - val isCrossing = (lpRateXY0 - oraclePoolRateXY) * (lpRateXY1 - oraclePoolRateXY) < 0 // if (and only if) oracle pool rate falls in between, then this will be negative + val isCrossing = (lpRateXY0 - oracleRateXY) * (lpRateXY1 - oracleRateXY) < 0 // if (and only if) oracle pool rate falls in between, then this will be negative - val crossTrackerIn = SELF.R5[Int].get - val crossTrackerOut = successor.R5[Int].get + val crossTrackerLowIn = SELF.R5[Int].get + val crossTrackerLowOut = successor.R5[Int].get + + val crossTrackerHighIn = SELF.R6[Int].get + val crossTrackerHighOut = successor.R6[Int].get val validCrossCounter = { if (isCrossing) { - if (lpRateXY1 > oraclePoolRateXY) { - crossTrackerOut >= HEIGHT - threshold - } else crossTrackerOut == 9223372036854775807L - } else crossTrackerOut == crossTrackerIn + if (lpRateXY1 > oracleRateXY) { + crossTrackerLowOut >= HEIGHT - threshold && + crossTrackerHighOut == 9223372036854775807L + } else { + crossTrackerHighOut >= HEIGHT - threshold && + crossTrackerLowOut == 9223372036854775807L + } + } else { + crossTrackerLowOut == crossTrackerLowIn && + crossTrackerHighOut == crossTrackerHighIn + } } - val validRateForRedeemingLP = oraclePoolRateXY > lpRateXY0 * 9 / 10 // lpRate must be >= 0.9 oraclePoolRate // these parameters need to be tweaked + val validRateForRedeemingLP = oracleRateXY > lpRateXY0 * 98 / 100 // lpRate must be >= 0.98 * oracleRate // these parameters need to be tweaked // Do we need above if we also have the tracking contract? val deltaSupplyLP = supplyLP1 - supplyLP0 @@ -201,7 +371,7 @@ This implies that a swap is valid only when the oracle pool rate falls below the validSuccessorScript && validOraclePoolBox && preservedLpNFT && - validLP && + validLpBox && validY && noMoreTokens && validAction && @@ -210,24 +380,28 @@ This implies that a swap is valid only when the oracle pool rate falls below the ) } ``` -## Swapping Contract + +## Intervention Contract ```scala { - val waitingPeriod = 20 // blocks after which a trigger swap event can be completed, provided rate has not crossed oracle pool rate - val emissionNFT = fromBase64("Bho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") + val lastIntervention = SELF.creationInfo._1 + val buffer = 3 // error margin in height + val T = 100 // from paper, gap between two interventions + val T_int = 20 // blocks after which a trigger swap event can be completed, provided rate has not crossed oracle pool rate + val bankNFT = fromBase64("hho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") val lpNFT = fromBase64("Nho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") - val oraclePoolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") + val oracleNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") - val thresholdPercent = 90 // 90% or less value (of LP in terms of OraclePool) will trigger action (ensure less than 100) + val thresholdPercent = 98 // 98% or less value (of LP in terms of OraclePool) will trigger action (ensure less than 100) - val oraclePoolBox = CONTEXT.dataInputs(0) + val oracleBox = CONTEXT.dataInputs(0) val lpBoxIn = INPUTS(0) - val emissionBoxIn = INPUTS(1) + val bankBoxIn = INPUTS(1) val lpBoxOut = OUTPUTS(0) - val emissionBoxOut = OUTPUTS(1) + val bankBoxOut = OUTPUTS(1) val successor = OUTPUTS(2) // SELF should be INPUTS(2) @@ -243,41 +417,45 @@ This implies that a swap is valid only when the oracle pool rate falls below the val lpRateXYIn = reservesXIn / reservesYIn // we can assume that reservesYIn > 0 (since at least one token must exist) val lpRateXYOut = reservesXOut / reservesYOut // we can assume that reservesYOut > 0 (since at least one token must exist) - val oraclePoolRateXY = oraclePoolBox.R4[Long].get + val oracleRateXY = oracleBox.R4[Long].get - val validThreshold = lpRateXYIn * 100 < thresholdPercent * oraclePoolRateXY + val validThreshold = lpRateXYIn * 100 < thresholdPercent * oracleRateXY - val validOraclePoolBox = oraclePoolBox.tokens(0)._1 == oraclePoolNFT + val validOraclePoolBox = oracleBox.tokens(0)._1 == oracleNFT val validLpBox = lpBoxIn.tokens(0)._1 == lpNFT val validSuccessor = successor.propositionBytes == SELF.propositionBytes && successor.tokens == SELF.tokens && - successor.value == SELF.value + successor.value == SELF.value && + successor.creationInfo._1 >= HEIGHT - buffer + + val validBankBoxIn = bankBoxIn.tokens(0)._1 == bankNFT + val validBankBoxOut = bankBoxOut.tokens(0) == bankBoxIn.tokens(0) && + bankBoxOut.tokens(1)._1 == bankBoxIn.tokens(1)._1 - val validEmissionBoxIn = emissionBoxIn.tokens(0)._1 == emissionNFT - val validEmissionBoxOut = emissionBoxOut.tokens(0) == emissionBoxIn.tokens(0) && - emissionBoxOut.tokens(1)._1 == emissionBoxIn.tokens(1)._1 + val validGap = lastIntervention < HEIGHT - T - val deltaEmissionTokens = emissionBoxOut.tokens(1)._2 - emissionBoxIn.tokens(1)._2 - val deltaEmissionErgs = emissionBoxIn.value - emissionBoxOut.value + val deltaBankTokens = bankBoxOut.tokens(1)._2 - bankBoxIn.tokens(1)._2 + val deltaBankErgs = bankBoxIn.value - bankBoxOut.value val deltaLpX = reservesXOut - reservesXIn val deltaLpY = reservesYIn - reservesYOut - val validLpIn = lpBoxIn.R5[Int].get < HEIGHT - waitingPeriod // at least waitingPeriod blocks have passed since the tracking started + val validLpIn = lpBoxIn.R5[Int].get < HEIGHT - T_int // at least T_int blocks have passed since the tracking started val lpRateXYOutTimes100 = lpRateXYOut * 100 - val validSwap = lpRateXYOutTimes100 >= oraclePoolRateXY * 105 && // new rate must be >= 1.05 times oracle rate - lpRateXYOutTimes100 <= oraclePoolRateXY * 110 && // new rate must be <= 1.1 times oracle rate - deltaEmissionErgs <= deltaLpX && // ergs reduced in emission box must be <= ergs gained in LP - deltaEmissionTokens >= deltaLpY && // tokens gained in emission box must be >= tokens reduced in LP - validEmissionBoxIn && - validEmissionBoxOut && + val validSwap = lpRateXYOutTimes100 >= oracleRateXY * 105 && // new rate must be >= 1.05 times oracle rate + lpRateXYOutTimes100 <= oracleRateXY * 110 && // new rate must be <= 1.1 times oracle rate + deltaBankErgs <= deltaLpX && // ergs reduced in bank box must be <= ergs gained in LP + deltaBankTokens >= deltaLpY && // tokens gained in bank box must be >= tokens reduced in LP + validBankBoxIn && + validBankBoxOut && validSuccessor && validLpBox && validOraclePoolBox && validThreshold && - validLpIn + validLpIn && + validGap sigmaProp(validSwap) } From 716ee536f2f44db1537cffa3a86578bf60ab0688 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Thu, 21 Jul 2022 13:41:57 +0530 Subject: [PATCH 8/8] Dexy: Use fixed output indices for copy of self --- eip-0030.md | 330 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 197 insertions(+), 133 deletions(-) diff --git a/eip-0030.md b/eip-0030.md index 3b608861..c8f4f9b9 100644 --- a/eip-0030.md +++ b/eip-0030.md @@ -45,60 +45,76 @@ This implies that a swap is valid only when the oracle pool rate falls below the ## Bank Contract ```scala -{ - // This box: (dexyUSD bank box) emits DexyUsd in exchange for Ergs - // tokens(0): bankNFT identifying the box - // tokens(1): dexyUSD tokens to be emitted - - // Usually bank box will be spent as follows - - // Arbitrage Mint - // Input | Output | Data-Input - // ------------------------------------- - // 0 Bank | Bank | Oracle - // 1 ArbitrageMint | ArbitrageMint | LP - - // Free Mint - // Input | Output | Data-Input - // ------------------------------------- - // 0 Bank | Bank | Oracle - // 1 FreeMint | FreeMit | LP - - // Intervention - // Input | Output | Data-Input - // ------------------------------------- - // 0 LP | LP | Oracle - // 1 Bank | Bank | - - val selfOutIndex = getVar[Int](0).get - - val interventionNFT = fromBase64("Fho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify intervention box for future use - val freeMintNFT = fromBase64("Bho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") - val arbitrageMintNFT = fromBase64("lho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") - - val selfOut = OUTPUTS(selfOutIndex) - val validSelfOut = selfOut.tokens(0) == SELF.tokens(0) && // bankNFT and quantity preserved - selfOut.propositionBytes == SELF.propositionBytes && // script preserved - selfOut.tokens(1)._1 == SELF.tokens(1)._1 // dexyUSD tokenId preserved - - val validMint = INPUTS(1).tokens(0)._1 == freeMintNFT || - INPUTS(1).tokens(0)._1 == arbitrageMintNFT - - val validIntervention = INPUTS(2).tokens(0)._1 == interventionNFT - - sigmaProp(validSelfOut && (validMint || validIntervention)) +{ + // This box: (dexyUSD bank box) + // tokens(0): bankNFT identifying the box + // tokens(1): dexyUSD tokens to be emitted + + // Bank box will be spent as follows + // Arbitrage Mint + // Input | Output | Data-Input + // ------------------------------------------------ + // 0 ArbitrageMint | ArbitrageMint | Oracle + // 1 Bank | Bank | LP + + // Free Mint + // Input | Output | Data-Input + // ------------------------------------- + // 0 FreeMint | FreeMint | Oracle + // 1 Bank | Bank | LP + + // Intervention + // Input | Output | Data-Input + // ----------------------------------------------- + // 0 LP | LP | Oracle + // 1 Bank | Bank | + // 2 Intervention | Intervention | + + val selfOutIndex = 1 // 2nd output is self copy + val mintInIndex = 0 // 1st input is mint or LP box + val interventionInIndex = 2 // 3rd input is intervention box + + val interventionNFT = fromBase64("Fho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify intervention box for future use + val freeMintNFT = fromBase64("Bho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") + val arbitrageMintNFT = fromBase64("lho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") + + val selfOut = OUTPUTS(selfOutIndex) + val validSelfOut = selfOut.tokens(0) == SELF.tokens(0) && // bankNFT and quantity preserved + selfOut.propositionBytes == SELF.propositionBytes && // script preserved + selfOut.tokens(1)._1 == SELF.tokens(1)._1 // dexyUSD tokenId preserved + + val validMint = INPUTS(mintInIndex).tokens(0)._1 == freeMintNFT || + INPUTS(mintInIndex).tokens(0)._1 == arbitrageMintNFT + + val validIntervention = INPUTS(interventionInIndex).tokens(0)._1 == interventionNFT + + sigmaProp(validSelfOut && (validMint || validIntervention)) } ``` ## Free Mint Contract ```scala -{ // +{ // ToDo: Add fee + // // this box: (free-mint box) // tokens(0): Free-mint NFT // // R4: (Int) height at which counter will reset // R5: (Long) remaining stablecoins available to be purchased before counter is reset + // Free Mint box will be spent as follows: + // Free Mint + // Input | Output | Data-Input + // ------------------------------------- + // 0 FreeMint | FreeMint | Oracle + // 1 Bank | Bank | LP + + val bankInIndex = 1 + val selfOutIndex = 0 + val bankOutIndex = 1 + val oracleBoxIndex = 0 + val lpBoxIndex = 1 + val oracleNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify oracle pool box val bankNFT = fromBase64("hho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") val lpNFT = fromBase64("Nho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") @@ -109,13 +125,10 @@ This implies that a swap is valid only when the oracle pool rate falls below the // actual fee ratio is feeNum / feeDenom // example if feeNum = 10 and feeDenom = 1000 then fee = 0.01 = 1 % - val oracleBox = CONTEXT.dataInputs(0) // oracle-pool (v1 and v2) box containing rate in R4 - val lpBox = CONTEXT.dataInputs(1) - val bankBoxIn = INPUTS(0) + val oracleBox = CONTEXT.dataInputs(oracleBoxIndex) // oracle-pool (v1 and v2) box containing rate in R4 + val lpBox = CONTEXT.dataInputs(lpBoxIndex) + val bankBoxIn = INPUTS(bankInIndex) - val selfOutIndex = getVar[Int](0).get - val bankOutIndex = getVar[Int](1).get - val selfOut = OUTPUTS(selfOutIndex) val bankBoxOut = OUTPUTS(bankOutIndex) @@ -158,17 +171,32 @@ This implies that a swap is valid only when the oracle pool rate falls below the sigmaProp(validAmount && validBankBoxInOut && validLpBox && validOracleBox && validSelfOut && validDelta && validRateFreeMint) } + ``` ## Arbitrage Mint Contract ```scala { + // this box: (arbitrage-mint box) // tokens(0): Arbitrage-mint NFT // // R4: (Int) height at which counter will reset // R5: (Long) remaining stablecoins available to be purchased before counter is reset + // Arbitrage Mint box will be spent as follows + // Arbitrage Mint + // Input | Output | Data-Input + // ------------------------------------------------ + // 0 ArbitrageMint | ArbitrageMint | Oracle + // 1 Bank | Bank | LP + + val bankInIndex = 1 + val selfOutIndex = 0 + val bankOutIndex = 1 + val oracleBoxIndex = 0 + val lpBoxIndex = 1 + val oracleNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify oracle pool box val bankNFT = fromBase64("hho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") val lpNFT = fromBase64("Nho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") @@ -180,13 +208,10 @@ This implies that a swap is valid only when the oracle pool rate falls below the // actual fee ratio is feeNum / feeDenom // example if feeNum = 5 and feeDenom = 1000 then fee = 0.005 = 0.5 % - val oracleBox = CONTEXT.dataInputs(0) // oracle-pool (v1 and v2) box containing rate in R4 - val lpBox = CONTEXT.dataInputs(1) - val bankBoxIn = INPUTS(0) + val oracleBox = CONTEXT.dataInputs(oracleBoxIndex) // oracle-pool (v1 and v2) box containing rate in R4 + val lpBox = CONTEXT.dataInputs(lpBoxIndex) + val bankBoxIn = INPUTS(bankInIndex) - val selfOutIndex = getVar[Int](0).get - val bankOutIndex = getVar[Int](1).get - val selfOut = OUTPUTS(selfOutIndex) val bankBoxOut = OUTPUTS(bankOutIndex) @@ -238,10 +263,7 @@ This implies that a swap is valid only when the oracle pool rate falls below the sigmaProp(validDelay && validThreshold && validAmount && validBankBoxInOut && validLpBox && validOracleBox && validSelfOut && validDelta) } ``` - - ## Liquidity Pool Contract - ```scala { // Notation: @@ -262,6 +284,32 @@ This implies that a swap is valid only when the oracle pool rate falls below the // Data Input #0: (oracle pool box) // R4: Rate in units of X per unit of Y // Token(0): OP NFT to uniquely identify Oracle Pool + + // LP box will be spent as follows + // Intervention + // Input | Output | Data-Input + // ----------------------------------------------- + // 0 LP | LP | Oracle + // 1 Bank | Bank | + // 2 Intervention | Intervention | + + // Swap + // Input | Output | Data-Input + // ----------------------------------------------- + // 0 LP | LP | Oracle + + // Redeem LP tokens + // Input | Output | Data-Input + // ----------------------------------------------- + // 0 LP | LP | Oracle + + // Mint LP tokens + // Input | Output | Data-Input + // ----------------------------------------------- + // 0 LP | LP | Oracle + + val selfOutIndex = 0 + val oracleBoxIndex = 0 // constants val threshold = 3 // error threshold in crossTrackerLow @@ -273,8 +321,8 @@ This implies that a swap is valid only when the oracle pool rate falls below the val minStorageRent = 10000000L // this many number of nanoErgs are going to be permanently locked - val successor = OUTPUTS(0) // copy of this box after exchange - val oracleBox = CONTEXT.dataInputs(0) // oracle pool box + val successor = OUTPUTS(selfOutIndex) // copy of this box after exchange + val oracleBox = CONTEXT.dataInputs(oracleBoxIndex) // oracle pool box val validOraclePoolBox = oracleBox.tokens(0)._1 == fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // to identify oracle pool box val lpNFT0 = SELF.tokens(0) @@ -384,79 +432,95 @@ This implies that a swap is valid only when the oracle pool rate falls below the ## Intervention Contract ```scala -{ - val lastIntervention = SELF.creationInfo._1 - val buffer = 3 // error margin in height - val T = 100 // from paper, gap between two interventions - val T_int = 20 // blocks after which a trigger swap event can be completed, provided rate has not crossed oracle pool rate - val bankNFT = fromBase64("hho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") - val lpNFT = fromBase64("Nho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") - val oracleNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") - - val thresholdPercent = 98 // 98% or less value (of LP in terms of OraclePool) will trigger action (ensure less than 100) - - val oracleBox = CONTEXT.dataInputs(0) - - val lpBoxIn = INPUTS(0) - val bankBoxIn = INPUTS(1) - - val lpBoxOut = OUTPUTS(0) - val bankBoxOut = OUTPUTS(1) - - val successor = OUTPUTS(2) // SELF should be INPUTS(2) - - val tokenYIn = lpBoxIn.tokens(2) - val tokenYOut = lpBoxOut.tokens(2) - - val reservesXIn = lpBoxIn.value - val reservesYIn = tokenYIn._2 - - val reservesXOut = lpBoxOut.value - val reservesYOut = tokenYOut._2 - - val lpRateXYIn = reservesXIn / reservesYIn // we can assume that reservesYIn > 0 (since at least one token must exist) - val lpRateXYOut = reservesXOut / reservesYOut // we can assume that reservesYOut > 0 (since at least one token must exist) - - val oracleRateXY = oracleBox.R4[Long].get - - val validThreshold = lpRateXYIn * 100 < thresholdPercent * oracleRateXY - - val validOraclePoolBox = oracleBox.tokens(0)._1 == oracleNFT - val validLpBox = lpBoxIn.tokens(0)._1 == lpNFT - - val validSuccessor = successor.propositionBytes == SELF.propositionBytes && - successor.tokens == SELF.tokens && - successor.value == SELF.value && - successor.creationInfo._1 >= HEIGHT - buffer - - val validBankBoxIn = bankBoxIn.tokens(0)._1 == bankNFT - val validBankBoxOut = bankBoxOut.tokens(0) == bankBoxIn.tokens(0) && - bankBoxOut.tokens(1)._1 == bankBoxIn.tokens(1)._1 - - val validGap = lastIntervention < HEIGHT - T - - val deltaBankTokens = bankBoxOut.tokens(1)._2 - bankBoxIn.tokens(1)._2 - val deltaBankErgs = bankBoxIn.value - bankBoxOut.value - val deltaLpX = reservesXOut - reservesXIn - val deltaLpY = reservesYIn - reservesYOut - - val validLpIn = lpBoxIn.R5[Int].get < HEIGHT - T_int // at least T_int blocks have passed since the tracking started - - val lpRateXYOutTimes100 = lpRateXYOut * 100 - - val validSwap = lpRateXYOutTimes100 >= oracleRateXY * 105 && // new rate must be >= 1.05 times oracle rate - lpRateXYOutTimes100 <= oracleRateXY * 110 && // new rate must be <= 1.1 times oracle rate - deltaBankErgs <= deltaLpX && // ergs reduced in bank box must be <= ergs gained in LP - deltaBankTokens >= deltaLpY && // tokens gained in bank box must be >= tokens reduced in LP - validBankBoxIn && - validBankBoxOut && - validSuccessor && - validLpBox && - validOraclePoolBox && - validThreshold && - validLpIn && - validGap +{ + + // Intervention box will be spent as follows + // Intervention + // Input | Output | Data-Input + // ----------------------------------------------- + // 0 LP | LP | Oracle + // 1 Bank | Bank | + // 2 Intervention | Intervention | + + val lpInIndex = 0 + val lpOutIndex = 0 + val bankInIndex = 1 + val bankOutIndex = 1 + val selfOutIndex = 2 // SELF should be third input + val oracleBoxIndex = 0 + + val lastIntervention = SELF.creationInfo._1 + val buffer = 3 // error margin in height + val T = 100 // from paper, gap between two interventions + val T_int = 20 // blocks after which a trigger swap event can be completed, provided rate has not crossed oracle pool rate + val bankNFT = fromBase64("hho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") + val lpNFT = fromBase64("Nho6UlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") + val oracleNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") + + val thresholdPercent = 98 // 98% or less value (of LP in terms of OraclePool) will trigger action (ensure less than 100) + + val oracleBox = CONTEXT.dataInputs(oracleBoxIndex) + + val lpBoxIn = INPUTS(lpInIndex) + val bankBoxIn = INPUTS(bankInIndex) - sigmaProp(validSwap) + val lpBoxOut = OUTPUTS(lpOutIndex) + val bankBoxOut = OUTPUTS(bankOutIndex) + + val successor = OUTPUTS(selfOutIndex) + + val tokenYIn = lpBoxIn.tokens(2) + val tokenYOut = lpBoxOut.tokens(2) + + val reservesXIn = lpBoxIn.value + val reservesYIn = tokenYIn._2 + + val reservesXOut = lpBoxOut.value + val reservesYOut = tokenYOut._2 + + val lpRateXYIn = reservesXIn / reservesYIn // we can assume that reservesYIn > 0 (since at least one token must exist) + val lpRateXYOut = reservesXOut / reservesYOut // we can assume that reservesYOut > 0 (since at least one token must exist) + + val oracleRateXY = oracleBox.R4[Long].get + + val validThreshold = lpRateXYIn * 100 < thresholdPercent * oracleRateXY + + val validOraclePoolBox = oracleBox.tokens(0)._1 == oracleNFT + val validLpBox = lpBoxIn.tokens(0)._1 == lpNFT + + val validSuccessor = successor.propositionBytes == SELF.propositionBytes && + successor.tokens == SELF.tokens && + successor.value == SELF.value && + successor.creationInfo._1 >= HEIGHT - buffer + + val validBankBoxIn = bankBoxIn.tokens(0)._1 == bankNFT + val validBankBoxOut = bankBoxOut.tokens(0) == bankBoxIn.tokens(0) && + bankBoxOut.tokens(1)._1 == bankBoxIn.tokens(1)._1 + + val validGap = lastIntervention < HEIGHT - T + + val deltaBankTokens = bankBoxOut.tokens(1)._2 - bankBoxIn.tokens(1)._2 + val deltaBankErgs = bankBoxIn.value - bankBoxOut.value + val deltaLpX = reservesXOut - reservesXIn + val deltaLpY = reservesYIn - reservesYOut + + val validLpIn = lpBoxIn.R5[Int].get < HEIGHT - T_int // at least T_int blocks have passed since the tracking started + + val lpRateXYOutTimes100 = lpRateXYOut * 100 + + val validSwap = lpRateXYOutTimes100 >= oracleRateXY * 105 && // new rate must be >= 1.05 times oracle rate + lpRateXYOutTimes100 <= oracleRateXY * 110 && // new rate must be <= 1.1 times oracle rate + deltaBankErgs <= deltaLpX && // ergs reduced in bank box must be <= ergs gained in LP + deltaBankTokens >= deltaLpY && // tokens gained in bank box must be >= tokens reduced in LP + validBankBoxIn && + validBankBoxOut && + validSuccessor && + validLpBox && + validOraclePoolBox && + validThreshold && + validLpIn && + validGap + + sigmaProp(validSwap) } ``` \ No newline at end of file