From bcee5ab356c8002630d9eb7207fe7979d8622dbc Mon Sep 17 00:00:00 2001 From: ScalaHub <23208922+scalahub@users.noreply.github.com> Date: Wed, 14 Jul 2021 02:21:58 +0530 Subject: [PATCH 1/5] Update eip-0014.md --- eip-0014.md | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/eip-0014.md b/eip-0014.md index 79b0cd71..0d3d59f7 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -510,3 +510,193 @@ PoolNFT | Coll[Byte] | Pool NFT ID sigmaProp(Pk || (validPoolIn && validReturnOut)) } ``` + +### Ergo AMM DEX Contracts [Ergo to Token] + +The Ergo-to-token exchange is essentially the native-to-token (N2T) contract of UniSwap 1.0. +There are two approaches to create a N2T exchange: +1. Use a T2T (token-to-token) exchange, where one of the tokens maps to Ergs and have a separate dApp that exchanges Ergs to tokens at 1:1 rate. +2. Implement N2T directly in the exchange contract. Here we use this approach. + +The N2T AMM DEX relies on two types of contracts as before: + +- Pool contracts +- Swap contracts + +#### Pool contracts + +The following is the modified pool contract representing a Liquidity Pool of the N2T AMM DEX. +As before, the pool contract ensures the following operations are performed according to protocol rules: + +- Depositing. An amount of LP tokens taken from LP reserves is proportional to an amount of underlying assets deposited. `LP = min(X_deposited * LP_supply / X_reserved, Y_deposited * LP_supply / Y_reserved)` +- Redemption. Amounts of underlying assets redeemed are proportional to an amount of LP tokens returned. `X_redeemed = LP_returned * X_reserved / LP_supply`, `Y_redeemed = LP_returned * Y_reserved / LP_supply` +- Swap. Tokens are exchanged at a price corresponding to a relation of a pair’s reserve balances while preserving constant product constraint (`CP = X_reserved * Y_reserved`). Correct amount of protocol fees is paid (0.03% currently). `X_output = X_reserved * Y_input * 997 / (Y_reserved * 1000 + Y_input * 997)` + +Variables: +- `X_deposited` - Amount of the first asset (nanoErgs) being deposited to the pool box +- `Y_deposited` - Amount of the second asset being deposited to the pool box +- `X_reserved` - Amount of the first asset (nanoErgs) locked in the pool box +- `Y_reserved` - Amount of the second asset locked in the pool box +- `LP_supply` - LP tokens circulating supply corresponding to the pool box + +#### Schema of the pool UTXO + +Section | Description +----------|------------------------------------------------------ +value | Asset X reserves (nanoErgs) +tokens[0] | Pool NFT +tokens[1] | Locked LP tokens +tokens[2] | Asset Y reserves +R4[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num). This represents the *non-fee* part of the sold asset + +#### Pool contract + +```scala +{ + val InitiallyLockedLP = 0x7fffffffffffffffL + val FeeDenom = 1000 + val minStorageRent = 10000000L // this many number of nanoErgs are going to be permanently locked + + val poolNFT0 = SELF.tokens(0) + val reservedLP0 = SELF.tokens(1) + val tokenY0 = SELF.tokens(2) + + val successor = OUTPUTS(0) + + val feeNum0 = SELF.R4[Long].get + val feeNum1 = successor.R4[Long].get + + val poolNFT1 = successor.tokens(0) + val reservedLP1 = successor.tokens(1) + val tokenY1 = successor.tokens(2) + + val validSuccessorScript = successor.propositionBytes == SELF.propositionBytes + val preservedFeeConfig = feeNum1 == feeNum0 + + val preservedPoolNFT = poolNFT1 == poolNFT0 + val validLP = reservedLP1._1 == reservedLP0._1 + val validY = tokenY1._1 == tokenY0._1 + // 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 supplyLP0 = InitiallyLockedLP - reservedLP0._2 + val supplyLP1 = InitiallyLockedLP - reservedLP1._2 + + val reservesX0 = SELF.value + val reservesY0 = tokenY0._2 + val reservesX1 = successor.value + val reservesY1 = tokenY1._2 + + val deltaSupplyLP = supplyLP1 - supplyLP0 + val deltaReservesX = reservesX1 - reservesX0 + val deltaReservesY = reservesY1 - reservesY0 + + val validDepositing = { + val sharesUnlocked = min( + deltaReservesX.toBigInt * supplyLP0 / reservesX0, + deltaReservesY.toBigInt * supplyLP0 / reservesY0 + ) + deltaSupplyLP <= sharesUnlocked + } + + val validRedemption = { + val _deltaSupplyLP = deltaSupplyLP.toBigInt + // note: _deltaSupplyLP and deltaReservesX, deltaReservesY are negative + deltaReservesX.toBigInt * supplyLP0 >= _deltaSupplyLP * reservesX0 && deltaReservesY.toBigInt * supplyLP0 >= _deltaSupplyLP * reservesY0 + } + + val validSwap = + if (deltaReservesX > 0) + reservesY0.toBigInt * deltaReservesX * feeNum0 >= -deltaReservesY * (reservesX0.toBigInt * FeeDenom + deltaReservesX * feeNum0) + else + reservesX0.toBigInt * deltaReservesY * feeNum0 >= -deltaReservesX * (reservesY0.toBigInt * FeeDenom + deltaReservesY * feeNum0) + + val validAction = + if (deltaSupplyLP == 0) + validSwap + else + if (deltaReservesX > 0 && deltaReservesY > 0) validDepositing + else validRedemption + + sigmaProp( + validSuccessorScript && + preservedFeeConfig && + preservedPoolNFT && + validLP && + validY && + noMoreTokens && + validAction && + validStorageRent + ) +} +``` + +#### Swap proxy-contract + +Swap contract ensures a swap is executed fairly from a user's perspective. The contract checks that: +* Assets are swapped at actual price derived from pool reserves. `X_output = X_reserved * Y_input * fee_num / (Y_reserved * 1000 + Y_input * fee_num)` +* Fair amount of DEX fee held in ERGs. `F = X_output * F_per_token` +* A minimal amount of quote asset received as an output in order to prevent front-running attacks. + +Once published swap contracts are tracked and executed by ErgoDEX bots automatically. +Until a swap is executed, it can be cancelled by a user who created it by simply spending the swap UTXO. + +##### Sell Ergs +###### Contract parameters +Constant | Type | Description +--------------------|------------|--------------- +Pk | ProveDLog | User PublicKey +FeeNum | Long | Pool fee numerator (must taken from pool params) +QuoteId | Coll[Byte] | Quote asset ID. This is the asset we are buying from the pool +MinQuoteAmount | Long | Minimal amount of quote asset +SellAmount | Long | The amount of nanoErgs to sell +DexFeePerTokenNum | Long | Numerator of the DEX fee in nanoERGs per one unit of quote asset +DexFeePerTokenDenom | Long | Denominator of the DEX fee in nanoERGs per one unit of quote asset +PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a concrete unique pool) + +```scala +{ // contract to sell Ergs and buy Token + val FeeDenom = 1000 + + val poolInput = INPUTS(0) + val poolNFT = poolInput.tokens(0)._1 + + val poolY_token = poolInput.tokens(2) + val poolY_tokenId = poolY_token._1 + + val poolReservesX = poolInput.value + val poolReservesY = poolY_token._2 + val validPoolInput = poolNFT == PoolNFT && poolY_tokenId == QuoteId + + val validTrade = + OUTPUTS.exists { (box: Box) => // box containing the purchased tokens and balance of Ergs + val quoteAsset = box.tokens(0) + + val quoteAssetID = quoteAsset._1 + val quoteAssetAmount = quoteAsset._2 + + val fairDexFee = box.value >= SELF.value - quoteAssetAmount * DexFeePerTokenNum / DexFeePerTokenDenom - SellAmount + + val relaxedOutput = quoteAssetAmount + 1 // handle rounding loss + val fairPrice = poolReservesY.toBigInt * SellAmount * FeeNum <= relaxedOutput * (poolReservesX.toBigInt * FeeDenom + SellAmount * FeeNum) + + val uniqueOutput = INPUTS(box.R4[Int].get).id == SELF.id // See https://www.ergoforum.org/t/ergoscript-design-patterns/222/15?u=scalahub + + + box.propositionBytes == Pk.propBytes && + quoteAssetID == QuoteId && + quoteAssetAmount >= MinQuoteAmount && + fairDexFee && + fairPrice && + uniqueOutput // prevent multiple input boxes with same script mapping to one single output box. + } + + sigmaProp(Pk || (validPoolInput && validTrade)) +} +``` + +##### Sell Tokens + +[...] From ca679c33a70b7754a16a4603e4857d4e7270a2c9 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Wed, 14 Jul 2021 18:13:18 +0530 Subject: [PATCH 2/5] Add N2T swap contract for selling tokens --- eip-0014.md | 72 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 0d3d59f7..5d0adb7f 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -672,17 +672,17 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a val validTrade = OUTPUTS.exists { (box: Box) => // box containing the purchased tokens and balance of Ergs - val quoteAsset = box.tokens(0) - - val quoteAssetID = quoteAsset._1 - val quoteAssetAmount = quoteAsset._2 - - val fairDexFee = box.value >= SELF.value - quoteAssetAmount * DexFeePerTokenNum / DexFeePerTokenDenom - SellAmount - - val relaxedOutput = quoteAssetAmount + 1 // handle rounding loss - val fairPrice = poolReservesY.toBigInt * SellAmount * FeeNum <= relaxedOutput * (poolReservesX.toBigInt * FeeDenom + SellAmount * FeeNum) - - val uniqueOutput = INPUTS(box.R4[Int].get).id == SELF.id // See https://www.ergoforum.org/t/ergoscript-design-patterns/222/15?u=scalahub + val quoteAsset = box.tokens(0) + + val quoteAssetID = quoteAsset._1 + val quoteAssetAmount = quoteAsset._2 + + val fairDexFee = box.value >= SELF.value - quoteAssetAmount * DexFeePerTokenNum / DexFeePerTokenDenom - SellAmount + + val relaxedOutput = quoteAssetAmount + 1 // handle rounding loss + val fairPrice = poolReservesY.toBigInt * SellAmount * FeeNum <= relaxedOutput * (poolReservesX.toBigInt * FeeDenom + SellAmount * FeeNum) + + val uniqueOutput = INPUTS(box.R4[Int].get).id == SELF.id // See https://www.ergoforum.org/t/ergoscript-design-patterns/222/15?u=scalahub box.propositionBytes == Pk.propBytes && @@ -690,7 +690,7 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a quoteAssetAmount >= MinQuoteAmount && fairDexFee && fairPrice && - uniqueOutput // prevent multiple input boxes with same script mapping to one single output box. + uniqueOutput // prevent multiple input boxes with same script mapping to one single output box } sigmaProp(Pk || (validPoolInput && validTrade)) @@ -699,4 +699,50 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a ##### Sell Tokens -[...] +###### Contract parameters: +Constant | Type | Description +--------------------|------------|--------------- +Pk | ProveDLog | User PublicKey +FeeNum | Long | Pool fee numerator (must taken from pool params) +MinQuoteAmount | Long | Minimal amount of quote asset +DexFeePerTokenNum | Long | Numerator of the DEX fee in nanoERGs per one unit of quote asset +DexFeePerTokenDenom | Long | Denominator of the DEX fee in nanoERGs per one unit of quote asset +PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a concrete unique pool) + +```scala +{ // contract to sell tokens and buy Ergs + val FeeDenom = 1000 + + val sellToken = SELF.tokens(0) + val sellTokenId = sellToken._1 + val sellAmount = sellToken._2 + + val poolInput = INPUTS(0) + val poolNFT = poolInput.tokens(0)._1 + + val poolY_token = poolInput.tokens(2) + val poolY_tokenId = poolY_token._1 + + val poolReservesX = poolInput.value + val poolReservesY = poolY_token._2 + val validPoolInput = poolNFT == PoolNFT && poolY_tokenId == sellTokenId + + val validTrade = + OUTPUTS.exists { (box: Box) => // box containing the purchased tokens and balance of Ergs + // bought nanoErgs are called quoteAssetAmount + val deltaNanoErgs = box.value - SELF.value // this is quoteAssetAmount - fee + val quoteAssetAmount = deltaNanoErgs * DexFeePerTokenDenom / (DexFeePerTokenDenom - DexFeePerTokenNum) + val relaxedOutput = quoteAssetAmount + 1 // handle rounding loss + val fairPrice = poolReservesX.toBigInt * sellAmount * FeeNum <= relaxedOutput * (poolReservesY.toBigInt * FeeDenom + sellAmount * FeeNum) + + val uniqueOutput = INPUTS(box.R4[Int].get).id == SELF.id // See https://www.ergoforum.org/t/ergoscript-design-patterns/222/15?u=scalahub + + box.propositionBytes == Pk.propBytes && + quoteAssetAmount >= MinQuoteAmount && + fairPrice && + uniqueOutput // prevent multiple input boxes with same script mapping to one single output box + } + + sigmaProp(Pk || (validPoolInput && validTrade)) +} +``` From 4c92df84c1e3076505665b37f0819aa10cf85108 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Fri, 16 Jul 2021 00:38:25 +0530 Subject: [PATCH 3/5] Use same terminology as T2T contract Remove reference to UniSwap 1.0 --- eip-0014.md | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 5d0adb7f..fdf55062 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -513,16 +513,11 @@ PoolNFT | Coll[Byte] | Pool NFT ID ### Ergo AMM DEX Contracts [Ergo to Token] -The Ergo-to-token exchange is essentially the native-to-token (N2T) contract of UniSwap 1.0. +The Ergo-to-token or the native-to-token (N2T) exchange is an exchange between Ergo's native token (nanoErgs) and some other token. There are two approaches to create a N2T exchange: 1. Use a T2T (token-to-token) exchange, where one of the tokens maps to Ergs and have a separate dApp that exchanges Ergs to tokens at 1:1 rate. 2. Implement N2T directly in the exchange contract. Here we use this approach. -The N2T AMM DEX relies on two types of contracts as before: - -- Pool contracts -- Swap contracts - #### Pool contracts The following is the modified pool contract representing a Liquidity Pool of the N2T AMM DEX. @@ -651,7 +646,7 @@ Pk | ProveDLog | User PublicKey FeeNum | Long | Pool fee numerator (must taken from pool params) QuoteId | Coll[Byte] | Quote asset ID. This is the asset we are buying from the pool MinQuoteAmount | Long | Minimal amount of quote asset -SellAmount | Long | The amount of nanoErgs to sell +BaseAmount | Long | The amount of nanoErgs to sell DexFeePerTokenNum | Long | Numerator of the DEX fee in nanoERGs per one unit of quote asset DexFeePerTokenDenom | Long | Denominator of the DEX fee in nanoERGs per one unit of quote asset PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a concrete unique pool) @@ -677,10 +672,10 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a val quoteAssetID = quoteAsset._1 val quoteAssetAmount = quoteAsset._2 - val fairDexFee = box.value >= SELF.value - quoteAssetAmount * DexFeePerTokenNum / DexFeePerTokenDenom - SellAmount + val fairDexFee = box.value >= SELF.value - quoteAssetAmount * DexFeePerTokenNum / DexFeePerTokenDenom - BaseAmount val relaxedOutput = quoteAssetAmount + 1 // handle rounding loss - val fairPrice = poolReservesY.toBigInt * SellAmount * FeeNum <= relaxedOutput * (poolReservesX.toBigInt * FeeDenom + SellAmount * FeeNum) + val fairPrice = poolReservesY.toBigInt * BaseAmount * FeeNum <= relaxedOutput * (poolReservesX.toBigInt * FeeDenom + BaseAmount * FeeNum) val uniqueOutput = INPUTS(box.R4[Int].get).id == SELF.id // See https://www.ergoforum.org/t/ergoscript-design-patterns/222/15?u=scalahub @@ -713,9 +708,9 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a { // contract to sell tokens and buy Ergs val FeeDenom = 1000 - val sellToken = SELF.tokens(0) - val sellTokenId = sellToken._1 - val sellAmount = sellToken._2 + val baseToken = SELF.tokens(0) // token being sold + val baseTokenId = baseToken._1 + val baseAmount = baseToken._2 val poolInput = INPUTS(0) val poolNFT = poolInput.tokens(0)._1 @@ -725,7 +720,7 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a val poolReservesX = poolInput.value val poolReservesY = poolY_token._2 - val validPoolInput = poolNFT == PoolNFT && poolY_tokenId == sellTokenId + val validPoolInput = poolNFT == PoolNFT && poolY_tokenId == baseTokenId val validTrade = OUTPUTS.exists { (box: Box) => // box containing the purchased tokens and balance of Ergs @@ -733,7 +728,7 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a val deltaNanoErgs = box.value - SELF.value // this is quoteAssetAmount - fee val quoteAssetAmount = deltaNanoErgs * DexFeePerTokenDenom / (DexFeePerTokenDenom - DexFeePerTokenNum) val relaxedOutput = quoteAssetAmount + 1 // handle rounding loss - val fairPrice = poolReservesX.toBigInt * sellAmount * FeeNum <= relaxedOutput * (poolReservesY.toBigInt * FeeDenom + sellAmount * FeeNum) + val fairPrice = poolReservesX.toBigInt * baseAmount * FeeNum <= relaxedOutput * (poolReservesY.toBigInt * FeeDenom + baseAmount * FeeNum) val uniqueOutput = INPUTS(box.R4[Int].get).id == SELF.id // See https://www.ergoforum.org/t/ergoscript-design-patterns/222/15?u=scalahub From 3429eb8c64b2a18190a4aa2130203e94791f3e21 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Fri, 16 Jul 2021 00:38:25 +0530 Subject: [PATCH 4/5] Use same terminology as T2T contract Remove reference to UniSwap 1.0 --- eip-0014.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eip-0014.md b/eip-0014.md index fdf55062..9bba8b3d 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -1,6 +1,6 @@ # Automated Decentralized Exchange -* Author: kushti, Ilya Oskin +* Authors: kushti, Ilya Oskin. scalahub * Status: Proposed * Created: 12-Mar-2021 * Last edited: 31-May-2021 From 520f05f068f839ca8fd5b61c7216bff63fbe6722 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Fri, 16 Jul 2021 09:58:36 +0530 Subject: [PATCH 5/5] Fix incorrect fee multiplier numerator example --- eip-0014.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eip-0014.md b/eip-0014.md index 9bba8b3d..228fd13b 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -542,7 +542,7 @@ value | Asset X reserves (nanoErgs) tokens[0] | Pool NFT tokens[1] | Locked LP tokens tokens[2] | Asset Y reserves -R4[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num). This represents the *non-fee* part of the sold asset +R4[Long] | Fee multiplier numerator (e.g. 0.3% fee -> 997 fee_num). This represents the *non-fee* part of the sold asset #### Pool contract