From 7dfae5db6c456f735283d7c6923750da05e951a1 Mon Sep 17 00:00:00 2001 From: Alex Chepurnoy Date: Mon, 15 Mar 2021 19:10:34 +0300 Subject: [PATCH 01/60] initial EIP-14 --- eip-0014.md | 214 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 eip-0014.md diff --git a/eip-0014.md b/eip-0014.md new file mode 100644 index 00000000..1880cb2c --- /dev/null +++ b/eip-0014.md @@ -0,0 +1,214 @@ +Decentralized Exchange Contracts Standard +========================================= + +* Author: kushti, Ilya Oskin +* Status: Proposed +* Created: 12-Mar-2021 +* Last edited: 12-Mar-2021 +* License: CC0 +* Track: Standards + +Motivation +---------- + +Act of exchange without trusted parties is a most basic primitive for decentralized finance on top of blockchains. Thus contracts for that were introduced early, and basic single-chain swap contract was introduced early in the [ErgoScript whitepaper](https://ergoplatform.org/docs/ErgoScript.pdf). Then a lot of other order contracts appeared: with partial filling, buyback guarantee and so on. What is good for traders in decentralized worlds, such contracts are usually composable. +While swap order contracts allows for orderbook-based decentralized exchanges (DEXes), now popular AMM-based DEXes (where AMM stands for Automated Market Maker) also possible on Ergo. +Interestingly, unlike other known blockchains, thanks to the extended UTXO model, liquidity pool contracts for AMM-based DEXes can be combined with order contracts (for orderbook-based DEXes). This gives unique possibility to have shared liquidity among different types of exchanges on top of the Ergo blockchain. + +This PR provides known DEX contracts for both orderbook-based and AMM-based DEXes, and also provides info on their composability. + + +Order Contracts +--------------- + +Order contracts are waiting for another orders to be matched, or for a refund command. Orders can be buy (i.e. buy tokens with ERG), sell (i.e. sell tokens for ERG), or swap (buy non-ERG tokens with other non-ERG tokens) orders. + +* simplest orders (from the ErgoScript whitepaper) + + Buy order: + + (HEIGHT > deadline && pkA) || { + val tokenData = OUTPUTS(0).R2[Coll[(Coll[Byte], Long)]].get(0) + allOf(Coll( + tokenData._1 == token1,tokenData._2 >= 60L, + OUTPUTS(0).propositionBytes == pkA.propBytes, + OUTPUTS(0).R4[Coll[Byte]].get == SELF.id + )) + } + + Sell order: + + (HEIGHT > deadline && pkB) || + allOf(Coll( + OUTPUTS(1).value >= 100L, + OUTPUTS(1).propositionBytes == pkB.propBytes, + OUTPUTS(1).R4[Coll[Byte]].get == SELF.id + )) + + +* simple swap order ([by Jason Davies](https://blog.plutomonkey.com/2021/01/generic-on-chain-ergo-swaps/)) + + Buy order: + + { + val user_pk = proveDlog(recipient); + val deadline = SELF.creationInfo._1 + 30; + + val erg_amount = SELF.value - fee; + val token_amount = erg_amount * rate / divisor; + + val valid_height = HEIGHT < deadline; + + sigmaProp(OUTPUTS.exists({ (box: Box) => + allOf(Coll( + if (valid_height) { + val t = box.tokens(0); + t._1 == token_id && + t._2 >= token_amount + } else { + // refund + box.value >= erg_amount + }, + box.R4[Coll[Byte]].get == SELF.id, + box.propositionBytes == user_pk.propBytes + )) + })) + } + + + Sell order: + + { + val user_pk = proveDlog(recipient); + val deadline = SELF.creationInfo._1 + 30; + + val self_tokens = SELF.tokens; + val token_amount = self_tokens(0)._2; + val erg_amount = token_amount * rate / divisor; + + val valid_height = HEIGHT < deadline; + + sigmaProp(OUTPUTS.exists({ (box: Box) => + allOf(Coll( + if (valid_height) { + box.value >= erg_amount + } else { + // refund + box.tokens == self_tokens + }, + box.R4[Coll[Byte]].get == SELF.id, + box.propositionBytes == user_pk.propBytes + )) + })) + } + + Swapping two tokens: + + { + val user_pk = proveDlog(recipient); + val deadline = SELF.creationInfo._1 + 30; + + val self_tokens = SELF.tokens; + val token_amount = self_tokens(0)._2; + val other_token_amount = token_amount * rate / divisor; + + val valid_height = HEIGHT < deadline; + + sigmaProp(OUTPUTS.exists({ (box: Box) => + allOf(Coll( + if (valid_height) { + val t = box.tokens(0); + t._1 == other_token_id && + t._2 >= other_token_amount + } else { + // refund + box.tokens == self_tokens + }, + box.R4[Coll[Byte]].get == SELF.id, + box.propositionBytes == user_pk.propBytes + )) + })) + } + + +* orders with partial filling support: + + Buy order: + + buyerPk || { + + val tokenPrice = $tokenPrice + val dexFeePerToken = $dexFeePerToken + + val returnBox = OUTPUTS.filter { (b: Box) => + b.R4[Coll[Byte]].isDefined && b.R4[Coll[Byte]].get == SELF.id && b.propositionBytes == buyerPk.propBytes + }(0) + + val returnTokenData = returnBox.tokens(0) + val returnTokenId = returnTokenData._1 + val returnTokenAmount = returnTokenData._2 + val maxReturnTokenErgValue = returnTokenAmount * tokenPrice + val totalReturnErgValue = maxReturnTokenErgValue + returnBox.value + val expectedDexFee = dexFeePerToken * returnTokenAmount + + val foundNewOrderBoxes = OUTPUTS.filter { (b: Box) => + b.R4[Coll[Byte]].isDefined && b.R4[Coll[Byte]].get == SELF.id && b.propositionBytes == SELF.propositionBytes + } + + val coinsSecured = (SELF.value - expectedDexFee) == maxReturnTokenErgValue || { + foundNewOrderBoxes.size == 1 && foundNewOrderBoxes(0).value >= (SELF.value - totalReturnErgValue - expectedDexFee) + } + + val tokenIdIsCorrect = returnTokenId == tokenId + + allOf(Coll( + tokenIdIsCorrect, + returnTokenAmount >= 1, + coinsSecured + )) + } + + Sell order: + + sellerPk || { + val tokenPrice = $tokenPrice + val dexFeePerToken = $dexFeePerToken + + val selfTokenAmount = SELF.tokens(0)._2 + + val returnBox = OUTPUTS.filter { (b: Box) => + b.R4[Coll[Byte]].isDefined && b.R4[Coll[Byte]].get == SELF.id && b.propositionBytes == sellerPk.propBytes + }(0) + + val foundNewOrderBoxes = OUTPUTS.filter { (b: Box) => + b.R4[Coll[Byte]].isDefined && b.R4[Coll[Byte]].get == SELF.id && b.propositionBytes == SELF.propositionBytes + } + + (returnBox.value == selfTokenAmount * tokenPrice) || { + foundNewOrderBoxes.size == 1 && { + val newOrderBox = foundNewOrderBoxes(0) + val newOrderTokenData = newOrderBox.tokens(0) + val newOrderTokenAmount = newOrderTokenData._2 + val soldTokenAmount = selfTokenAmount - newOrderTokenAmount + val minSoldTokenErgValue = soldTokenAmount * tokenPrice + val expectedDexFee = dexFeePerToken * soldTokenAmount + + val newOrderTokenId = newOrderTokenData._1 + val tokenIdIsCorrect = newOrderTokenId == tokenId + + tokenIdIsCorrect && soldTokenAmount >= 1 && newOrderBox.value >= (SELF.value - minSoldTokenErgValue - expectedDexFee) + } + } + } + +Liquidity Pool Contracts +------------------------ + +An AMM pool is a contract fulfilling orders. Basic operations (after pool bootstrapping) are add liquidity and remove liquduity. + + +* ErgoSwap V. 1 Contracts + + Pool contract: + + \ No newline at end of file From 1702aa54af09a49f6d51d94397bf61c9972bc7ce Mon Sep 17 00:00:00 2001 From: oskin1 Date: Wed, 17 Mar 2021 22:36:09 +0300 Subject: [PATCH 02/60] ErgoSwap AMM pool contracts added. --- eip-0014.md | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 1880cb2c..5f729fe5 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -206,9 +206,117 @@ Liquidity Pool Contracts An AMM pool is a contract fulfilling orders. Basic operations (after pool bootstrapping) are add liquidity and remove liquduity. - -* ErgoSwap V. 1 Contracts +* ErgoSwap V1 Contracts [Arbitrary Pairs] - Pool contract: +Pool bootstrapping contract: + +```scala +{ + val SuccessorScriptHash = $ergoSwapScriptHash // Regular ErgoSwapAMM contract hash. + + val liquidityTokenId = SELF.id + + def reservedLP(box: Box): Long = { + val maybeShares = box.tokens(1) + if (maybeShares._1 == liquidityTokenId) maybeShares._2 + else 0L + } + + val successor = OUTPUTS(0) + + val isValidContract = blake2b256(successor.propositionBytes) == SuccessorScriptHash + val isValidErgAmount = successor.value >= SELF.value + val isValidPoolNFT = successor.tokens(0) == (SELF.id, 1) + + val isValidInitialDepositing = { + val depositedA = successor.tokens(2)._2 + val depositedB = successor.tokens(3)._2 + val desiredShare = SELF.R4[Long].get + val productAB = depositedA * depositedB + val validDeposit = productAB * productAB == desiredShare // Deposits satisfy desired share + val validShares = reservedLP(successor) >= (reservedLP(SELF) - desiredShare) // valid amount of liquidity shares taken from reserves + validDeposit && validShares + } + + sigmaProp(isValidContract && isValidErgAmount && isValidPoolNFT && isValidInitialDepositing) +} +``` + +Pool contract: + +```scala +{ + val InitialLiquiditySharesLocked = 1000000000000000000L + + val ergs0 = SELF.value + val poolNFT0 = SELF.tokens(0) + val reservedLP0 = SELF.tokens(1) + val tokenA0 = SELF.tokens(2) + val tokenB0 = SELF.tokens(3) + + val successor = OUTPUTS(0) + + val ergs1 = successor.value + val poolNFT1 = successor.tokens(0) + val reservedLP1 = successor.tokens(1) + val tokenA1 = successor.tokens(2) + val tokenB1 = successor.tokens(3) + + val isValidSuccessor = successor.propositionBytes == SELF.propositionBytes + val isValidErgs = ergs1 >= ergs0 + val isValidPoolNFT = poolNFT1 == poolNFT0 + val isValidLP = reservedLP1._1 == reservedLP0._1 + val isValidPair = tokenA1._1 == tokenA0._1 && tokenB1._1 == tokenB0._1 + + val supplyLP0 = InitialLiquiditySharesLocked - reservedLP0._2 + val supplyLP1 = InitialLiquiditySharesLocked - reservedLP1._2 + + val reservesA0 = tokenA0._2 + val reservesB0 = tokenB0._2 + val reservesA1 = tokenA1._2 + val reservesB1 = tokenB1._2 + + val deltaSupplyLP = supplyLP1 - supplyLP0 // optimize? reservedLP0._2 - reservedLP1._2 + val deltaReservesA = reservesA1 - reservesA0 + val deltaReservesB = reservesB1 - reservesB0 + + val isValidDepositing = { + val sharesUnlocked = min( + deltaReservesA.toBigInt * supplyLP0 / reservesA0, + deltaReservesB.toBigInt * supplyLP0 / reservesB0 + ) + -deltaSupplyLP <= sharesUnlocked + } + + val isValidRedemption = { + val shareLP = deltaSupplyLP.toBigInt / supplyLP0 + // note: shareLP and deltaReservesA, deltaReservesB are negative + deltaReservesA >= shareLP * reservesA0 && deltaReservesB >= shareLP * reservesB0 + } + + val isValidSwaption = + if (deltaReservesA > 0) + -deltaReservesB <= (reservesB0.toBigInt * deltaReservesA * 997) / (reservesA0.toBigInt * 1000) + else + -deltaReservesA <= (reservesA0.toBigInt * deltaReservesB * 997) / (reservesB0.toBigInt * 1000) + + val isValidAction = + if (deltaSupplyLP == 0) + isValidSwaption + else + if (deltaReservesA > 0 && deltaReservesB > 0) isValidDepositing + else isValidRedemption + + sigmaProp( + isValidSuccessor && + isValidErgs && + isValidPoolNFT && + isValidLP && + isValidPair && + isValidAction + ) +} +``` +Swap contract: TODO \ No newline at end of file From db6b03c564e72105f6a325b74c9f1b8112ac9335 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Wed, 17 Mar 2021 23:02:35 +0300 Subject: [PATCH 03/60] Comments and formatting. --- eip-0014.md | 321 +++++++++++++++++++++++++++------------------------- 1 file changed, 166 insertions(+), 155 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 5f729fe5..2235ec1d 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -27,179 +27,191 @@ Order contracts are waiting for another orders to be matched, or for a refund co Buy order: - (HEIGHT > deadline && pkA) || { - val tokenData = OUTPUTS(0).R2[Coll[(Coll[Byte], Long)]].get(0) - allOf(Coll( - tokenData._1 == token1,tokenData._2 >= 60L, - OUTPUTS(0).propositionBytes == pkA.propBytes, - OUTPUTS(0).R4[Coll[Byte]].get == SELF.id - )) - } +```scala +(HEIGHT > deadline && pkA) || { + val tokenData = OUTPUTS(0).R2[Coll[(Coll[Byte], Long)]].get(0) + allOf(Coll( + tokenData._1 == token1,tokenData._2 >= 60L, + OUTPUTS(0).propositionBytes == pkA.propBytes, + OUTPUTS(0).R4[Coll[Byte]].get == SELF.id + )) +} +``` Sell order: - (HEIGHT > deadline && pkB) || - allOf(Coll( - OUTPUTS(1).value >= 100L, - OUTPUTS(1).propositionBytes == pkB.propBytes, - OUTPUTS(1).R4[Coll[Byte]].get == SELF.id - )) +```scala +(HEIGHT > deadline && pkB) || + allOf(Coll( + OUTPUTS(1).value >= 100L, + OUTPUTS(1).propositionBytes == pkB.propBytes, + OUTPUTS(1).R4[Coll[Byte]].get == SELF.id + )) +``` * simple swap order ([by Jason Davies](https://blog.plutomonkey.com/2021/01/generic-on-chain-ergo-swaps/)) Buy order: - { - val user_pk = proveDlog(recipient); - val deadline = SELF.creationInfo._1 + 30; - - val erg_amount = SELF.value - fee; - val token_amount = erg_amount * rate / divisor; - - val valid_height = HEIGHT < deadline; - - sigmaProp(OUTPUTS.exists({ (box: Box) => - allOf(Coll( - if (valid_height) { - val t = box.tokens(0); - t._1 == token_id && - t._2 >= token_amount - } else { - // refund - box.value >= erg_amount - }, - box.R4[Coll[Byte]].get == SELF.id, - box.propositionBytes == user_pk.propBytes - )) - })) - } - +```scala +{ + val user_pk = proveDlog(recipient); + val deadline = SELF.creationInfo._1 + 30; + + val erg_amount = SELF.value - fee; + val token_amount = erg_amount * rate / divisor; + + val valid_height = HEIGHT < deadline; + + sigmaProp(OUTPUTS.exists({ (box: Box) => + allOf(Coll( + if (valid_height) { + val t = box.tokens(0); + t._1 == token_id && + t._2 >= token_amount + } else { + // refund + box.value >= erg_amount + }, + box.R4[Coll[Byte]].get == SELF.id, + box.propositionBytes == user_pk.propBytes + )) + })) +} +``` Sell order: - { - val user_pk = proveDlog(recipient); - val deadline = SELF.creationInfo._1 + 30; - - val self_tokens = SELF.tokens; - val token_amount = self_tokens(0)._2; - val erg_amount = token_amount * rate / divisor; - - val valid_height = HEIGHT < deadline; - - sigmaProp(OUTPUTS.exists({ (box: Box) => - allOf(Coll( - if (valid_height) { - box.value >= erg_amount - } else { - // refund - box.tokens == self_tokens - }, - box.R4[Coll[Byte]].get == SELF.id, - box.propositionBytes == user_pk.propBytes - )) - })) - } +```scala +{ + val user_pk = proveDlog(recipient); + val deadline = SELF.creationInfo._1 + 30; + + val self_tokens = SELF.tokens; + val token_amount = self_tokens(0)._2; + val erg_amount = token_amount * rate / divisor; + + val valid_height = HEIGHT < deadline; + + sigmaProp(OUTPUTS.exists({ (box: Box) => + allOf(Coll( + if (valid_height) { + box.value >= erg_amount + } else { + // refund + box.tokens == self_tokens + }, + box.R4[Coll[Byte]].get == SELF.id, + box.propositionBytes == user_pk.propBytes + )) + })) +} +``` Swapping two tokens: - { - val user_pk = proveDlog(recipient); - val deadline = SELF.creationInfo._1 + 30; - - val self_tokens = SELF.tokens; - val token_amount = self_tokens(0)._2; - val other_token_amount = token_amount * rate / divisor; - - val valid_height = HEIGHT < deadline; - - sigmaProp(OUTPUTS.exists({ (box: Box) => - allOf(Coll( - if (valid_height) { - val t = box.tokens(0); - t._1 == other_token_id && - t._2 >= other_token_amount - } else { - // refund - box.tokens == self_tokens - }, - box.R4[Coll[Byte]].get == SELF.id, - box.propositionBytes == user_pk.propBytes - )) - })) - } - +```scala +{ + val user_pk = proveDlog(recipient); + val deadline = SELF.creationInfo._1 + 30; + + val self_tokens = SELF.tokens; + val token_amount = self_tokens(0)._2; + val other_token_amount = token_amount * rate / divisor; + + val valid_height = HEIGHT < deadline; + + sigmaProp(OUTPUTS.exists({ (box: Box) => + allOf(Coll( + if (valid_height) { + val t = box.tokens(0); + t._1 == other_token_id && + t._2 >= other_token_amount + } else { + // refund + box.tokens == self_tokens + }, + box.R4[Coll[Byte]].get == SELF.id, + box.propositionBytes == user_pk.propBytes + )) + })) +} +``` * orders with partial filling support: Buy order: - buyerPk || { - - val tokenPrice = $tokenPrice - val dexFeePerToken = $dexFeePerToken - - val returnBox = OUTPUTS.filter { (b: Box) => - b.R4[Coll[Byte]].isDefined && b.R4[Coll[Byte]].get == SELF.id && b.propositionBytes == buyerPk.propBytes - }(0) - - val returnTokenData = returnBox.tokens(0) - val returnTokenId = returnTokenData._1 - val returnTokenAmount = returnTokenData._2 - val maxReturnTokenErgValue = returnTokenAmount * tokenPrice - val totalReturnErgValue = maxReturnTokenErgValue + returnBox.value - val expectedDexFee = dexFeePerToken * returnTokenAmount - - val foundNewOrderBoxes = OUTPUTS.filter { (b: Box) => - b.R4[Coll[Byte]].isDefined && b.R4[Coll[Byte]].get == SELF.id && b.propositionBytes == SELF.propositionBytes - } - - val coinsSecured = (SELF.value - expectedDexFee) == maxReturnTokenErgValue || { - foundNewOrderBoxes.size == 1 && foundNewOrderBoxes(0).value >= (SELF.value - totalReturnErgValue - expectedDexFee) - } - - val tokenIdIsCorrect = returnTokenId == tokenId - - allOf(Coll( - tokenIdIsCorrect, - returnTokenAmount >= 1, - coinsSecured - )) - } +```scala +buyerPk || { + + val tokenPrice = $tokenPrice + val dexFeePerToken = $dexFeePerToken + + val returnBox = OUTPUTS.filter { (b: Box) => + b.R4[Coll[Byte]].isDefined && b.R4[Coll[Byte]].get == SELF.id && b.propositionBytes == buyerPk.propBytes + }(0) + + val returnTokenData = returnBox.tokens(0) + val returnTokenId = returnTokenData._1 + val returnTokenAmount = returnTokenData._2 + val maxReturnTokenErgValue = returnTokenAmount * tokenPrice + val totalReturnErgValue = maxReturnTokenErgValue + returnBox.value + val expectedDexFee = dexFeePerToken * returnTokenAmount + + val foundNewOrderBoxes = OUTPUTS.filter { (b: Box) => + b.R4[Coll[Byte]].isDefined && b.R4[Coll[Byte]].get == SELF.id && b.propositionBytes == SELF.propositionBytes + } + + val coinsSecured = (SELF.value - expectedDexFee) == maxReturnTokenErgValue || { + foundNewOrderBoxes.size == 1 && foundNewOrderBoxes(0).value >= (SELF.value - totalReturnErgValue - expectedDexFee) + } + + val tokenIdIsCorrect = returnTokenId == tokenId + + allOf(Coll( + tokenIdIsCorrect, + returnTokenAmount >= 1, + coinsSecured + )) +} +``` Sell order: - sellerPk || { - val tokenPrice = $tokenPrice - val dexFeePerToken = $dexFeePerToken - - val selfTokenAmount = SELF.tokens(0)._2 - - val returnBox = OUTPUTS.filter { (b: Box) => - b.R4[Coll[Byte]].isDefined && b.R4[Coll[Byte]].get == SELF.id && b.propositionBytes == sellerPk.propBytes - }(0) - - val foundNewOrderBoxes = OUTPUTS.filter { (b: Box) => - b.R4[Coll[Byte]].isDefined && b.R4[Coll[Byte]].get == SELF.id && b.propositionBytes == SELF.propositionBytes - } - - (returnBox.value == selfTokenAmount * tokenPrice) || { - foundNewOrderBoxes.size == 1 && { - val newOrderBox = foundNewOrderBoxes(0) - val newOrderTokenData = newOrderBox.tokens(0) - val newOrderTokenAmount = newOrderTokenData._2 - val soldTokenAmount = selfTokenAmount - newOrderTokenAmount - val minSoldTokenErgValue = soldTokenAmount * tokenPrice - val expectedDexFee = dexFeePerToken * soldTokenAmount - - val newOrderTokenId = newOrderTokenData._1 - val tokenIdIsCorrect = newOrderTokenId == tokenId - - tokenIdIsCorrect && soldTokenAmount >= 1 && newOrderBox.value >= (SELF.value - minSoldTokenErgValue - expectedDexFee) - } - } - } +```scala +sellerPk || { + val tokenPrice = $tokenPrice + val dexFeePerToken = $dexFeePerToken + + val selfTokenAmount = SELF.tokens(0)._2 + + val returnBox = OUTPUTS.filter { (b: Box) => + b.R4[Coll[Byte]].isDefined && b.R4[Coll[Byte]].get == SELF.id && b.propositionBytes == sellerPk.propBytes + }(0) + + val foundNewOrderBoxes = OUTPUTS.filter { (b: Box) => + b.R4[Coll[Byte]].isDefined && b.R4[Coll[Byte]].get == SELF.id && b.propositionBytes == SELF.propositionBytes + } + + (returnBox.value == selfTokenAmount * tokenPrice) || { + foundNewOrderBoxes.size == 1 && { + val newOrderBox = foundNewOrderBoxes(0) + val newOrderTokenData = newOrderBox.tokens(0) + val newOrderTokenAmount = newOrderTokenData._2 + val soldTokenAmount = selfTokenAmount - newOrderTokenAmount + val minSoldTokenErgValue = soldTokenAmount * tokenPrice + val expectedDexFee = dexFeePerToken * soldTokenAmount + + val newOrderTokenId = newOrderTokenData._1 + val tokenIdIsCorrect = newOrderTokenId == tokenId + + tokenIdIsCorrect && soldTokenAmount >= 1 && newOrderBox.value >= (SELF.value - minSoldTokenErgValue - expectedDexFee) + } + } +} +``` Liquidity Pool Contracts ------------------------ @@ -232,8 +244,7 @@ Pool bootstrapping contract: val depositedA = successor.tokens(2)._2 val depositedB = successor.tokens(3)._2 val desiredShare = SELF.R4[Long].get - val productAB = depositedA * depositedB - val validDeposit = productAB * productAB == desiredShare // Deposits satisfy desired share + val validDeposit = depositedA * depositedB == desiredShare * desiredShare // S = sqrt(A_deposited * B_deposited) Deposits satisfy desired share val validShares = reservedLP(successor) >= (reservedLP(SELF) - desiredShare) // valid amount of liquidity shares taken from reserves validDeposit && validShares } @@ -246,7 +257,7 @@ Pool contract: ```scala { - val InitialLiquiditySharesLocked = 1000000000000000000L + val InitiallyLockedLP = 1000000000000000000L val ergs0 = SELF.value val poolNFT0 = SELF.tokens(0) @@ -268,8 +279,8 @@ Pool contract: val isValidLP = reservedLP1._1 == reservedLP0._1 val isValidPair = tokenA1._1 == tokenA0._1 && tokenB1._1 == tokenB0._1 - val supplyLP0 = InitialLiquiditySharesLocked - reservedLP0._2 - val supplyLP1 = InitialLiquiditySharesLocked - reservedLP1._2 + val supplyLP0 = InitiallyLockedLP - reservedLP0._2 + val supplyLP1 = InitiallyLockedLP - reservedLP1._2 val reservesA0 = tokenA0._2 val reservesB0 = tokenB0._2 From 36952873c8f83a9154b4519f0d7f3116593ec102 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Fri, 19 Mar 2021 10:25:32 +0300 Subject: [PATCH 04/60] AMM Pool contract updated. Swap contract added. --- eip-0014.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 2235ec1d..4a1f3a05 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -305,11 +305,17 @@ Pool contract: deltaReservesA >= shareLP * reservesA0 && deltaReservesB >= shareLP * reservesB0 } - val isValidSwaption = - if (deltaReservesA > 0) - -deltaReservesB <= (reservesB0.toBigInt * deltaReservesA * 997) / (reservesA0.toBigInt * 1000) - else - -deltaReservesA <= (reservesA0.toBigInt * deltaReservesB * 997) / (reservesB0.toBigInt * 1000) + val isValidSwaption = { + val isFairAmountSwapped = + if (deltaReservesA > 0) + -deltaReservesB <= (reservesB0.toBigInt * deltaReservesA * 997) / (reservesA0.toBigInt * 1000) // todo: better precision const? + else + -deltaReservesA <= (reservesA0.toBigInt * deltaReservesB * 997) / (reservesB0.toBigInt * 1000) + + val isConstantProductPreserved = reservesA1 * reservesB1 >= reservesA0 * reservesB0 + + isFairAmountSwapped && isConstantProductPreserved + } val isValidAction = if (deltaSupplyLP == 0) @@ -328,6 +334,47 @@ Pool contract: ) } ``` -Swap contract: TODO +Swap contract: +```scala +{ + val Pk = $pk + + val PoolScriptHash = $poolScriptHash + + val MinQuoteAmount = $minQuoteAmount + val QuoteId = $quoteId + + val base = SELF.tokens(0) + val baseId = base._1 + val baseAmount = base._2 + + val poolInput = INPUTS(0) + val poolAssetA = poolInput.tokens(2) + val poolAssetB = poolInput.tokens(3) + + val isValidPoolInput = + blake2b256(poolInput.propositionBytes) == PoolScriptHash && + (poolAssetA._1 == QuoteId || poolAssetB._1 == QuoteId) && + (poolAssetA._1 == baseId || poolAssetB._1 == baseId) + + val isValidSwap = + OUTPUTS.exists { (box: Box) => + val quoteAsset = box.tokens(0) + val quoteAmount = quoteAsset._2 + val isFairPrice = + if (poolAssetA._1 == QuoteId) + quoteAmount >= (poolAssetA._2 * baseAmount * 997) / (poolAssetB._2 * 1000) + else + quoteAmount >= (poolAssetB._2 * baseAmount * 997) / (poolAssetA._2 * 1000) + + box.propositionBytes == Pk.propBytes && + quoteAsset._1 == QuoteId && + quoteAsset._2 >= MinQuoteAmount && + isFairPrice + } + + sigmaProp(Pk || (isValidPoolInput && isValidSwap)) +} +``` \ No newline at end of file From fe19cdefc6d8466e684743969636219d52502376 Mon Sep 17 00:00:00 2001 From: "i.oskin" Date: Fri, 19 Mar 2021 11:41:35 +0300 Subject: [PATCH 05/60] Swap formula updated. --- eip-0014.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 4a1f3a05..95570397 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -308,9 +308,9 @@ Pool contract: val isValidSwaption = { val isFairAmountSwapped = if (deltaReservesA > 0) - -deltaReservesB <= (reservesB0.toBigInt * deltaReservesA * 997) / (reservesA0.toBigInt * 1000) // todo: better precision const? + -deltaReservesB <= (reservesB0.toBigInt * deltaReservesA * 997) / (reservesA0.toBigInt * 1000 + deltaReservesA * 997) // todo: better precision const? else - -deltaReservesA <= (reservesA0.toBigInt * deltaReservesB * 997) / (reservesB0.toBigInt * 1000) + -deltaReservesA <= (reservesA0.toBigInt * deltaReservesB * 997) / (reservesB0.toBigInt * 1000 + deltaReservesB * 997) val isConstantProductPreserved = reservesA1 * reservesB1 >= reservesA0 * reservesB0 From a5487f58dbe4fd0e9dcf96abd2055abedeffd338 Mon Sep 17 00:00:00 2001 From: "i.oskin" Date: Fri, 19 Mar 2021 12:49:55 +0300 Subject: [PATCH 06/60] Pool contract simplified. Swap price formula updated. --- eip-0014.md | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 95570397..ce6b9dcf 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -305,17 +305,11 @@ Pool contract: deltaReservesA >= shareLP * reservesA0 && deltaReservesB >= shareLP * reservesB0 } - val isValidSwaption = { - val isFairAmountSwapped = - if (deltaReservesA > 0) - -deltaReservesB <= (reservesB0.toBigInt * deltaReservesA * 997) / (reservesA0.toBigInt * 1000 + deltaReservesA * 997) // todo: better precision const? - else - -deltaReservesA <= (reservesA0.toBigInt * deltaReservesB * 997) / (reservesB0.toBigInt * 1000 + deltaReservesB * 997) - - val isConstantProductPreserved = reservesA1 * reservesB1 >= reservesA0 * reservesB0 - - isFairAmountSwapped && isConstantProductPreserved - } + val isValidSwaption = + if (deltaReservesA > 0) + -deltaReservesB <= (reservesB0.toBigInt * deltaReservesA * 997) / (reservesA0.toBigInt * 1000 + deltaReservesA * 997) // todo: better precision const? + else + -deltaReservesA <= (reservesA0.toBigInt * deltaReservesB * 997) / (reservesB0.toBigInt * 1000 + deltaReservesB * 997) val isValidAction = if (deltaSupplyLP == 0) @@ -364,9 +358,9 @@ Swap contract: val quoteAmount = quoteAsset._2 val isFairPrice = if (poolAssetA._1 == QuoteId) - quoteAmount >= (poolAssetA._2 * baseAmount * 997) / (poolAssetB._2 * 1000) + quoteAmount >= (poolAssetA._2 * baseAmount * 997) / (poolAssetB._2 * 1000 + baseAmount * 997) else - quoteAmount >= (poolAssetB._2 * baseAmount * 997) / (poolAssetA._2 * 1000) + quoteAmount >= (poolAssetB._2 * baseAmount * 997) / (poolAssetA._2 * 1000 + baseAmount * 997) box.propositionBytes == Pk.propBytes && quoteAsset._1 == QuoteId && From 9f3540210ac9434a5b2ad8b19f6ec5eb02440232 Mon Sep 17 00:00:00 2001 From: "i.oskin" Date: Sun, 21 Mar 2021 00:51:51 +0300 Subject: [PATCH 07/60] ErgoDEX description: WIP. --- eip-0014.md | 205 ++++++++++++++++++++++++++++------------------------ 1 file changed, 109 insertions(+), 96 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index ce6b9dcf..99035341 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -1,58 +1,29 @@ -Decentralized Exchange Contracts Standard -========================================= +# Automated Decentralized Exchange * Author: kushti, Ilya Oskin * Status: Proposed * Created: 12-Mar-2021 -* Last edited: 12-Mar-2021 +* Last edited: 21-Mar-2021 * License: CC0 * Track: Standards -Motivation ----------- +## Motivation Act of exchange without trusted parties is a most basic primitive for decentralized finance on top of blockchains. Thus contracts for that were introduced early, and basic single-chain swap contract was introduced early in the [ErgoScript whitepaper](https://ergoplatform.org/docs/ErgoScript.pdf). Then a lot of other order contracts appeared: with partial filling, buyback guarantee and so on. What is good for traders in decentralized worlds, such contracts are usually composable. -While swap order contracts allows for orderbook-based decentralized exchanges (DEXes), now popular AMM-based DEXes (where AMM stands for Automated Market Maker) also possible on Ergo. +While swap order contracts allows for orderbook-based decentralized exchanges (DEXes), now popular AMM-based DEXes (where AMM stands for Automated Market Maker) are also possible on Ergo. Interestingly, unlike other known blockchains, thanks to the extended UTXO model, liquidity pool contracts for AMM-based DEXes can be combined with order contracts (for orderbook-based DEXes). This gives unique possibility to have shared liquidity among different types of exchanges on top of the Ergo blockchain. -This PR provides known DEX contracts for both orderbook-based and AMM-based DEXes, and also provides info on their composability. +This PR provides a description of the Automated Decentralized Exchange protocol on top of the Ergo. +## Order-book DEX -Order Contracts ---------------- +Orders are waiting for another orders to be matched, or for a refund command. There're the following three types of orders — "buy" (i.e. buy tokens with ERG), "sell" (i.e. sell tokens for ERG), or "swap" (buy non-ERG tokens with other non-ERG tokens) orders. Order-based markets have the advantage of working best for those pairs with high liquidity. -Order contracts are waiting for another orders to be matched, or for a refund command. Orders can be buy (i.e. buy tokens with ERG), sell (i.e. sell tokens for ERG), or swap (buy non-ERG tokens with other non-ERG tokens) orders. +### Atomic orders -* simplest orders (from the ErgoScript whitepaper) +Atomic orders can only be executed completely. Such orders can be either be aggregated by the ErgoDEX client so that users can choose from them or matched with partial orders which will be defined next. - Buy order: - -```scala -(HEIGHT > deadline && pkA) || { - val tokenData = OUTPUTS(0).R2[Coll[(Coll[Byte], Long)]].get(0) - allOf(Coll( - tokenData._1 == token1,tokenData._2 >= 60L, - OUTPUTS(0).propositionBytes == pkA.propBytes, - OUTPUTS(0).R4[Coll[Byte]].get == SELF.id - )) -} -``` - - Sell order: - -```scala -(HEIGHT > deadline && pkB) || - allOf(Coll( - OUTPUTS(1).value >= 100L, - OUTPUTS(1).propositionBytes == pkB.propBytes, - OUTPUTS(1).R4[Coll[Byte]].get == SELF.id - )) -``` - - -* simple swap order ([by Jason Davies](https://blog.plutomonkey.com/2021/01/generic-on-chain-ergo-swaps/)) - - Buy order: +**Buy order [ERG -> Token]:** ```scala { @@ -81,7 +52,7 @@ Order contracts are waiting for another orders to be matched, or for a refund co } ``` - Sell order: +**Sell order [Token -> ERG]:** ```scala { @@ -109,7 +80,7 @@ Order contracts are waiting for another orders to be matched, or for a refund co } ``` - Swapping two tokens: +**Swapping two tokens [TokenX -> TokenY]:** ```scala { @@ -139,86 +110,128 @@ Order contracts are waiting for another orders to be matched, or for a refund co } ``` -* orders with partial filling support: +### Orders with partial filling support: + +Partial orders are something more familiar to those who've ever used classical CEX'es. These orders can be partially executed so the best way to work with them is an order-book, where they can be aggregated, matched and executed by ErgoDEX bots. - Buy order: +**Buy order [ERG -> Token]:** ```scala -buyerPk || { +{ + val PrecisionConstant = 1000000000L - val tokenPrice = $tokenPrice - val dexFeePerToken = $dexFeePerToken + val quoteId = SELF.R4[Coll[Byte]].get // R4 - quote tokenId + val price = SELF.R5[Long].get // R5 - price per token + val feePerToken = SELF.R6[Long].get // R6 - fee per token - val returnBox = OUTPUTS.filter { (b: Box) => - b.R4[Coll[Byte]].isDefined && b.R4[Coll[Byte]].get == SELF.id && b.propositionBytes == buyerPk.propBytes - }(0) + val maybeRewardBox = OUTPUTS(0) + val isValidRewardProposition = maybeRewardBox.propositionBytes == pk.propBytes + val maybeRewardToken = maybeRewardBox.tokens(0) - val returnTokenData = returnBox.tokens(0) - val returnTokenId = returnTokenData._1 - val returnTokenAmount = returnTokenData._2 - val maxReturnTokenErgValue = returnTokenAmount * tokenPrice - val totalReturnErgValue = maxReturnTokenErgValue + returnBox.value - val expectedDexFee = dexFeePerToken * returnTokenAmount + val rewardTokens = + if (isValidRewardProposition && maybeRewardToken._1 == quoteId) maybeRewardToken._2 + else 0L - val foundNewOrderBoxes = OUTPUTS.filter { (b: Box) => - b.R4[Coll[Byte]].isDefined && b.R4[Coll[Byte]].get == SELF.id && b.propositionBytes == SELF.propositionBytes - } + val hasResidualBox = OUTPUTS.size > 1 + val maybeResidualBox = OUTPUTS(1) + val isValidResidualProposition = maybeResidualBox.propositionBytes == SELF.propositionBytes + val isValidResidualRegisters = + maybeResidualBox.R4[Coll[Byte]].get == quoteId && + maybeResidualBox.R5[Long].get == price && + maybeResidualBox.R6[Long].get == feePerToken - val coinsSecured = (SELF.value - expectedDexFee) == maxReturnTokenErgValue || { - foundNewOrderBoxes.size == 1 && foundNewOrderBoxes(0).value >= (SELF.value - totalReturnErgValue - expectedDexFee) - } + val validResidualBoxExists = hasResidualBox && isValidResidualProposition && isValidResidualRegisters - val tokenIdIsCorrect = returnTokenId == tokenId + val leftErgs = + if (validResidualBoxExists) maybeResidualBox.value + else 0L - allOf(Coll( - tokenIdIsCorrect, - returnTokenAmount >= 1, - coinsSecured - )) + val feeCharged = rewardTokens * feePerToken + val isValidReward = rewardTokens.toBigInt * PrecisionConstant >= (SELF.value.toBigInt - feeCharged - leftErgs) * PrecisionConstant / price + + sigmaProp(pk || isValidReward) } ``` - Sell order: +**Sell order [Token -> ERG]:** ```scala -sellerPk || { - val tokenPrice = $tokenPrice - val dexFeePerToken = $dexFeePerToken +{ + val baseId = SELF.R4[Coll[Byte]].get // R4 - quote tokenId + val price = SELF.R5[Long].get // R5 - price per token + val feePerToken = SELF.R6[Long].get // R6 - fee per token - val selfTokenAmount = SELF.tokens(0)._2 + val maybeRewardBox = OUTPUTS(0) + val isValidRewardProposition = maybeRewardBox.propositionBytes == pk.propBytes - val returnBox = OUTPUTS.filter { (b: Box) => - b.R4[Coll[Byte]].isDefined && b.R4[Coll[Byte]].get == SELF.id && b.propositionBytes == sellerPk.propBytes - }(0) + val ergs0 = SELF.value + val ergs1 = + if (isValidRewardProposition) maybeRewardBox.value + else 0L - val foundNewOrderBoxes = OUTPUTS.filter { (b: Box) => - b.R4[Coll[Byte]].isDefined && b.R4[Coll[Byte]].get == SELF.id && b.propositionBytes == SELF.propositionBytes - } + val deltaErgs = ergs1 - ergs0 - (returnBox.value == selfTokenAmount * tokenPrice) || { - foundNewOrderBoxes.size == 1 && { - val newOrderBox = foundNewOrderBoxes(0) - val newOrderTokenData = newOrderBox.tokens(0) - val newOrderTokenAmount = newOrderTokenData._2 - val soldTokenAmount = selfTokenAmount - newOrderTokenAmount - val minSoldTokenErgValue = soldTokenAmount * tokenPrice - val expectedDexFee = dexFeePerToken * soldTokenAmount + val hasResidualBox = OUTPUTS.size > 1 + val maybeResidualBox = OUTPUTS(1) + val maybeResidualAsset = maybeResidualBox.tokens(0) + val isValidResidualProposition = maybeResidualBox.propositionBytes == SELF.propositionBytes + val isValidResidualAsset = maybeResidualAsset._1 == baseId + val isValidResidualRegisters = + maybeResidualBox.R4[Coll[Byte]].get == baseId && + maybeResidualBox.R5[Long].get == price && + maybeResidualBox.R6[Long].get == feePerToken - val newOrderTokenId = newOrderTokenData._1 - val tokenIdIsCorrect = newOrderTokenId == tokenId + val validResidualBoxExists = hasResidualBox && isValidResidualProposition && isValidResidualAsset && isValidResidualRegisters - tokenIdIsCorrect && soldTokenAmount >= 1 && newOrderBox.value >= (SELF.value - minSoldTokenErgValue - expectedDexFee) - } - } + val tokens0 = SELF.tokens(0)._2 + val tokens1 = + if (validResidualBoxExists) maybeResidualAsset._2 + else 0L + + val soldTokens = tokens0 - tokens1 + + val feeCharged = soldTokens * feePerToken + val isValidReward = deltaErgs.toBigInt >= soldTokens.toBigInt * price - feeCharged + + sigmaProp(pk || isValidReward) } -``` +``` + +### On-chain matching vs Off-chain + +todo + +## Automated Liquidity Pools + +Unlike order-book based DEX which relies on an order-book to represent liquidity and determine prices AMM DEX uses an automated market maker mechanism to provide instant feedback on rates and slippage. + +Each AMM liquidity pool is a trading venue for a pair of assets. In order to facilitate trades a liquidity pool accepts deposits of underlying assets proportional to their price rates. Whenever deposit happens a proportional amount of unique tokens known as liquidity tokens is minted. Minted liquidity tokens are distributed among liquidity providers proportional to their deposits. Liquidity providers can later exchange their liquidity tokens share for a proportional amount of underlying reserves. + +### Ergo AMM DEX Contracts [Arbitrary Pairs] + +Ergo AMM DEX relies on two types of contracts: + +- Pool contracts +- Swap contracts + +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. +- Swap. Tokens are exchanged at a price corresponding to a relation of a pair’s reserve balances while preserving constant product constraint. Correct amount of protocol fees is paid. + +#### Liquidity pool bootstrapping + +A liquidity pool is bootstrapped in two steps: + +1. In order to track pro-rata LP shares of the total reserves of a new pair a unique token must be issued. As soon as tokens can’t be re-issued on Ergo the whole LP emission has to be done at once. A distribution of emitted tokens is controlled by the pool contract. +2. In order to start facilitating trades a liquidity pool must be initialised by depositing initial amounts of pair assets. For the initializing deposit the amount of LP tokens is calculated using special formula which is `LP = sqrt(X_deposited, Y_deposited)`. -Liquidity Pool Contracts ------------------------- +In order to avoid blowing up the pool contract with code which handles specific intialization aspects a dedicated type of contract is used. -An AMM pool is a contract fulfilling orders. Basic operations (after pool bootstrapping) are add liquidity and remove liquduity. +#### Tracking pool identity -* ErgoSwap V1 Contracts [Arbitrary Pairs] +Pool NFT token. Pool bootstrapping contract: From 3e6d16f266752272a8e7c1e9bbc8a80893500e3f Mon Sep 17 00:00:00 2001 From: "i.oskin" Date: Sun, 21 Mar 2021 15:02:18 +0300 Subject: [PATCH 08/60] Complete protocol description. --- eip-0014.md | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 99035341..b7c07c2a 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -17,7 +17,7 @@ This PR provides a description of the Automated Decentralized Exchange protocol ## Order-book DEX -Orders are waiting for another orders to be matched, or for a refund command. There're the following three types of orders — "buy" (i.e. buy tokens with ERG), "sell" (i.e. sell tokens for ERG), or "swap" (buy non-ERG tokens with other non-ERG tokens) orders. Order-based markets have the advantage of working best for those pairs with high liquidity. +Orders are waiting for another orders to be matched, or for a refund command. There're the following three types of orders — "buy" (i.e. buy tokens with ERG), "sell" (i.e. sell tokens for ERG), or "swap" (buy non-ERG tokens with other non-ERG tokens) orders. Order-book DEX has the advantage of working best for those pairs with high liquidity. ### Atomic orders @@ -199,13 +199,13 @@ Partial orders are something more familiar to those who've ever used classical C ### On-chain matching vs Off-chain -todo +It is not neccessary to publish orders on chain in order for them to be matched. ErgoDEX bots can synchronize orders off-chain, match them and only then execute in chained transactions. ## Automated Liquidity Pools -Unlike order-book based DEX which relies on an order-book to represent liquidity and determine prices AMM DEX uses an automated market maker mechanism to provide instant feedback on rates and slippage. +Unlike order-book based DEX which relies on an order-book to represent liquidity and determine prices AMM DEX uses an automated market maker mechanism to provide instant feedback on rates and slippage. AMM DEX suits best for pairs with low liquidity. -Each AMM liquidity pool is a trading venue for a pair of assets. In order to facilitate trades a liquidity pool accepts deposits of underlying assets proportional to their price rates. Whenever deposit happens a proportional amount of unique tokens known as liquidity tokens is minted. Minted liquidity tokens are distributed among liquidity providers proportional to their deposits. Liquidity providers can later exchange their liquidity tokens share for a proportional amount of underlying reserves. +Each AMM liquidity pool is a trading venue for a pair of assets. In order to facilitate trades a liquidity pool accepts deposits of underlying assets proportional to their price rates. Whenever deposit happens a proportional amount of unique tokens known as liquidity tokens is minted. Minted liquidity tokens are distributed among liquidity providers proportional to their deposits. Liquidity providers can later exchange their liquidity tokens share for a proportional amount of underlying reserves. ### Ergo AMM DEX Contracts [Arbitrary Pairs] @@ -214,11 +214,13 @@ Ergo AMM DEX relies on two types of contracts: - Pool contracts - Swap contracts +#### Pool contracts + 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. -- Swap. Tokens are exchanged at a price corresponding to a relation of a pair’s reserve balances while preserving constant product constraint. Correct amount of protocol fees is paid. +- 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)` #### Liquidity pool bootstrapping @@ -227,11 +229,12 @@ A liquidity pool is bootstrapped in two steps: 1. In order to track pro-rata LP shares of the total reserves of a new pair a unique token must be issued. As soon as tokens can’t be re-issued on Ergo the whole LP emission has to be done at once. A distribution of emitted tokens is controlled by the pool contract. 2. In order to start facilitating trades a liquidity pool must be initialised by depositing initial amounts of pair assets. For the initializing deposit the amount of LP tokens is calculated using special formula which is `LP = sqrt(X_deposited, Y_deposited)`. -In order to avoid blowing up the pool contract with code which handles specific intialization aspects a dedicated type of contract is used. +In order to avoid blowing up the pool contract with a code which handles only specific intialization aspects a dedicated type of contract is used. #### Tracking pool identity -Pool NFT token. +In order to preserve pool uniqueness a non-fungible token (NFT) is used. Then concrete pool can be identified by a unique NFT containing in pool UTXO. +Pool NFT is created at pool initialization stage. The pool bootstrapping contract ensures this step is done correctly. Pool bootstrapping contract: @@ -300,7 +303,7 @@ Pool contract: val reservesA1 = tokenA1._2 val reservesB1 = tokenB1._2 - val deltaSupplyLP = supplyLP1 - supplyLP0 // optimize? reservedLP0._2 - reservedLP1._2 + val deltaSupplyLP = supplyLP1 - supplyLP0 val deltaReservesA = reservesA1 - reservesA0 val deltaReservesB = reservesB1 - reservesB0 @@ -341,6 +344,15 @@ Pool contract: ) } ``` + +#### Swap contracts + +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 * 997 / (Y_reserved * 1000 + Y_input * 997)` +* 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. + Swap contract: ```scala From e78df02a596098296b02a15a1e8553eb531356b0 Mon Sep 17 00:00:00 2001 From: "i.oskin" Date: Sun, 21 Mar 2021 17:09:08 +0300 Subject: [PATCH 09/60] More detailed protocol description. --- eip-0014.md | 80 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index b7c07c2a..ee607467 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -235,6 +235,12 @@ In order to avoid blowing up the pool contract with a code which handles only sp In order to preserve pool uniqueness a non-fungible token (NFT) is used. Then concrete pool can be identified by a unique NFT containing in pool UTXO. Pool NFT is created at pool initialization stage. The pool bootstrapping contract ensures this step is done correctly. + +Structure of pool bootstrapping UTXO: + +Section | Description +value | Constant amount of ERGs +tokens[0] | LP token reserves Pool bootstrapping contract: @@ -245,7 +251,7 @@ Pool bootstrapping contract: val liquidityTokenId = SELF.id def reservedLP(box: Box): Long = { - val maybeShares = box.tokens(1) + val maybeShares = box.tokens(0) if (maybeShares._1 == liquidityTokenId) maybeShares._2 else 0L } @@ -254,13 +260,13 @@ Pool bootstrapping contract: val isValidContract = blake2b256(successor.propositionBytes) == SuccessorScriptHash val isValidErgAmount = successor.value >= SELF.value - val isValidPoolNFT = successor.tokens(0) == (SELF.id, 1) + val isValidPoolNFT = successor.tokens(1) == (SELF.id, 1) val isValidInitialDepositing = { - val depositedA = successor.tokens(2)._2 - val depositedB = successor.tokens(3)._2 - val desiredShare = SELF.R4[Long].get - val validDeposit = depositedA * depositedB == desiredShare * desiredShare // S = sqrt(A_deposited * B_deposited) Deposits satisfy desired share + val depositedX = successor.tokens(2)._2 + val depositedY = successor.tokens(3)._2 + val desiredShare = successor.R4[Long].get + val validDeposit = depositedX * depositedY == desiredShare * desiredShare // S = sqrt(X_deposited * Y_deposited) Deposits satisfy desired share val validShares = reservedLP(successor) >= (reservedLP(SELF) - desiredShare) // valid amount of liquidity shares taken from reserves validDeposit && validShares } @@ -269,6 +275,16 @@ Pool bootstrapping contract: } ``` +Structure of pool UTXO: + +Section | Description +value | Constant amount of ERGs +tokens[0] | LP token reserves +tokens[1] | Pool NFT +tokens[2] | Asset X +tokens[3] | Asset Y +R4[Long] | Desired share (Required only at initialisation stage) + Pool contract: ```scala @@ -276,62 +292,62 @@ Pool contract: val InitiallyLockedLP = 1000000000000000000L val ergs0 = SELF.value - val poolNFT0 = SELF.tokens(0) - val reservedLP0 = SELF.tokens(1) - val tokenA0 = SELF.tokens(2) - val tokenB0 = SELF.tokens(3) + val reservedLP0 = SELF.tokens(0) + val poolNFT0 = SELF.tokens(1) + val tokenX0 = SELF.tokens(2) + val tokenY0 = SELF.tokens(3) val successor = OUTPUTS(0) val ergs1 = successor.value - val poolNFT1 = successor.tokens(0) - val reservedLP1 = successor.tokens(1) - val tokenA1 = successor.tokens(2) - val tokenB1 = successor.tokens(3) + val reservedLP1 = successor.tokens(0) + val poolNFT1 = successor.tokens(1) + val tokenX1 = successor.tokens(2) + val tokenY1 = successor.tokens(3) val isValidSuccessor = successor.propositionBytes == SELF.propositionBytes val isValidErgs = ergs1 >= ergs0 val isValidPoolNFT = poolNFT1 == poolNFT0 val isValidLP = reservedLP1._1 == reservedLP0._1 - val isValidPair = tokenA1._1 == tokenA0._1 && tokenB1._1 == tokenB0._1 + val isValidPair = tokenX1._1 == tokenX0._1 && tokenY1._1 == tokenY0._1 val supplyLP0 = InitiallyLockedLP - reservedLP0._2 val supplyLP1 = InitiallyLockedLP - reservedLP1._2 - val reservesA0 = tokenA0._2 - val reservesB0 = tokenB0._2 - val reservesA1 = tokenA1._2 - val reservesB1 = tokenB1._2 + val reservesX0 = tokenX0._2 + val reservesY0 = tokenY0._2 + val reservesX1 = tokenX1._2 + val reservesY1 = tokenY1._2 - val deltaSupplyLP = supplyLP1 - supplyLP0 - val deltaReservesA = reservesA1 - reservesA0 - val deltaReservesB = reservesB1 - reservesB0 + val deltaSupplyLP = supplyLP1 - supplyLP0 // optimize? reservedLP0._2 - reservedLP1._2 + val deltaReservesX = reservesX1 - reservesX0 + val deltaReservesY = reservesY1 - reservesY0 val isValidDepositing = { val sharesUnlocked = min( - deltaReservesA.toBigInt * supplyLP0 / reservesA0, - deltaReservesB.toBigInt * supplyLP0 / reservesB0 + deltaReservesX.toBigInt * supplyLP0 / reservesX0, + deltaReservesY.toBigInt * supplyLP0 / reservesY0 ) -deltaSupplyLP <= sharesUnlocked } val isValidRedemption = { val shareLP = deltaSupplyLP.toBigInt / supplyLP0 - // note: shareLP and deltaReservesA, deltaReservesB are negative - deltaReservesA >= shareLP * reservesA0 && deltaReservesB >= shareLP * reservesB0 + // note: shareLP and deltaReservesX, deltaReservesY are negative + deltaReservesX >= shareLP * reservesX0 && deltaReservesY >= shareLP * reservesY0 } - val isValidSwaption = - if (deltaReservesA > 0) - -deltaReservesB <= (reservesB0.toBigInt * deltaReservesA * 997) / (reservesA0.toBigInt * 1000 + deltaReservesA * 997) // todo: better precision const? + val isValidSwap = + if (deltaReservesX > 0) + -deltaReservesY <= (reservesY0.toBigInt * deltaReservesX * 997) / (reservesX0.toBigInt * 1000 + deltaReservesX * 997) // todo: better precision const? else - -deltaReservesA <= (reservesA0.toBigInt * deltaReservesB * 997) / (reservesB0.toBigInt * 1000 + deltaReservesB * 997) + -deltaReservesX <= (reservesX0.toBigInt * deltaReservesY * 997) / (reservesY0.toBigInt * 1000 + deltaReservesY * 997) val isValidAction = if (deltaSupplyLP == 0) - isValidSwaption + isValidSwap else - if (deltaReservesA > 0 && deltaReservesB > 0) isValidDepositing + if (deltaReservesX > 0 && deltaReservesY > 0) isValidDepositing else isValidRedemption sigmaProp( From b6f014b556d4cb6ed3141047677cd02713a21661 Mon Sep 17 00:00:00 2001 From: "i.oskin" Date: Sun, 21 Mar 2021 17:14:46 +0300 Subject: [PATCH 10/60] Formatting. --- eip-0014.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index ee607467..3627dfd7 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -23,7 +23,7 @@ Orders are waiting for another orders to be matched, or for a refund command. Th Atomic orders can only be executed completely. Such orders can be either be aggregated by the ErgoDEX client so that users can choose from them or matched with partial orders which will be defined next. -**Buy order [ERG -> Token]:** +#### Buy order [ERG -> Token] ```scala { @@ -52,7 +52,7 @@ Atomic orders can only be executed completely. Such orders can be either be aggr } ``` -**Sell order [Token -> ERG]:** +#### Sell order [Token -> ERG] ```scala { @@ -80,7 +80,7 @@ Atomic orders can only be executed completely. Such orders can be either be aggr } ``` -**Swapping two tokens [TokenX -> TokenY]:** +#### Swap [TokenX -> TokenY] ```scala { @@ -114,7 +114,7 @@ Atomic orders can only be executed completely. Such orders can be either be aggr Partial orders are something more familiar to those who've ever used classical CEX'es. These orders can be partially executed so the best way to work with them is an order-book, where they can be aggregated, matched and executed by ErgoDEX bots. -**Buy order [ERG -> Token]:** +#### Buy order [ERG -> Token] ```scala { @@ -153,7 +153,7 @@ Partial orders are something more familiar to those who've ever used classical C } ``` -**Sell order [Token -> ERG]:** +#### Sell order [Token -> ERG] ```scala { @@ -236,13 +236,14 @@ In order to avoid blowing up the pool contract with a code which handles only sp In order to preserve pool uniqueness a non-fungible token (NFT) is used. Then concrete pool can be identified by a unique NFT containing in pool UTXO. Pool NFT is created at pool initialization stage. The pool bootstrapping contract ensures this step is done correctly. -Structure of pool bootstrapping UTXO: +#### Structure of pool bootstrapping UTXO Section | Description +----------|------------------------ value | Constant amount of ERGs tokens[0] | LP token reserves -Pool bootstrapping contract: +#### Pool bootstrapping contract ```scala { @@ -275,9 +276,10 @@ Pool bootstrapping contract: } ``` -Structure of pool UTXO: +#### Structure of pool UTXO Section | Description +----------|------------------------------------------------------ value | Constant amount of ERGs tokens[0] | LP token reserves tokens[1] | Pool NFT @@ -285,7 +287,7 @@ tokens[2] | Asset X tokens[3] | Asset Y R4[Long] | Desired share (Required only at initialisation stage) -Pool contract: +#### Pool contract ```scala { @@ -369,8 +371,6 @@ Swap contract ensures a swap is executed fairly from a user's perspective. The c 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. -Swap contract: - ```scala { val Pk = $pk From 139cef77e0a17e3cefad2de3d84337dda5c7a55e Mon Sep 17 00:00:00 2001 From: "i.oskin" Date: Sun, 21 Mar 2021 17:19:08 +0300 Subject: [PATCH 11/60] Contracts updated. --- eip-0014.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 3627dfd7..7d380bdb 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -249,11 +249,11 @@ tokens[0] | LP token reserves { val SuccessorScriptHash = $ergoSwapScriptHash // Regular ErgoSwapAMM contract hash. - val liquidityTokenId = SELF.id + val tokenIdLP = SELF.id def reservedLP(box: Box): Long = { - val maybeShares = box.tokens(0) - if (maybeShares._1 == liquidityTokenId) maybeShares._2 + val maybeLP = box.tokens(0) + if (maybeLP._1 == tokenIdLP) maybeLP._2 else 0L } @@ -385,23 +385,23 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic val baseAmount = base._2 val poolInput = INPUTS(0) - val poolAssetA = poolInput.tokens(2) - val poolAssetB = poolInput.tokens(3) + val poolAssetX = poolInput.tokens(2) + val poolAssetY = poolInput.tokens(3) val isValidPoolInput = blake2b256(poolInput.propositionBytes) == PoolScriptHash && - (poolAssetA._1 == QuoteId || poolAssetB._1 == QuoteId) && - (poolAssetA._1 == baseId || poolAssetB._1 == baseId) + (poolAssetX._1 == QuoteId || poolAssetY._1 == QuoteId) && + (poolAssetX._1 == baseId || poolAssetY._1 == baseId) - val isValidSwap = + val isValidOutput = OUTPUTS.exists { (box: Box) => val quoteAsset = box.tokens(0) val quoteAmount = quoteAsset._2 val isFairPrice = - if (poolAssetA._1 == QuoteId) - quoteAmount >= (poolAssetA._2 * baseAmount * 997) / (poolAssetB._2 * 1000 + baseAmount * 997) + if (poolAssetX._1 == QuoteId) + quoteAmount >= (poolAssetX._2.toBigInt * baseAmount * 997) / (poolAssetY._2.toBigInt * 1000 + baseAmount * 997) else - quoteAmount >= (poolAssetB._2 * baseAmount * 997) / (poolAssetA._2 * 1000 + baseAmount * 997) + quoteAmount >= (poolAssetY._2.toBigInt * baseAmount * 997) / (poolAssetX._2.toBigInt * 1000 + baseAmount * 997) box.propositionBytes == Pk.propBytes && quoteAsset._1 == QuoteId && @@ -409,7 +409,7 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic isFairPrice } - sigmaProp(Pk || (isValidPoolInput && isValidSwap)) + sigmaProp(Pk || (isValidPoolInput && isValidOutput)) } ``` \ No newline at end of file From ab3fdf58f43695aaeb4127513f2e6ce649d29b36 Mon Sep 17 00:00:00 2001 From: "i.oskin" Date: Sun, 21 Mar 2021 23:22:37 +0300 Subject: [PATCH 12/60] Order contracts updated. --- eip-0014.md | 147 ++++++++++++++++++++++++---------------------------- 1 file changed, 69 insertions(+), 78 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 7d380bdb..52eed649 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -27,28 +27,22 @@ Atomic orders can only be executed completely. Such orders can be either be aggr ```scala { - val user_pk = proveDlog(recipient); - val deadline = SELF.creationInfo._1 + 30; - - val erg_amount = SELF.value - fee; - val token_amount = erg_amount * rate / divisor; - - val valid_height = HEIGHT < deadline; - - sigmaProp(OUTPUTS.exists({ (box: Box) => - allOf(Coll( - if (valid_height) { - val t = box.tokens(0); - t._1 == token_id && - t._2 >= token_amount - } else { - // refund - box.value >= erg_amount - }, - box.R4[Coll[Byte]].get == SELF.id, - box.propositionBytes == user_pk.propBytes - )) - })) + val quoteId = SELF.R4[Coll[Byte]].get // R4 - quote tokenId + val price = SELF.R5[Long].get // R5 - price per token + val feePerToken = SELF.R6[Long].get // R6 - fee per token + + val maybeRewardBox = OUTPUTS(0) + val isValidRewardProposition = maybeRewardBox.propositionBytes == pk.propBytes + val maybeRewardToken = maybeRewardBox.tokens(0) + + val rewardTokens = + if (isValidRewardProposition && maybeRewardToken._1 == quoteId) maybeRewardToken._2 + else 0L + + val feeCharged = rewardTokens * feePerToken + val isValidReward = (SELF.value.toBigInt - feeCharged) <= rewardTokens * price + + sigmaProp(pk || isValidReward) } ``` @@ -56,27 +50,25 @@ Atomic orders can only be executed completely. Such orders can be either be aggr ```scala { - val user_pk = proveDlog(recipient); - val deadline = SELF.creationInfo._1 + 30; - - val self_tokens = SELF.tokens; - val token_amount = self_tokens(0)._2; - val erg_amount = token_amount * rate / divisor; - - val valid_height = HEIGHT < deadline; - - sigmaProp(OUTPUTS.exists({ (box: Box) => - allOf(Coll( - if (valid_height) { - box.value >= erg_amount - } else { - // refund - box.tokens == self_tokens - }, - box.R4[Coll[Byte]].get == SELF.id, - box.propositionBytes == user_pk.propBytes - )) - })) + val price = SELF.R5[Long].get // R5 - price per token + val feePerToken = SELF.R6[Long].get // R6 - fee per token + + val maybeRewardBox = OUTPUTS(0) + val isValidRewardProposition = maybeRewardBox.propositionBytes == pk.propBytes + + val ergs0 = SELF.value + val ergs1 = + if (isValidRewardProposition) maybeRewardBox.value + else 0L + + val deltaErgs = ergs1 - ergs0 + + val soldTokens = SELF.tokens(0)._2 + + val feeCharged = soldTokens * feePerToken + val isValidReward = deltaErgs.toBigInt >= soldTokens.toBigInt * price - feeCharged + + sigmaProp(pk || isValidReward) } ``` @@ -84,30 +76,32 @@ Atomic orders can only be executed completely. Such orders can be either be aggr ```scala { - val user_pk = proveDlog(recipient); - val deadline = SELF.creationInfo._1 + 30; - - val self_tokens = SELF.tokens; - val token_amount = self_tokens(0)._2; - val other_token_amount = token_amount * rate / divisor; - - val valid_height = HEIGHT < deadline; - - sigmaProp(OUTPUTS.exists({ (box: Box) => - allOf(Coll( - if (valid_height) { - val t = box.tokens(0); - t._1 == other_token_id && - t._2 >= other_token_amount - } else { - // refund - box.tokens == self_tokens - }, - box.R4[Coll[Byte]].get == SELF.id, - box.propositionBytes == user_pk.propBytes - )) - })) -} + val quoteAssetId = SELF.R4[Coll[Byte]].get // R4 - quote asset ID + val price = SELF.R5[Long].get // R5 - price per token + val feePerToken = SELF.R6[Long].get // R6 - fee per quote token + + val maybeRewardBox = OUTPUTS(0) + val maybeOutputQuoteAsset = maybeRewardBox.tokens(0) + val isValidRewardProposition = maybeRewardBox.propositionBytes == pk.propBytes + val isValidQuoteAsset = maybeOutputQuoteAsset._1 == quoteAssetId + + val ergs0 = SELF.value + val ergs1 = + if (isValidRewardProposition) maybeRewardBox.value + else 0L + + val baseInput = SELF.tokens(0)._2 + val quoteOutput = + if (isValidRewardProposition && isValidQuoteAsset) maybeOutputQuoteAsset._2 + else 0L + + val deltaErgs = ergs0 - ergs1 + + val isValidOutput = baseInput <= quoteOutput * price + val isValidFee = deltaErgs <= quoteOutput * feePerToken + + sigmaProp(pk || (isValidOutput && isValidFee)) +} ``` ### Orders with partial filling support: @@ -118,8 +112,6 @@ Partial orders are something more familiar to those who've ever used classical C ```scala { - val PrecisionConstant = 1000000000L - val quoteId = SELF.R4[Coll[Byte]].get // R4 - quote tokenId val price = SELF.R5[Long].get // R5 - price per token val feePerToken = SELF.R6[Long].get // R6 - fee per token @@ -147,7 +139,7 @@ Partial orders are something more familiar to those who've ever used classical C else 0L val feeCharged = rewardTokens * feePerToken - val isValidReward = rewardTokens.toBigInt * PrecisionConstant >= (SELF.value.toBigInt - feeCharged - leftErgs) * PrecisionConstant / price + val isValidReward = SELF.value.toBigInt - feeCharged - leftErgs <= rewardTokens.toBigInt * price sigmaProp(pk || isValidReward) } @@ -157,7 +149,7 @@ Partial orders are something more familiar to those who've ever used classical C ```scala { - val baseId = SELF.R4[Coll[Byte]].get // R4 - quote tokenId + val quoteAsset = SELF.tokens(0) val price = SELF.R5[Long].get // R5 - price per token val feePerToken = SELF.R6[Long].get // R6 - fee per token @@ -175,15 +167,14 @@ Partial orders are something more familiar to those who've ever used classical C val maybeResidualBox = OUTPUTS(1) val maybeResidualAsset = maybeResidualBox.tokens(0) val isValidResidualProposition = maybeResidualBox.propositionBytes == SELF.propositionBytes - val isValidResidualAsset = maybeResidualAsset._1 == baseId + val isValidResidualAsset = maybeResidualAsset._1 == quoteAsset._1 val isValidResidualRegisters = - maybeResidualBox.R4[Coll[Byte]].get == baseId && - maybeResidualBox.R5[Long].get == price && - maybeResidualBox.R6[Long].get == feePerToken + maybeResidualBox.R5[Long].get == price && + maybeResidualBox.R6[Long].get == feePerToken val validResidualBoxExists = hasResidualBox && isValidResidualProposition && isValidResidualAsset && isValidResidualRegisters - val tokens0 = SELF.tokens(0)._2 + val tokens0 = quoteAsset._2 val tokens1 = if (validResidualBoxExists) maybeResidualAsset._2 else 0L @@ -194,12 +185,12 @@ Partial orders are something more familiar to those who've ever used classical C val isValidReward = deltaErgs.toBigInt >= soldTokens.toBigInt * price - feeCharged sigmaProp(pk || isValidReward) -} +} ``` ### On-chain matching vs Off-chain -It is not neccessary to publish orders on chain in order for them to be matched. ErgoDEX bots can synchronize orders off-chain, match them and only then execute in chained transactions. +It is not neccessary to publish orders on chain in order for them to be matched. ErgoDEX bots can synchronize orders off-chain, match them and only then execute in chained transactions. This approach allows to avoid committing cancelled orders on-chain. ## Automated Liquidity Pools From cf2a2ca3ba98f0c0bc24f2937d5639a070d17052 Mon Sep 17 00:00:00 2001 From: "i.oskin" Date: Sun, 21 Mar 2021 23:47:47 +0300 Subject: [PATCH 13/60] AMM contracts improved. --- eip-0014.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 52eed649..f76b21c3 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -227,7 +227,7 @@ In order to avoid blowing up the pool contract with a code which handles only sp In order to preserve pool uniqueness a non-fungible token (NFT) is used. Then concrete pool can be identified by a unique NFT containing in pool UTXO. Pool NFT is created at pool initialization stage. The pool bootstrapping contract ensures this step is done correctly. -#### Structure of pool bootstrapping UTXO +#### Schema of the pool bootstrapping UTXO Section | Description ----------|------------------------ @@ -267,7 +267,7 @@ tokens[0] | LP token reserves } ``` -#### Structure of pool UTXO +#### Schema of the pool UTXO Section | Description ----------|------------------------------------------------------ @@ -332,9 +332,9 @@ R4[Long] | Desired share (Required only at initialisation stage) val isValidSwap = if (deltaReservesX > 0) - -deltaReservesY <= (reservesY0.toBigInt * deltaReservesX * 997) / (reservesX0.toBigInt * 1000 + deltaReservesX * 997) // todo: better precision const? + reservesY0.toBigInt * deltaReservesX * 997 >= -deltaReservesY * (reservesX0.toBigInt * 1000 + deltaReservesX * 997) else - -deltaReservesX <= (reservesX0.toBigInt * deltaReservesY * 997) / (reservesY0.toBigInt * 1000 + deltaReservesY * 997) + reservesX0.toBigInt * deltaReservesY * 997 >= -deltaReservesX * (reservesY0.toBigInt * 1000 + deltaReservesY * 997) val isValidAction = if (deltaSupplyLP == 0) @@ -390,9 +390,9 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic val quoteAmount = quoteAsset._2 val isFairPrice = if (poolAssetX._1 == QuoteId) - quoteAmount >= (poolAssetX._2.toBigInt * baseAmount * 997) / (poolAssetY._2.toBigInt * 1000 + baseAmount * 997) + poolAssetX._2.toBigInt * baseAmount * 997 <= quoteAmount * (poolAssetY._2.toBigInt * 1000 + baseAmount * 997) else - quoteAmount >= (poolAssetY._2.toBigInt * baseAmount * 997) / (poolAssetX._2.toBigInt * 1000 + baseAmount * 997) + poolAssetY._2.toBigInt * baseAmount * 997 <= quoteAmount * (poolAssetX._2.toBigInt * 1000 + baseAmount * 997) box.propositionBytes == Pk.propBytes && quoteAsset._1 == QuoteId && From c5b777520f7b1757147e5f5f4c594e7e5cf9233a Mon Sep 17 00:00:00 2001 From: "i.oskin" Date: Mon, 22 Mar 2021 08:55:50 +0300 Subject: [PATCH 14/60] Description improved. --- eip-0014.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eip-0014.md b/eip-0014.md index f76b21c3..f0591367 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -225,7 +225,7 @@ In order to avoid blowing up the pool contract with a code which handles only sp #### Tracking pool identity In order to preserve pool uniqueness a non-fungible token (NFT) is used. Then concrete pool can be identified by a unique NFT containing in pool UTXO. -Pool NFT is created at pool initialization stage. The pool bootstrapping contract ensures this step is done correctly. +Pool NFT is created at pool initialization stage. The pool bootstrapping contract ensures the NFT is issued while the main pool contract ensures its preservation along the whole lifecycle. #### Schema of the pool bootstrapping UTXO From 2e6f99237c3058cd6e39ecfb806e86a31a60b080 Mon Sep 17 00:00:00 2001 From: "i.oskin" Date: Mon, 22 Mar 2021 10:04:57 +0300 Subject: [PATCH 15/60] Pool bootstrapping contract updated. --- eip-0014.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index f0591367..1a2c4997 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -258,8 +258,8 @@ tokens[0] | LP token reserves val depositedX = successor.tokens(2)._2 val depositedY = successor.tokens(3)._2 val desiredShare = successor.R4[Long].get - val validDeposit = depositedX * depositedY == desiredShare * desiredShare // S = sqrt(X_deposited * Y_deposited) Deposits satisfy desired share - val validShares = reservedLP(successor) >= (reservedLP(SELF) - desiredShare) // valid amount of liquidity shares taken from reserves + val validDeposit = depositedX.toBigInt * depositedY == desiredShare.toBigInt * desiredShare // deposits satisfy desired share + val validShares = reservedLP(successor) >= (reservedLP(SELF) - desiredShare) // valid amount of liquidity shares taken from reserves validDeposit && validShares } From 3bcc0accf15722fb61d66ccb3eb16ecb5ad2e65b Mon Sep 17 00:00:00 2001 From: oskin1 Date: Mon, 29 Mar 2021 17:46:23 +0300 Subject: [PATCH 16/60] AMM contracts updated. --- eip-0014.md | 92 ++++++++++++++++++++++++++--------------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 1a2c4997..b3b5ac06 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -238,32 +238,32 @@ tokens[0] | LP token reserves ```scala { - val SuccessorScriptHash = $ergoSwapScriptHash // Regular ErgoSwapAMM contract hash. - - val tokenIdLP = SELF.id + val PoolScriptHash = $poolScriptHash - def reservedLP(box: Box): Long = { - val maybeLP = box.tokens(0) - if (maybeLP._1 == tokenIdLP) maybeLP._2 - else 0L - } + val selfLP = SELF.tokens(0) + val selfAmountLP = selfLP._2 - val successor = OUTPUTS(0) + val pool = OUTPUTS(0) + + val maybePoolLP = pool.tokens(1) + val poolAmountLP = + if (maybePoolLP._1 == selfLP._1) maybePoolLP._2 + else 0L - val isValidContract = blake2b256(successor.propositionBytes) == SuccessorScriptHash - val isValidErgAmount = successor.value >= SELF.value - val isValidPoolNFT = successor.tokens(1) == (SELF.id, 1) + val validContract = blake2b256(pool.propositionBytes) == PoolScriptHash + val validErgAmount = pool.value >= SELF.value + val validPoolNFT = pool.tokens(0) == (SELF.id, 1L) - val isValidInitialDepositing = { - val depositedX = successor.tokens(2)._2 - val depositedY = successor.tokens(3)._2 - val desiredShare = successor.R4[Long].get - val validDeposit = depositedX.toBigInt * depositedY == desiredShare.toBigInt * desiredShare // deposits satisfy desired share - val validShares = reservedLP(successor) >= (reservedLP(SELF) - desiredShare) // valid amount of liquidity shares taken from reserves + val validInitialDepositing = { + val depositedX = pool.tokens(2)._2 + val depositedY = pool.tokens(3)._2 + val desiredShare = pool.R4[Long].get + val validDeposit = depositedX.toBigInt * depositedY == desiredShare.toBigInt * desiredShare + val validShares = poolAmountLP >= (selfAmountLP - desiredShare) validDeposit && validShares } - sigmaProp(isValidContract && isValidErgAmount && isValidPoolNFT && isValidInitialDepositing) + sigmaProp(validContract && validErgAmount && validPoolNFT && validInitialDepositing) } ``` @@ -272,8 +272,8 @@ tokens[0] | LP token reserves Section | Description ----------|------------------------------------------------------ value | Constant amount of ERGs -tokens[0] | LP token reserves -tokens[1] | Pool NFT +tokens[0] | Pool NFT +tokens[1] | LP token reserves tokens[2] | Asset X tokens[3] | Asset Y R4[Long] | Desired share (Required only at initialisation stage) @@ -285,24 +285,24 @@ R4[Long] | Desired share (Required only at initialisation stage) val InitiallyLockedLP = 1000000000000000000L val ergs0 = SELF.value - val reservedLP0 = SELF.tokens(0) - val poolNFT0 = SELF.tokens(1) + val poolNFT0 = SELF.tokens(0) + val reservedLP0 = SELF.tokens(1) val tokenX0 = SELF.tokens(2) val tokenY0 = SELF.tokens(3) val successor = OUTPUTS(0) val ergs1 = successor.value - val reservedLP1 = successor.tokens(0) - val poolNFT1 = successor.tokens(1) + val poolNFT1 = successor.tokens(0) + val reservedLP1 = successor.tokens(1) val tokenX1 = successor.tokens(2) val tokenY1 = successor.tokens(3) - val isValidSuccessor = successor.propositionBytes == SELF.propositionBytes - val isValidErgs = ergs1 >= ergs0 - val isValidPoolNFT = poolNFT1 == poolNFT0 - val isValidLP = reservedLP1._1 == reservedLP0._1 - val isValidPair = tokenX1._1 == tokenX0._1 && tokenY1._1 == tokenY0._1 + val validSuccessorScript = successor.propositionBytes == SELF.propositionBytes + val pereservedErgs = ergs1 >= ergs0 + val preservedPoolNFT = poolNFT1 == poolNFT0 + val validLP = reservedLP1._1 == reservedLP0._1 + val validPair = tokenX1._1 == tokenX0._1 && tokenY1._1 == tokenY0._1 val supplyLP0 = InitiallyLockedLP - reservedLP0._2 val supplyLP1 = InitiallyLockedLP - reservedLP1._2 @@ -312,11 +312,11 @@ R4[Long] | Desired share (Required only at initialisation stage) val reservesX1 = tokenX1._2 val reservesY1 = tokenY1._2 - val deltaSupplyLP = supplyLP1 - supplyLP0 // optimize? reservedLP0._2 - reservedLP1._2 - val deltaReservesX = reservesX1 - reservesX0 - val deltaReservesY = reservesY1 - reservesY0 + val deltaSupplyLP = supplyLP1 - supplyLP0 // optimize? reservedLP0._2 - reservedLP1._2 + val deltaReservesX = reservesX1 - reservesX0 + val deltaReservesY = reservesY1 - reservesY0 - val isValidDepositing = { + val validDepositing = { val sharesUnlocked = min( deltaReservesX.toBigInt * supplyLP0 / reservesX0, deltaReservesY.toBigInt * supplyLP0 / reservesY0 @@ -324,32 +324,32 @@ R4[Long] | Desired share (Required only at initialisation stage) -deltaSupplyLP <= sharesUnlocked } - val isValidRedemption = { + val validRedemption = { val shareLP = deltaSupplyLP.toBigInt / supplyLP0 // note: shareLP and deltaReservesX, deltaReservesY are negative deltaReservesX >= shareLP * reservesX0 && deltaReservesY >= shareLP * reservesY0 } - val isValidSwap = + val validSwap = if (deltaReservesX > 0) reservesY0.toBigInt * deltaReservesX * 997 >= -deltaReservesY * (reservesX0.toBigInt * 1000 + deltaReservesX * 997) else reservesX0.toBigInt * deltaReservesY * 997 >= -deltaReservesX * (reservesY0.toBigInt * 1000 + deltaReservesY * 997) - val isValidAction = + val validAction = if (deltaSupplyLP == 0) - isValidSwap + validSwap else - if (deltaReservesX > 0 && deltaReservesY > 0) isValidDepositing - else isValidRedemption + if (deltaReservesX > 0 && deltaReservesY > 0) validDepositing + else validRedemption sigmaProp( - isValidSuccessor && - isValidErgs && - isValidPoolNFT && - isValidLP && - isValidPair && - isValidAction + validSuccessorScript && + pereservedErgs && + preservedPoolNFT && + validLP && + validPair && + validAction ) } ``` From d67f1e473ca386277ff75148cdf3ea123cb4ee9f Mon Sep 17 00:00:00 2001 From: oskin1 Date: Mon, 29 Mar 2021 22:57:16 +0300 Subject: [PATCH 17/60] Concentrated liquidity pools added. --- eip-0014.md | 269 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 265 insertions(+), 4 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index b3b5ac06..9efc627b 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -234,7 +234,7 @@ Section | Description value | Constant amount of ERGs tokens[0] | LP token reserves -#### Pool bootstrapping contract +#### Simple pool bootstrapping contract ```scala { @@ -278,7 +278,7 @@ tokens[2] | Asset X tokens[3] | Asset Y R4[Long] | Desired share (Required only at initialisation stage) -#### Pool contract +#### Simple pool contract ```scala { @@ -354,7 +354,7 @@ R4[Long] | Desired share (Required only at initialisation stage) } ``` -#### Swap contracts +#### Simple swap contracts 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 * 997 / (Y_reserved * 1000 + Y_input * 997)` @@ -403,4 +403,265 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic sigmaProp(Pk || (isValidPoolInput && isValidOutput)) } ``` - \ No newline at end of file + +#### Concentrated liquidity pools + +For those pairs with not too high price volatility it's more efficient to allow LPs to provide liquidity in a narrow price range. + +In order to implement this pool contracts introduced previously need to be slightly modified. Now the space of all possible prices is demarcated by discrete intervals (`ticks`). While in the simple implementation an entire price range was represented by a single UTxO, now it is represended by many UTxOs each corresponding to a specific price range. + +For each pair the following set of parameters which is applied to all sub-pools needs to be specified: +* `tick_step` - length of each price interval +* `fee_numerator` - numerator of a pair fee (denominator is fixed to `1000`) + +#### Initializing concentrated liquidity pools + +Pools for the entire price range can't be initialized all at once. Luckily they don't need to. Instead they are intialized one by one only when LPs whant to. In order to facilitate spawning of new pools a pool root UTXO is introduced. Its contract: +* Ensures all pools corresponding to some specific pair is instantiated with same parameters. +* Distributes special root token (RT) among all pools in order to track them. + +#### Schema of the Pool Root UTxO + +Section | Description +-----------------------------|------------------------------------------------------ +value | Constant amount of ERGs +tokens[0] | RT tokens +R4[Int] | Fee numerator +R5[(Int, Int)] | `tick_step` represented by a tuple of integers `(numerator, denominator)` +R6[(Coll[Byte], Coll[Byte])] | (assetIdX, assetIdY) + +#### Pool Root contract + +```scala +{ + val InitiallyLockedLP = 1000000000000000000L + val PoolBootScriptHash = $poolBootScriptHash + + val selfRT = SELF.tokens(0) + val tokenIdRT = selfRT._1 + val feeNumerator = SELF.R4[Int].get + val tickStep = SELF.R5[(Int, Int)].get + val assets = SELF.R6[(Coll[Byte], Coll[Byte])].get + + val successor = OUTPUTS(0) + + val successorRT = successor.tokens(0) + + val validSuccessor = + successor.propositionBytes == SELF.propositionBytes && + successor.value >= SELF.value && + successorRT == (tokenIdRT, selfRT._2 - 1) && + successor.R4[Int].get == feeNumerator && + successor.R5[(Int, Int)].get == tickStep && + successor.R6[(Coll[Byte], Coll[Byte])].get == assets + + val poolBoot = OUTPUTS(1) + + val poolBootRT = poolBoot.tokens(1) + val poolBootLP = poolBoot.tokens(0) + + val poolBootTick = poolBoot.R5[Int].get + val poolBootTickPriceLower = poolBoot.R6[(Int, Int)].get + val poolBootTickPriceUpper = poolBoot.R7[(Int, Int)].get + + val validPriceBounds = + poolBootTickPriceLower._1 * tickStep._2 == poolBootTickPriceLower._2 * tickStep._1 * poolBootTick && + poolBootTickPriceUpper._1 * tickStep._2 == poolBootTickPriceUpper._2 * tickStep._1 * (poolBootTick + 1) + + val validPoolBoot = + blake2b256(poolBoot.propositionBytes) == PoolBootScriptHash && + poolBoot.R4[Int].get == feeNumerator && // fee numerator preserved + poolBoot.R8[(Coll[Byte], Coll[Byte])].get == assets && // pair IDs preserved + poolBootRT._1 == tokenIdRT && // valid RT + poolBootLP == (SELF.id, InitiallyLockedLP) && // LP issued + validPriceBounds + + sigmaProp(validSuccessor && validPoolBoot) +} +``` + +#### Schema of the Pool Boot UTxO + +Section | Description +-----------------------------|------------------------------------------------------ +tokens[0] | LP tokens +tokens[1] | RT tokens +R4[Int] | Fee numerator +R5[Int] | Tick +R6[(Int, Int)] | Lower bound of the price range (represented by a tuple of integers `(numerator, denominator)`) +R7[(Int, Int)] | Upper bound of the price range (represented by a tuple of integers `(numerator, denominator)`) +R8[(Coll[Byte], Coll[Byte])] | Pair assets `(assetIdX, assetIdY)` + +#### Pool Boot contract + +```scala +{ + val PoolScriptHash = $poolScriptHash + + val selfLP = SELF.tokens(0) + val selfAmountLP = selfLP._2 + + val selfRT = SELF.tokens(1) + val tokenIdRT = selfRT._1 + + val feeNumerator = SELF.R4[Int].get + val priceLower = SELF.R6[(Int, Int)].get + val priceUpper = SELF.R7[(Int, Int)].get + val pair = SELF.R8[(Coll[Byte], Coll[Byte])].get + + val pool = OUTPUTS(0) + + val maybePoolLP = pool.tokens(2) + val poolAmountLP = + if (maybePoolLP._1 == selfLP._1) maybePoolLP._2 + else 0L + + val poolFeeNumerator = pool.R4[Int].get + val poolPriceLower = pool.R5[(Int, Int)].get + val poolPriceUpper = pool.R6[(Int, Int)].get + + val validContract = blake2b256(pool.propositionBytes) == PoolScriptHash + val validErgAmount = pool.value >= SELF.value + val validPoolNFT = pool.tokens(0) == (SELF.id, 1L) + val validPoolRT = pool.tokens(1) == (tokenIdRT, 1L) + val validPoolSettings = + poolFeeNumerator == feeNumerator && + poolPriceLower == priceLower && + poolPriceUpper == priceUpper + + val validInitialDepositing = { + val assetX = pool.tokens(3) + val assetY = pool.tokens(4) + val depositedX = assetX._2 + val depositedY = assetX._2 + val desiredShare = pool.R7[Long].get + + val validAssets = (assetX._1, assetY._1) == pair + val validDeposit = depositedX.toBigInt * depositedY == desiredShare.toBigInt * desiredShare + val validShares = poolAmountLP >= (selfAmountLP - desiredShare) + + val validPriceBounds = + depositedX * poolPriceLower._2 >= depositedY * poolPriceLower._1 && + depositedX * poolPriceUpper._2 < depositedY * poolPriceUpper._1 + + validAssets && validDeposit && validShares && validPriceBounds + } + + sigmaProp( + validContract && + validErgAmount && + validPoolNFT && + validPoolRT && + validPoolSettings && + validInitialDepositing + ) +} +``` + +#### Schema of the Pool UTxO + +Section | Description +-----------------------------|------------------------------------------------------ +tokens[0] | pool NFT tokens +tokens[1] | RT tokens +tokens[2] | LP tokens +tokens[3] | X tokens +tokens[4] | Y tokens +R4[Int] | Fee numerator +R5[(Int, Int)] | Lower bound of the price range (represented by a tuple of integers `(numerator, denominator)`) +R6[(Int, Int)] | Upper bound of the price range (represented by a tuple of integers `(numerator, denominator)`) +R7[Long] | Desired share (Required only at initialisation stage) + +#### Pool contract + +```scala +{ + val InitiallyLockedLP = 1000000000000000000L + val FeeDenominator = 1000 + + val ergs0 = SELF.value + val poolNFT0 = SELF.tokens(0) + val poolRT0 = SELF.tokens(1) + val reservedLP0 = SELF.tokens(2) + val tokenX0 = SELF.tokens(3) + val tokenY0 = SELF.tokens(4) + + val feeNumerator = SELF.R4[Int].get + val priceLower = SELF.R5[(Int, Int)].get + val priceUpper = SELF.R6[(Int, Int)].get + + val successor = OUTPUTS(0) + + val ergs1 = successor.value + val poolNFT1 = successor.tokens(0) + val poolRT1 = successor.tokens(1) + val reservedLP1 = successor.tokens(2) + val tokenX1 = successor.tokens(3) + val tokenY1 = successor.tokens(4) + + val validSuccessorScript = successor.propositionBytes == SELF.propositionBytes + val preservedErgs = ergs1 >= ergs0 + val preservedPoolNFT = poolNFT1 == poolNFT0 + val preservedPoolRT = poolRT1 == poolRT0 + val validLP = reservedLP1._1 == reservedLP0._1 + val validPair = tokenX1._1 == tokenX0._1 && tokenY1._1 == tokenY0._1 + val preservedRegisters = + successor.R4[Int].get == feeNumerator && + successor.R5[(Int, Int)].get == priceLower && + successor.R6[(Int, Int)].get == priceUpper + + val supplyLP0 = InitiallyLockedLP - reservedLP0._2 + val supplyLP1 = InitiallyLockedLP - reservedLP1._2 + + val reservesX0 = tokenX0._2 + val reservesY0 = tokenY0._2 + val reservesX1 = tokenX1._2 + 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 shareLP = deltaSupplyLP.toBigInt / supplyLP0 + // note: shareLP and deltaReservesX, deltaReservesY are negative + deltaReservesX >= shareLP * reservesX0 && deltaReservesY >= shareLP * reservesY0 + } + + val validSwap = + if (deltaReservesX > 0) + reservesY0.toBigInt * deltaReservesX * feeNumerator >= -deltaReservesY * (reservesX0.toBigInt * FeeDenominator + deltaReservesX * feeNumerator) + else + reservesX0.toBigInt * deltaReservesY * feeNumerator >= -deltaReservesX * (reservesY0.toBigInt * FeeDenominator + deltaReservesY * feeNumerator) + + val validPrice = + reservesX1 * priceLower._2 >= reservesY1 * priceLower._1 && + reservesX1 * priceUpper._2 < reservesY1 * priceUpper._1 + + val validAction = + if (deltaSupplyLP == 0) + validSwap && validPrice + else + if (deltaReservesX > 0 && deltaReservesY > 0) validDepositing && validPrice + else validRedemption + + sigmaProp( + validSuccessorScript && + preservedErgs && + preservedPoolNFT && + preservedPoolRT && + validLP && + validPair && + preservedRegisters && + validAction + ) +} +``` From 3d77da3d601711b954fb046b6b4b4d08d944310e Mon Sep 17 00:00:00 2001 From: oskin1 Date: Tue, 30 Mar 2021 11:55:39 +0300 Subject: [PATCH 18/60] Pool script updated. --- eip-0014.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 9efc627b..b75065d0 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -3,7 +3,7 @@ * Author: kushti, Ilya Oskin * Status: Proposed * Created: 12-Mar-2021 -* Last edited: 21-Mar-2021 +* Last edited: 30-Mar-2021 * License: CC0 * Track: Standards @@ -643,8 +643,8 @@ R7[Long] | Desired share (Required only at initialisation st reservesX0.toBigInt * deltaReservesY * feeNumerator >= -deltaReservesX * (reservesY0.toBigInt * FeeDenominator + deltaReservesY * feeNumerator) val validPrice = - reservesX1 * priceLower._2 >= reservesY1 * priceLower._1 && - reservesX1 * priceUpper._2 < reservesY1 * priceUpper._1 + reservesX1.toBigInt * priceLower._2 >= reservesY1.toBigInt * priceLower._1 && + reservesX1.toBigInt * priceUpper._2 < reservesY1.toBigInt * priceUpper._1 val validAction = if (deltaSupplyLP == 0) From b6842111db712871326f13162dc0eb7b5d6ec694 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Tue, 30 Mar 2021 17:04:41 +0300 Subject: [PATCH 19/60] CL Pools reemoved. --- eip-0014.md | 262 ---------------------------------------------------- 1 file changed, 262 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index b75065d0..2ff5a834 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -403,265 +403,3 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic sigmaProp(Pk || (isValidPoolInput && isValidOutput)) } ``` - -#### Concentrated liquidity pools - -For those pairs with not too high price volatility it's more efficient to allow LPs to provide liquidity in a narrow price range. - -In order to implement this pool contracts introduced previously need to be slightly modified. Now the space of all possible prices is demarcated by discrete intervals (`ticks`). While in the simple implementation an entire price range was represented by a single UTxO, now it is represended by many UTxOs each corresponding to a specific price range. - -For each pair the following set of parameters which is applied to all sub-pools needs to be specified: -* `tick_step` - length of each price interval -* `fee_numerator` - numerator of a pair fee (denominator is fixed to `1000`) - -#### Initializing concentrated liquidity pools - -Pools for the entire price range can't be initialized all at once. Luckily they don't need to. Instead they are intialized one by one only when LPs whant to. In order to facilitate spawning of new pools a pool root UTXO is introduced. Its contract: -* Ensures all pools corresponding to some specific pair is instantiated with same parameters. -* Distributes special root token (RT) among all pools in order to track them. - -#### Schema of the Pool Root UTxO - -Section | Description ------------------------------|------------------------------------------------------ -value | Constant amount of ERGs -tokens[0] | RT tokens -R4[Int] | Fee numerator -R5[(Int, Int)] | `tick_step` represented by a tuple of integers `(numerator, denominator)` -R6[(Coll[Byte], Coll[Byte])] | (assetIdX, assetIdY) - -#### Pool Root contract - -```scala -{ - val InitiallyLockedLP = 1000000000000000000L - val PoolBootScriptHash = $poolBootScriptHash - - val selfRT = SELF.tokens(0) - val tokenIdRT = selfRT._1 - val feeNumerator = SELF.R4[Int].get - val tickStep = SELF.R5[(Int, Int)].get - val assets = SELF.R6[(Coll[Byte], Coll[Byte])].get - - val successor = OUTPUTS(0) - - val successorRT = successor.tokens(0) - - val validSuccessor = - successor.propositionBytes == SELF.propositionBytes && - successor.value >= SELF.value && - successorRT == (tokenIdRT, selfRT._2 - 1) && - successor.R4[Int].get == feeNumerator && - successor.R5[(Int, Int)].get == tickStep && - successor.R6[(Coll[Byte], Coll[Byte])].get == assets - - val poolBoot = OUTPUTS(1) - - val poolBootRT = poolBoot.tokens(1) - val poolBootLP = poolBoot.tokens(0) - - val poolBootTick = poolBoot.R5[Int].get - val poolBootTickPriceLower = poolBoot.R6[(Int, Int)].get - val poolBootTickPriceUpper = poolBoot.R7[(Int, Int)].get - - val validPriceBounds = - poolBootTickPriceLower._1 * tickStep._2 == poolBootTickPriceLower._2 * tickStep._1 * poolBootTick && - poolBootTickPriceUpper._1 * tickStep._2 == poolBootTickPriceUpper._2 * tickStep._1 * (poolBootTick + 1) - - val validPoolBoot = - blake2b256(poolBoot.propositionBytes) == PoolBootScriptHash && - poolBoot.R4[Int].get == feeNumerator && // fee numerator preserved - poolBoot.R8[(Coll[Byte], Coll[Byte])].get == assets && // pair IDs preserved - poolBootRT._1 == tokenIdRT && // valid RT - poolBootLP == (SELF.id, InitiallyLockedLP) && // LP issued - validPriceBounds - - sigmaProp(validSuccessor && validPoolBoot) -} -``` - -#### Schema of the Pool Boot UTxO - -Section | Description ------------------------------|------------------------------------------------------ -tokens[0] | LP tokens -tokens[1] | RT tokens -R4[Int] | Fee numerator -R5[Int] | Tick -R6[(Int, Int)] | Lower bound of the price range (represented by a tuple of integers `(numerator, denominator)`) -R7[(Int, Int)] | Upper bound of the price range (represented by a tuple of integers `(numerator, denominator)`) -R8[(Coll[Byte], Coll[Byte])] | Pair assets `(assetIdX, assetIdY)` - -#### Pool Boot contract - -```scala -{ - val PoolScriptHash = $poolScriptHash - - val selfLP = SELF.tokens(0) - val selfAmountLP = selfLP._2 - - val selfRT = SELF.tokens(1) - val tokenIdRT = selfRT._1 - - val feeNumerator = SELF.R4[Int].get - val priceLower = SELF.R6[(Int, Int)].get - val priceUpper = SELF.R7[(Int, Int)].get - val pair = SELF.R8[(Coll[Byte], Coll[Byte])].get - - val pool = OUTPUTS(0) - - val maybePoolLP = pool.tokens(2) - val poolAmountLP = - if (maybePoolLP._1 == selfLP._1) maybePoolLP._2 - else 0L - - val poolFeeNumerator = pool.R4[Int].get - val poolPriceLower = pool.R5[(Int, Int)].get - val poolPriceUpper = pool.R6[(Int, Int)].get - - val validContract = blake2b256(pool.propositionBytes) == PoolScriptHash - val validErgAmount = pool.value >= SELF.value - val validPoolNFT = pool.tokens(0) == (SELF.id, 1L) - val validPoolRT = pool.tokens(1) == (tokenIdRT, 1L) - val validPoolSettings = - poolFeeNumerator == feeNumerator && - poolPriceLower == priceLower && - poolPriceUpper == priceUpper - - val validInitialDepositing = { - val assetX = pool.tokens(3) - val assetY = pool.tokens(4) - val depositedX = assetX._2 - val depositedY = assetX._2 - val desiredShare = pool.R7[Long].get - - val validAssets = (assetX._1, assetY._1) == pair - val validDeposit = depositedX.toBigInt * depositedY == desiredShare.toBigInt * desiredShare - val validShares = poolAmountLP >= (selfAmountLP - desiredShare) - - val validPriceBounds = - depositedX * poolPriceLower._2 >= depositedY * poolPriceLower._1 && - depositedX * poolPriceUpper._2 < depositedY * poolPriceUpper._1 - - validAssets && validDeposit && validShares && validPriceBounds - } - - sigmaProp( - validContract && - validErgAmount && - validPoolNFT && - validPoolRT && - validPoolSettings && - validInitialDepositing - ) -} -``` - -#### Schema of the Pool UTxO - -Section | Description ------------------------------|------------------------------------------------------ -tokens[0] | pool NFT tokens -tokens[1] | RT tokens -tokens[2] | LP tokens -tokens[3] | X tokens -tokens[4] | Y tokens -R4[Int] | Fee numerator -R5[(Int, Int)] | Lower bound of the price range (represented by a tuple of integers `(numerator, denominator)`) -R6[(Int, Int)] | Upper bound of the price range (represented by a tuple of integers `(numerator, denominator)`) -R7[Long] | Desired share (Required only at initialisation stage) - -#### Pool contract - -```scala -{ - val InitiallyLockedLP = 1000000000000000000L - val FeeDenominator = 1000 - - val ergs0 = SELF.value - val poolNFT0 = SELF.tokens(0) - val poolRT0 = SELF.tokens(1) - val reservedLP0 = SELF.tokens(2) - val tokenX0 = SELF.tokens(3) - val tokenY0 = SELF.tokens(4) - - val feeNumerator = SELF.R4[Int].get - val priceLower = SELF.R5[(Int, Int)].get - val priceUpper = SELF.R6[(Int, Int)].get - - val successor = OUTPUTS(0) - - val ergs1 = successor.value - val poolNFT1 = successor.tokens(0) - val poolRT1 = successor.tokens(1) - val reservedLP1 = successor.tokens(2) - val tokenX1 = successor.tokens(3) - val tokenY1 = successor.tokens(4) - - val validSuccessorScript = successor.propositionBytes == SELF.propositionBytes - val preservedErgs = ergs1 >= ergs0 - val preservedPoolNFT = poolNFT1 == poolNFT0 - val preservedPoolRT = poolRT1 == poolRT0 - val validLP = reservedLP1._1 == reservedLP0._1 - val validPair = tokenX1._1 == tokenX0._1 && tokenY1._1 == tokenY0._1 - val preservedRegisters = - successor.R4[Int].get == feeNumerator && - successor.R5[(Int, Int)].get == priceLower && - successor.R6[(Int, Int)].get == priceUpper - - val supplyLP0 = InitiallyLockedLP - reservedLP0._2 - val supplyLP1 = InitiallyLockedLP - reservedLP1._2 - - val reservesX0 = tokenX0._2 - val reservesY0 = tokenY0._2 - val reservesX1 = tokenX1._2 - 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 shareLP = deltaSupplyLP.toBigInt / supplyLP0 - // note: shareLP and deltaReservesX, deltaReservesY are negative - deltaReservesX >= shareLP * reservesX0 && deltaReservesY >= shareLP * reservesY0 - } - - val validSwap = - if (deltaReservesX > 0) - reservesY0.toBigInt * deltaReservesX * feeNumerator >= -deltaReservesY * (reservesX0.toBigInt * FeeDenominator + deltaReservesX * feeNumerator) - else - reservesX0.toBigInt * deltaReservesY * feeNumerator >= -deltaReservesX * (reservesY0.toBigInt * FeeDenominator + deltaReservesY * feeNumerator) - - val validPrice = - reservesX1.toBigInt * priceLower._2 >= reservesY1.toBigInt * priceLower._1 && - reservesX1.toBigInt * priceUpper._2 < reservesY1.toBigInt * priceUpper._1 - - val validAction = - if (deltaSupplyLP == 0) - validSwap && validPrice - else - if (deltaReservesX > 0 && deltaReservesY > 0) validDepositing && validPrice - else validRedemption - - sigmaProp( - validSuccessorScript && - preservedErgs && - preservedPoolNFT && - preservedPoolRT && - validLP && - validPair && - preservedRegisters && - validAction - ) -} -``` From 4b9fb2762814604c8d478c133225d4addca95a10 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Thu, 1 Apr 2021 14:11:13 +0300 Subject: [PATCH 20/60] AMM DEX economics improved. --- eip-0014.md | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 2ff5a834..a114f196 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -198,6 +198,18 @@ Unlike order-book based DEX which relies on an order-book to represent liquidity Each AMM liquidity pool is a trading venue for a pair of assets. In order to facilitate trades a liquidity pool accepts deposits of underlying assets proportional to their price rates. Whenever deposit happens a proportional amount of unique tokens known as liquidity tokens is minted. Minted liquidity tokens are distributed among liquidity providers proportional to their deposits. Liquidity providers can later exchange their liquidity tokens share for a proportional amount of underlying reserves. +## Economics of Ergo AMM DEX + +There are three types of economic agents in an AMM DEX ecosystem: +* DEXes (Parties which run DEX bots and UI) +* Liquidity providers (LPs) +* Traders + +Each agent type benefits from using DEX in their own way +* DEXes are earning fees from traders' swaps in ERGs +* LPs benefit from protocol fees paid in tokens and accumulated in liquidity pools +* Traders benefit from DEX services they use + ### Ergo AMM DEX Contracts [Arbitrary Pairs] Ergo AMM DEX relies on two types of contracts: @@ -284,6 +296,9 @@ R4[Long] | Desired share (Required only at initialisation stage) { val InitiallyLockedLP = 1000000000000000000L + val FeeNum = 997 + val FeeDenom = 1000 + val ergs0 = SELF.value val poolNFT0 = SELF.tokens(0) val reservedLP0 = SELF.tokens(1) @@ -312,7 +327,7 @@ R4[Long] | Desired share (Required only at initialisation stage) val reservesX1 = tokenX1._2 val reservesY1 = tokenY1._2 - val deltaSupplyLP = supplyLP1 - supplyLP0 // optimize? reservedLP0._2 - reservedLP1._2 + val deltaSupplyLP = supplyLP1 - supplyLP0 val deltaReservesX = reservesX1 - reservesX0 val deltaReservesY = reservesY1 - reservesY0 @@ -332,9 +347,9 @@ R4[Long] | Desired share (Required only at initialisation stage) val validSwap = if (deltaReservesX > 0) - reservesY0.toBigInt * deltaReservesX * 997 >= -deltaReservesY * (reservesX0.toBigInt * 1000 + deltaReservesX * 997) + reservesY0.toBigInt * deltaReservesX * FeeNum >= -deltaReservesY * (reservesX0.toBigInt * FeeDenom + deltaReservesX * FeeNum) else - reservesX0.toBigInt * deltaReservesY * 997 >= -deltaReservesX * (reservesY0.toBigInt * 1000 + deltaReservesY * 997) + reservesX0.toBigInt * deltaReservesY * FeeNum >= -deltaReservesX * (reservesY0.toBigInt * FeeDenom + deltaReservesY * FeeNum) val validAction = if (deltaSupplyLP == 0) @@ -358,6 +373,7 @@ R4[Long] | Desired share (Required only at initialisation stage) 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 * 997 / (Y_reserved * 1000 + Y_input * 997)` +* 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. @@ -368,8 +384,12 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic val PoolScriptHash = $poolScriptHash + val DexFeePerToken = $dexFeePerToken val MinQuoteAmount = $minQuoteAmount val QuoteId = $quoteId + + val FeeNum = 997 + val FeeDenom = 1000 val base = SELF.tokens(0) val baseId = base._1 @@ -379,27 +399,29 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic val poolAssetX = poolInput.tokens(2) val poolAssetY = poolInput.tokens(3) - val isValidPoolInput = + val validPoolInput = blake2b256(poolInput.propositionBytes) == PoolScriptHash && (poolAssetX._1 == QuoteId || poolAssetY._1 == QuoteId) && (poolAssetX._1 == baseId || poolAssetY._1 == baseId) - val isValidOutput = + val validTrade = OUTPUTS.exists { (box: Box) => val quoteAsset = box.tokens(0) val quoteAmount = quoteAsset._2 - val isFairPrice = + val fairDexFee = box.value >= SELF.value - quoteAmount * DexFeePerToken + val fairPrice = if (poolAssetX._1 == QuoteId) - poolAssetX._2.toBigInt * baseAmount * 997 <= quoteAmount * (poolAssetY._2.toBigInt * 1000 + baseAmount * 997) + poolAssetX._2.toBigInt * baseAmount * FeeNum <= quoteAmount * (poolAssetY._2.toBigInt * FeeDenom + baseAmount * FeeNum) else - poolAssetY._2.toBigInt * baseAmount * 997 <= quoteAmount * (poolAssetX._2.toBigInt * 1000 + baseAmount * 997) + poolAssetY._2.toBigInt * baseAmount * FeeNum <= quoteAmount * (poolAssetX._2.toBigInt * FeeDenom + baseAmount * FeeNum) box.propositionBytes == Pk.propBytes && quoteAsset._1 == QuoteId && quoteAsset._2 >= MinQuoteAmount && - isFairPrice + fairDexFee && + fairPrice } - sigmaProp(Pk || (isValidPoolInput && isValidOutput)) + sigmaProp(Pk || (validPoolInput && validTrade)) } ``` From b2fd5b8d2a2e4351286b8955f99ff30fef439b3e Mon Sep 17 00:00:00 2001 From: oskin1 Date: Thu, 8 Apr 2021 13:22:41 +0300 Subject: [PATCH 21/60] Pool boot proxy improved. --- eip-0014.md | 92 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 64 insertions(+), 28 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index a114f196..2cd3e637 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -225,57 +225,93 @@ Pool contract ensures the following operations are performed according to protoc - 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)` +#### Tracking pool identity + +In order to preserve pool uniqueness a non-fungible token (NFT) is used. Then concrete pool can be identified by a unique NFT containing in pool UTXO. +Pool NFT is created at pool initialization stage. The pool bootstrapping contract ensures the NFT is issued while the main pool contract ensures its preservation along the whole lifecycle. + #### Liquidity pool bootstrapping A liquidity pool is bootstrapped in two steps: -1. In order to track pro-rata LP shares of the total reserves of a new pair a unique token must be issued. As soon as tokens can’t be re-issued on Ergo the whole LP emission has to be done at once. A distribution of emitted tokens is controlled by the pool contract. -2. In order to start facilitating trades a liquidity pool must be initialised by depositing initial amounts of pair assets. For the initializing deposit the amount of LP tokens is calculated using special formula which is `LP = sqrt(X_deposited, Y_deposited)`. +1. Initial amounts of tokens are deposited to the pool bootstrapping proxy contract. For the initializing deposit the amount of LP tokens is calculated using special formula which is `LP = sqrt(X_deposited, Y_deposited)`. Also in order to track pro-rata LP shares of the total reserves of a new pair a unique token called "LP token" must be issued. As soon as tokens can’t be re-issued on Ergo the whole LP emission has to be done at once. A distribution of emitted LP tokens is controlled by pool bootstrapping and pool contracts. +2. Initial reserves of token pair and a reemainder of LP tokens left after initial depositing are passed to the pool contract. Also pool NFT is issued. Thus the pool is created in an initial state. Correctness of the state is checked by the proxy contract (This is why it is important to check whether a pool was initialized through the proxy contract. Luckily it can be done easily using pool NFT ID). In order to avoid blowing up the pool contract with a code which handles only specific intialization aspects a dedicated type of contract is used. -#### Tracking pool identity - -In order to preserve pool uniqueness a non-fungible token (NFT) is used. Then concrete pool can be identified by a unique NFT containing in pool UTXO. -Pool NFT is created at pool initialization stage. The pool bootstrapping contract ensures the NFT is issued while the main pool contract ensures its preservation along the whole lifecycle. +``` + InitialInput [X:N, Y:M] + | + | + PoolBootProxy [LP:1000000000000000000, X:N, Y:M] + | | + | | +LPRewardOut [LP:sqrt(N*M)] Pool [NFT:1, LP:1000000000000000000-sqrt(N*M), X:N, Y:M] +``` #### Schema of the pool bootstrapping UTXO -Section | Description -----------|------------------------ -value | Constant amount of ERGs -tokens[0] | LP token reserves +Section | Description +---------------|------------------------ +value | Constant amount of ERGs +tokens[0] | LP token reserves +tokens[1] | X token initial reserves +tokens[2] | Y token initial reserves +R4[Coll[Byte]] | Blake2b256 hash of the pool script +R5[Long] | Desired amount of shares LP expects to receive -#### Simple pool bootstrapping contract +#### Simple pool bootstrapping proxy contract ```scala { - val PoolScriptHash = $poolScriptHash + val InitiallyLockedLP = 1000000000000000000L + val LPOutFundingNanoErgs = 1000000L + + val poolScriptHash = SELF.R4[Coll[Byte]].get + val desiredSharesLP = SELF.R5[Long].get - val selfLP = SELF.tokens(0) - val selfAmountLP = selfLP._2 + val selfLP = SELF.tokens(0) + val selfX = SELF.tokens(1) + val selfY = SELF.tokens(2) - val pool = OUTPUTS(0) + val tokenIdLP = selfLP._1 + + val validSelfLP = selfLP._2 == InitiallyLockedLP + + val pool = OUTPUTS(0) + val sharesRewardLP = OUTPUTS(1) val maybePoolLP = pool.tokens(1) val poolAmountLP = - if (maybePoolLP._1 == selfLP._1) maybePoolLP._2 + if (maybePoolLP._1 == tokenIdLP) maybePoolLP._2 else 0L - val validContract = blake2b256(pool.propositionBytes) == PoolScriptHash - val validErgAmount = pool.value >= SELF.value - val validPoolNFT = pool.tokens(0) == (SELF.id, 1L) + val validPoolContract = blake2b256(pool.propositionBytes) == poolScriptHash + val validPoolErgAmount = pool.value == SELF.value - LPOutFundingNanoErgs + val validPoolNFT = pool.tokens(0) == (SELF.id, 1L) val validInitialDepositing = { - val depositedX = pool.tokens(2)._2 - val depositedY = pool.tokens(3)._2 - val desiredShare = pool.R4[Long].get - val validDeposit = depositedX.toBigInt * depositedY == desiredShare.toBigInt * desiredShare - val validShares = poolAmountLP >= (selfAmountLP - desiredShare) - validDeposit && validShares + val tokenX = pool.tokens(2) + val tokenY = pool.tokens(3) + val depositedX = tokenX._2 + val depositedY = tokenY._2 + + val validTokens = tokenX == selfX && tokenY == selfY + val validDeposit = depositedX.toBigInt * depositedY == desiredSharesLP.toBigInt * desiredSharesLP // S = sqrt(X_deposited * Y_deposited) Deposits satisfy desired share + val validShares = poolAmountLP >= (InitiallyLockedLP - desiredSharesLP) // valid amount of liquidity shares taken from reserves + + validTokens && validDeposit && validShares } + + val validPool = validPoolContract && validPoolErgAmount && validPoolNFT && validInitialDepositing + + val initialDepositorProp = INPUTS(0).propositionBytes + + val validSharesRewardLP = + sharesRewardLP.propositionBytes == initialDepositorProp && + sharesRewardLP.tokens(0) == (tokenIdLP, desiredSharesLP) - sigmaProp(validContract && validErgAmount && validPoolNFT && validInitialDepositing) + sigmaProp(validSelfLP && validPool && validSharesRewardLP) } ``` @@ -385,7 +421,7 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic val PoolScriptHash = $poolScriptHash val DexFeePerToken = $dexFeePerToken - val MinQuoteAmount = $minQuoteAmount + val MinQuoteOutput = $minQuoteOutput val QuoteId = $quoteId val FeeNum = 997 @@ -417,7 +453,7 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic box.propositionBytes == Pk.propBytes && quoteAsset._1 == QuoteId && - quoteAsset._2 >= MinQuoteAmount && + quoteAsset._2 >= MinQuoteOutput && fairDexFee && fairPrice } From 30afb37ed9ac6169df5d75fa60e1dd59f7b4ce2a Mon Sep 17 00:00:00 2001 From: oskin1 Date: Thu, 8 Apr 2021 13:25:36 +0300 Subject: [PATCH 22/60] Scheme formatting. --- eip-0014.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 2cd3e637..d0bcc640 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -3,7 +3,7 @@ * Author: kushti, Ilya Oskin * Status: Proposed * Created: 12-Mar-2021 -* Last edited: 30-Mar-2021 +* Last edited: 8-Apr-2021 * License: CC0 * Track: Standards @@ -240,13 +240,13 @@ A liquidity pool is bootstrapped in two steps: In order to avoid blowing up the pool contract with a code which handles only specific intialization aspects a dedicated type of contract is used. ``` - InitialInput [X:N, Y:M] - | - | - PoolBootProxy [LP:1000000000000000000, X:N, Y:M] - | | - | | -LPRewardOut [LP:sqrt(N*M)] Pool [NFT:1, LP:1000000000000000000-sqrt(N*M), X:N, Y:M] + InitialInput [X:N, Y:M] + 1. | + | + PoolBootProxy [LP:1000000000000000000, X:N, Y:M] + 2. | | + | | + LPRewardOut [LP:sqrt(N*M)] Pool [NFT:1, LP:1000000000000000000-sqrt(N*M), X:N, Y:M] ``` #### Schema of the pool bootstrapping UTXO From e05ea3288042d12300a91a7cc48a4530543bd239 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Thu, 8 Apr 2021 13:43:50 +0300 Subject: [PATCH 23/60] Configurabl fee. --- eip-0014.md | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index d0bcc640..91a3a132 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -269,6 +269,7 @@ R5[Long] | Desired amount of shares LP expects to receive val poolScriptHash = SELF.R4[Coll[Byte]].get val desiredSharesLP = SELF.R5[Long].get + val poolFeeConfig = SELF.R6[Long].get val selfLP = SELF.tokens(0) val selfX = SELF.tokens(1) @@ -276,7 +277,9 @@ R5[Long] | Desired amount of shares LP expects to receive val tokenIdLP = selfLP._1 - val validSelfLP = selfLP._2 == InitiallyLockedLP + // self checks + val validSelfLP = selfLP._2 == InitiallyLockedLP // Correct amount of LP tokens issued + val validSelfPoolFeeConfig = poolFeeConfig <= 1000L && poolFeeConfig > 750L // Correct pool fee config val pool = OUTPUTS(0) val sharesRewardLP = OUTPUTS(1) @@ -289,6 +292,7 @@ R5[Long] | Desired amount of shares LP expects to receive val validPoolContract = blake2b256(pool.propositionBytes) == poolScriptHash val validPoolErgAmount = pool.value == SELF.value - LPOutFundingNanoErgs val validPoolNFT = pool.tokens(0) == (SELF.id, 1L) + val validPoolConfig = pool.R4[Long].get == poolFeeConfig val validInitialDepositing = { val tokenX = pool.tokens(2) @@ -297,8 +301,8 @@ R5[Long] | Desired amount of shares LP expects to receive val depositedY = tokenY._2 val validTokens = tokenX == selfX && tokenY == selfY - val validDeposit = depositedX.toBigInt * depositedY == desiredSharesLP.toBigInt * desiredSharesLP // S = sqrt(X_deposited * Y_deposited) Deposits satisfy desired share - val validShares = poolAmountLP >= (InitiallyLockedLP - desiredSharesLP) // valid amount of liquidity shares taken from reserves + val validDeposit = depositedX.toBigInt * depositedY == desiredSharesLP.toBigInt * desiredSharesLP + val validShares = poolAmountLP >= (InitiallyLockedLP - desiredSharesLP) validTokens && validDeposit && validShares } @@ -311,7 +315,7 @@ R5[Long] | Desired amount of shares LP expects to receive sharesRewardLP.propositionBytes == initialDepositorProp && sharesRewardLP.tokens(0) == (tokenIdLP, desiredSharesLP) - sigmaProp(validSelfLP && validPool && validSharesRewardLP) + sigmaProp(validSelfLP && validSelfPoolFeeConfig && validPool && validSharesRewardLP) } ``` @@ -332,7 +336,7 @@ R4[Long] | Desired share (Required only at initialisation stage) { val InitiallyLockedLP = 1000000000000000000L - val FeeNum = 997 + val feeNum0 = SELF.R4[Long].get val FeeDenom = 1000 val ergs0 = SELF.value @@ -343,6 +347,8 @@ R4[Long] | Desired share (Required only at initialisation stage) val successor = OUTPUTS(0) + val feeNum1 = successor.R4[Long].get + val ergs1 = successor.value val poolNFT1 = successor.tokens(0) val reservedLP1 = successor.tokens(1) @@ -350,7 +356,8 @@ R4[Long] | Desired share (Required only at initialisation stage) val tokenY1 = successor.tokens(3) val validSuccessorScript = successor.propositionBytes == SELF.propositionBytes - val pereservedErgs = ergs1 >= ergs0 + val preservedFeeConfig = feeNum1 == feeNum0 + val preservedErgs = ergs1 >= ergs0 val preservedPoolNFT = poolNFT1 == poolNFT0 val validLP = reservedLP1._1 == reservedLP0._1 val validPair = tokenX1._1 == tokenX0._1 && tokenY1._1 == tokenY0._1 @@ -376,16 +383,15 @@ R4[Long] | Desired share (Required only at initialisation stage) } val validRedemption = { - val shareLP = deltaSupplyLP.toBigInt / supplyLP0 - // note: shareLP and deltaReservesX, deltaReservesY are negative + val shareLP = deltaSupplyLP.toBigInt / supplyLP0 // note: shareLP and deltaReservesX, deltaReservesY are negative deltaReservesX >= shareLP * reservesX0 && deltaReservesY >= shareLP * reservesY0 } val validSwap = if (deltaReservesX > 0) - reservesY0.toBigInt * deltaReservesX * FeeNum >= -deltaReservesY * (reservesX0.toBigInt * FeeDenom + deltaReservesX * FeeNum) + reservesY0.toBigInt * deltaReservesX * feeNum0 >= -deltaReservesY * (reservesX0.toBigInt * FeeDenom + deltaReservesX * feeNum0) else - reservesX0.toBigInt * deltaReservesY * FeeNum >= -deltaReservesX * (reservesY0.toBigInt * FeeDenom + deltaReservesY * FeeNum) + reservesX0.toBigInt * deltaReservesY * feeNum0 >= -deltaReservesX * (reservesY0.toBigInt * FeeDenom + deltaReservesY * feeNum0) val validAction = if (deltaSupplyLP == 0) @@ -396,7 +402,8 @@ R4[Long] | Desired share (Required only at initialisation stage) sigmaProp( validSuccessorScript && - pereservedErgs && + preservedFeeConfig && + preservedErgs && preservedPoolNFT && validLP && validPair && From 26d773df8e08b640a12f8dfc64e8ac9a640e4a91 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Thu, 8 Apr 2021 20:17:40 +0300 Subject: [PATCH 24/60] AMM description updated. Missing context vars added. --- eip-0014.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 91a3a132..406bb66f 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -259,6 +259,7 @@ tokens[1] | X token initial reserves tokens[2] | Y token initial reserves R4[Coll[Byte]] | Blake2b256 hash of the pool script R5[Long] | Desired amount of shares LP expects to receive +R6[Long] | Pre-configured pool fee multiplier numerator #### Simple pool bootstrapping proxy contract @@ -328,7 +329,7 @@ tokens[0] | Pool NFT tokens[1] | LP token reserves tokens[2] | Asset X tokens[3] | Asset Y -R4[Long] | Desired share (Required only at initialisation stage) +R4[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) #### Simple pool contract @@ -415,7 +416,7 @@ R4[Long] | Desired share (Required only at initialisation stage) #### Simple swap contracts 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 * 997 / (Y_reserved * 1000 + Y_input * 997)` +* 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. @@ -431,7 +432,7 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic val MinQuoteOutput = $minQuoteOutput val QuoteId = $quoteId - val FeeNum = 997 + val FeeNum = $poolFeeNum val FeeDenom = 1000 val base = SELF.tokens(0) From 2723c541e29ce59c7a7a4900391d893c61f077b4 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Fri, 9 Apr 2021 09:05:05 +0300 Subject: [PATCH 25/60] AMM proxy script improved. --- eip-0014.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 406bb66f..7b7aa623 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -266,7 +266,6 @@ R6[Long] | Pre-configured pool fee multiplier numerator ```scala { val InitiallyLockedLP = 1000000000000000000L - val LPOutFundingNanoErgs = 1000000L val poolScriptHash = SELF.R4[Coll[Byte]].get val desiredSharesLP = SELF.R5[Long].get @@ -291,7 +290,7 @@ R6[Long] | Pre-configured pool fee multiplier numerator else 0L val validPoolContract = blake2b256(pool.propositionBytes) == poolScriptHash - val validPoolErgAmount = pool.value == SELF.value - LPOutFundingNanoErgs + val validPoolErgAmount = pool.value == SELF.value - sharesRewardLP.value val validPoolNFT = pool.tokens(0) == (SELF.id, 1L) val validPoolConfig = pool.R4[Long].get == poolFeeConfig @@ -302,8 +301,8 @@ R6[Long] | Pre-configured pool fee multiplier numerator val depositedY = tokenY._2 val validTokens = tokenX == selfX && tokenY == selfY - val validDeposit = depositedX.toBigInt * depositedY == desiredSharesLP.toBigInt * desiredSharesLP - val validShares = poolAmountLP >= (InitiallyLockedLP - desiredSharesLP) + val validDeposit = depositedX.toBigInt * depositedY == desiredSharesLP.toBigInt * desiredSharesLP // S = sqrt(X_deposited * Y_deposited) Deposits satisfy desired share + val validShares = poolAmountLP >= (InitiallyLockedLP - desiredSharesLP) // valid amount of liquidity shares taken from reserves validTokens && validDeposit && validShares } From 2c201ca1abc01beb6f43b8d008d3cf02f63fbe27 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Mon, 12 Apr 2021 11:52:30 +0300 Subject: [PATCH 26/60] AMM proxy script improved. --- eip-0014.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eip-0014.md b/eip-0014.md index 7b7aa623..864fc37b 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -270,6 +270,7 @@ R6[Long] | Pre-configured pool fee multiplier numerator val poolScriptHash = SELF.R4[Coll[Byte]].get val desiredSharesLP = SELF.R5[Long].get val poolFeeConfig = SELF.R6[Long].get + val minerFeeNErgs = SELF.R7[Long].get val selfLP = SELF.tokens(0) val selfX = SELF.tokens(1) @@ -290,7 +291,7 @@ R6[Long] | Pre-configured pool fee multiplier numerator else 0L val validPoolContract = blake2b256(pool.propositionBytes) == poolScriptHash - val validPoolErgAmount = pool.value == SELF.value - sharesRewardLP.value + val validPoolErgAmount = pool.value == SELF.value - sharesRewardLP.value - minerFeeNErgs val validPoolNFT = pool.tokens(0) == (SELF.id, 1L) val validPoolConfig = pool.R4[Long].get == poolFeeConfig From d8ea467bc6f3940961c43f5e2336837cb2fb44eb Mon Sep 17 00:00:00 2001 From: oskin1 Date: Fri, 16 Apr 2021 10:17:50 +0300 Subject: [PATCH 27/60] Depositing proxy added. --- eip-0014.md | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/eip-0014.md b/eip-0014.md index 864fc37b..d659f208 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -413,7 +413,7 @@ R4[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) } ``` -#### Simple swap contracts +#### Simple 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)` @@ -469,3 +469,44 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic sigmaProp(Pk || (validPoolInput && validTrade)) } ``` + +#### Generic depositing proxy-contract + +Depositing contract ensures a liquidity provider gets fair amount of LP tokens. + +```scala +{ + val InitiallyLockedLP = 1000000000000000000L + + val PoolNFT = $poolNFT + val Pk = $pk + + val selfX = SELF.tokens(0) + val selfY = SELF.tokens(1) + + val poolIn = INPUTS(0) + + val validPoolIn = poolIn.tokens(0) == (PoolNFT, 1L) + + val poolLP = poolIn.tokens(1) + val reservesX = poolIn.tokens(2) + val reservesY = poolIn.tokens(2) + + val supplyLP = InitiallyLockedLP - poolLP._2 + + val minimalReward = min( + selfX.toBigInt * supplyLP / reservesX, + selfY.toBigInt * supplyLP / reservesY + ) + + val rewardOut = OUTPUTS(1) + val rewardLP = rewardOut.tokens(0) + + val validRewardOut = + rewardOut.propositionBytes == Pk.propBytes && + rewardLP._1 == poolLP._1 && + rewardLP._2 >= minimalReward + + sigmaProp(Pk || (validPoolIn && validRewardOut)) +} +``` From ca7b7fa283f0f325d1d82bcc4ad82dc8f24e0a4b Mon Sep 17 00:00:00 2001 From: oskin1 Date: Fri, 16 Apr 2021 12:45:51 +0300 Subject: [PATCH 28/60] Depositing contract improved. --- eip-0014.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eip-0014.md b/eip-0014.md index d659f208..70191041 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -3,7 +3,7 @@ * Author: kushti, Ilya Oskin * Status: Proposed * Created: 12-Mar-2021 -* Last edited: 8-Apr-2021 +* Last edited: 16-Apr-2021 * License: CC0 * Track: Standards @@ -504,6 +504,7 @@ Depositing contract ensures a liquidity provider gets fair amount of LP tokens. val validRewardOut = rewardOut.propositionBytes == Pk.propBytes && + rewardOut.value >= SELF.value && rewardLP._1 == poolLP._1 && rewardLP._2 >= minimalReward From c744b2cba3d7c34022e019da885953c5ccd0cf3d Mon Sep 17 00:00:00 2001 From: oskin1 Date: Fri, 23 Apr 2021 12:40:10 +0300 Subject: [PATCH 29/60] Pool boot contract fixed. --- eip-0014.md | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 70191041..cc0a3a4f 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -3,7 +3,7 @@ * Author: kushti, Ilya Oskin * Status: Proposed * Created: 12-Mar-2021 -* Last edited: 16-Apr-2021 +* Last edited: 23-Apr-2021 * License: CC0 * Track: Standards @@ -257,9 +257,11 @@ value | Constant amount of ERGs tokens[0] | LP token reserves tokens[1] | X token initial reserves tokens[2] | Y token initial reserves -R4[Coll[Byte]] | Blake2b256 hash of the pool script -R5[Long] | Desired amount of shares LP expects to receive -R6[Long] | Pre-configured pool fee multiplier numerator +R5[Coll[Byte]] | Blake2b256 hash of the pool script +R6[Long] | Desired amount of shares LP expects to receive +R7[Long] | Pre-configured pool fee multiplier numerator +R8[Long] | Miner fee +R9[Coll[Byte]] | Initiator proposition bytes #### Simple pool bootstrapping proxy contract @@ -267,10 +269,11 @@ R6[Long] | Pre-configured pool fee multiplier numerator { val InitiallyLockedLP = 1000000000000000000L - val poolScriptHash = SELF.R4[Coll[Byte]].get - val desiredSharesLP = SELF.R5[Long].get - val poolFeeConfig = SELF.R6[Long].get - val minerFeeNErgs = SELF.R7[Long].get + val poolScriptHash = SELF.R5[Coll[Byte]].get + val desiredSharesLP = SELF.R6[Long].get + val poolFeeConfig = SELF.R7[Long].get + val minerFeeNErgs = SELF.R8[Long].get + val initiatorProp = SELF.R9[Coll[Byte]].get val selfLP = SELF.tokens(0) val selfX = SELF.tokens(1) @@ -293,7 +296,7 @@ R6[Long] | Pre-configured pool fee multiplier numerator val validPoolContract = blake2b256(pool.propositionBytes) == poolScriptHash val validPoolErgAmount = pool.value == SELF.value - sharesRewardLP.value - minerFeeNErgs val validPoolNFT = pool.tokens(0) == (SELF.id, 1L) - val validPoolConfig = pool.R4[Long].get == poolFeeConfig + val validPoolConfig = pool.R9[Long].get == poolFeeConfig val validInitialDepositing = { val tokenX = pool.tokens(2) @@ -302,7 +305,7 @@ R6[Long] | Pre-configured pool fee multiplier numerator val depositedY = tokenY._2 val validTokens = tokenX == selfX && tokenY == selfY - val validDeposit = depositedX.toBigInt * depositedY == desiredSharesLP.toBigInt * desiredSharesLP // S = sqrt(X_deposited * Y_deposited) Deposits satisfy desired share + val validDeposit = depositedX.toBigInt * depositedY >= desiredSharesLP.toBigInt * desiredSharesLP // S >= sqrt(X_deposited * Y_deposited) Deposits satisfy desired share val validShares = poolAmountLP >= (InitiallyLockedLP - desiredSharesLP) // valid amount of liquidity shares taken from reserves validTokens && validDeposit && validShares @@ -310,10 +313,8 @@ R6[Long] | Pre-configured pool fee multiplier numerator val validPool = validPoolContract && validPoolErgAmount && validPoolNFT && validInitialDepositing - val initialDepositorProp = INPUTS(0).propositionBytes - val validSharesRewardLP = - sharesRewardLP.propositionBytes == initialDepositorProp && + sharesRewardLP.propositionBytes == initiatorProp && sharesRewardLP.tokens(0) == (tokenIdLP, desiredSharesLP) sigmaProp(validSelfLP && validSelfPoolFeeConfig && validPool && validSharesRewardLP) @@ -329,7 +330,7 @@ tokens[0] | Pool NFT tokens[1] | LP token reserves tokens[2] | Asset X tokens[3] | Asset Y -R4[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) +R9[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) #### Simple pool contract @@ -337,7 +338,7 @@ R4[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) { val InitiallyLockedLP = 1000000000000000000L - val feeNum0 = SELF.R4[Long].get + val feeNum0 = SELF.R9[Long].get val FeeDenom = 1000 val ergs0 = SELF.value @@ -348,7 +349,7 @@ R4[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) val successor = OUTPUTS(0) - val feeNum1 = successor.R4[Long].get + val feeNum1 = successor.R9[Long].get val ergs1 = successor.value val poolNFT1 = successor.tokens(0) @@ -384,7 +385,8 @@ R4[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) } val validRedemption = { - val shareLP = deltaSupplyLP.toBigInt / supplyLP0 // note: shareLP and deltaReservesX, deltaReservesY are negative + val shareLP = deltaSupplyLP.toBigInt / supplyLP0 + // note: shareLP and deltaReservesX, deltaReservesY are negative deltaReservesX >= shareLP * reservesX0 && deltaReservesY >= shareLP * reservesY0 } From e48d7853f3704edddc286e5bcac008a14e8da6ff Mon Sep 17 00:00:00 2001 From: oskin1 Date: Sat, 24 Apr 2021 17:39:30 +0300 Subject: [PATCH 30/60] AMM contracts updated. --- eip-0014.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index cc0a3a4f..1ca1bd8f 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -296,7 +296,7 @@ R9[Coll[Byte]] | Initiator proposition bytes val validPoolContract = blake2b256(pool.propositionBytes) == poolScriptHash val validPoolErgAmount = pool.value == SELF.value - sharesRewardLP.value - minerFeeNErgs val validPoolNFT = pool.tokens(0) == (SELF.id, 1L) - val validPoolConfig = pool.R9[Long].get == poolFeeConfig + val validPoolConfig = pool.R4[Long].get == poolFeeConfig val validInitialDepositing = { val tokenX = pool.tokens(2) @@ -338,7 +338,7 @@ R9[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) { val InitiallyLockedLP = 1000000000000000000L - val feeNum0 = SELF.R9[Long].get + val feeNum0 = SELF.R4[Long].get val FeeDenom = 1000 val ergs0 = SELF.value @@ -349,7 +349,7 @@ R9[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) val successor = OUTPUTS(0) - val feeNum1 = successor.R9[Long].get + val feeNum1 = successor.R4[Long].get val ergs1 = successor.value val poolNFT1 = successor.tokens(0) @@ -431,9 +431,11 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic val PoolScriptHash = $poolScriptHash val DexFeePerToken = $dexFeePerToken - val MinQuoteOutput = $minQuoteOutput + val MinQuoteAmount = $minQuoteAmount val QuoteId = $quoteId + val MinerFee = $minerFee + val FeeNum = $poolFeeNum val FeeDenom = 1000 @@ -454,16 +456,16 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic OUTPUTS.exists { (box: Box) => val quoteAsset = box.tokens(0) val quoteAmount = quoteAsset._2 - val fairDexFee = box.value >= SELF.value - quoteAmount * DexFeePerToken + val fairDexFee = box.value >= SELF.value - MinerFee - quoteAmount * DexFeePerToken val fairPrice = if (poolAssetX._1 == QuoteId) - poolAssetX._2.toBigInt * baseAmount * FeeNum <= quoteAmount * (poolAssetY._2.toBigInt * FeeDenom + baseAmount * FeeNum) + poolAssetX._2.toBigInt * baseAmount * FeeNum >= quoteAmount * (poolAssetY._2.toBigInt * FeeDenom + baseAmount * FeeNum) else - poolAssetY._2.toBigInt * baseAmount * FeeNum <= quoteAmount * (poolAssetX._2.toBigInt * FeeDenom + baseAmount * FeeNum) + poolAssetY._2.toBigInt * baseAmount * FeeNum >= quoteAmount * (poolAssetX._2.toBigInt * FeeDenom + baseAmount * FeeNum) box.propositionBytes == Pk.propBytes && quoteAsset._1 == QuoteId && - quoteAsset._2 >= MinQuoteOutput && + quoteAsset._2 >= MinQuoteAmount && fairDexFee && fairPrice } From 7927fbe0c1caaacca3a07fb48b3fc9969e0ebb90 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Mon, 26 Apr 2021 14:39:09 +0300 Subject: [PATCH 31/60] AMM swap proxy fixed. --- eip-0014.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 1ca1bd8f..e28fc2fd 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -454,14 +454,15 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic val validTrade = OUTPUTS.exists { (box: Box) => - val quoteAsset = box.tokens(0) - val quoteAmount = quoteAsset._2 - val fairDexFee = box.value >= SELF.value - MinerFee - quoteAmount * DexFeePerToken - val fairPrice = + val quoteAsset = box.tokens(0) + val quoteAmount = quoteAsset._2 + val fairDexFee = box.value >= SELF.value - MinerFee - quoteAmount * DexFeePerToken + val relaxedInput = baseAmount - 1 + val fairPrice = if (poolAssetX._1 == QuoteId) - poolAssetX._2.toBigInt * baseAmount * FeeNum >= quoteAmount * (poolAssetY._2.toBigInt * FeeDenom + baseAmount * FeeNum) + poolAssetX._2.toBigInt * relaxedInput * FeeNum <= quoteAmount * (poolAssetY._2.toBigInt * FeeDenom + relaxedInput * FeeNum) else - poolAssetY._2.toBigInt * baseAmount * FeeNum >= quoteAmount * (poolAssetX._2.toBigInt * FeeDenom + baseAmount * FeeNum) + poolAssetY._2.toBigInt * relaxedInput * FeeNum <= quoteAmount * (poolAssetX._2.toBigInt * FeeDenom + relaxedInput * FeeNum) box.propositionBytes == Pk.propBytes && quoteAsset._1 == QuoteId && From e1562b8fdf401f3d9726fed6628b0b0e75579eed Mon Sep 17 00:00:00 2001 From: oskin1 Date: Thu, 6 May 2021 17:55:22 +0300 Subject: [PATCH 32/60] AMM contract bugfix. --- eip-0014.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index e28fc2fd..8949cf1c 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -381,13 +381,13 @@ R9[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) deltaReservesX.toBigInt * supplyLP0 / reservesX0, deltaReservesY.toBigInt * supplyLP0 / reservesY0 ) - -deltaSupplyLP <= sharesUnlocked + deltaSupplyLP <= sharesUnlocked } val validRedemption = { - val shareLP = deltaSupplyLP.toBigInt / supplyLP0 - // note: shareLP and deltaReservesX, deltaReservesY are negative - deltaReservesX >= shareLP * reservesX0 && deltaReservesY >= shareLP * reservesY0 + val _deltaSupplyLP = deltaSupplyLP.toBigInt + // note: _deltaSupplyLP and deltaReservesX, deltaReservesY are negative + deltaReservesX.toBigInt * supplyLP0 >= _deltaSupplyLP * reservesX0 && deltaReservesY.toBigInt * supplyLP0 >= _deltaSupplyLP * reservesY0 } val validSwap = From b454c3ca87b7fd6f83ea24de6c6b53e98dd85c73 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Fri, 7 May 2021 19:31:22 +0300 Subject: [PATCH 33/60] Minor corrections. --- eip-0014.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 8949cf1c..fb27e3a9 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -3,7 +3,7 @@ * Author: kushti, Ilya Oskin * Status: Proposed * Created: 12-Mar-2021 -* Last edited: 23-Apr-2021 +* Last edited: 7-May-2021 * License: CC0 * Track: Standards @@ -330,7 +330,7 @@ tokens[0] | Pool NFT tokens[1] | LP token reserves tokens[2] | Asset X tokens[3] | Asset Y -R9[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) +R4[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) #### Simple pool contract From bc625925c7f7b4cfdd7dd4088885a0ade569a0ea Mon Sep 17 00:00:00 2001 From: oskin1 Date: Sun, 30 May 2021 22:21:46 +0300 Subject: [PATCH 34/60] AMM DEX improvements. --- eip-0014.md | 98 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index fb27e3a9..30246b04 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -3,7 +3,7 @@ * Author: kushti, Ilya Oskin * Status: Proposed * Created: 12-Mar-2021 -* Last edited: 7-May-2021 +* Last edited: 30-May-2021 * License: CC0 * Track: Standards @@ -138,8 +138,9 @@ Partial orders are something more familiar to those who've ever used classical C if (validResidualBoxExists) maybeResidualBox.value else 0L - val feeCharged = rewardTokens * feePerToken - val isValidReward = SELF.value.toBigInt - feeCharged - leftErgs <= rewardTokens.toBigInt * price + val feeCharged = rewardTokens * feePerToken + val nanoErgsConsumed = SELF.value.toBigInt - feeCharged - leftErgs + val isValidReward = nanoErgsConsumed <= rewardTokens.toBigInt * price sigmaProp(pk || isValidReward) } @@ -225,6 +226,13 @@ Pool contract ensures the following operations are performed according to protoc - 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 being deposited to a pool +- `Y_deposited` - Amount of the second asset being deposited to a pool +- `X_reserved` - Amount of the first asset locked in a pool +- `Y_reserved` - Amount of the second asset locked in a pool +- `LP_supply` - LP tokens circulating supply + #### Tracking pool identity In order to preserve pool uniqueness a non-fungible token (NFT) is used. Then concrete pool can be identified by a unique NFT containing in pool UTXO. @@ -234,8 +242,8 @@ Pool NFT is created at pool initialization stage. The pool bootstrapping contrac A liquidity pool is bootstrapped in two steps: -1. Initial amounts of tokens are deposited to the pool bootstrapping proxy contract. For the initializing deposit the amount of LP tokens is calculated using special formula which is `LP = sqrt(X_deposited, Y_deposited)`. Also in order to track pro-rata LP shares of the total reserves of a new pair a unique token called "LP token" must be issued. As soon as tokens can’t be re-issued on Ergo the whole LP emission has to be done at once. A distribution of emitted LP tokens is controlled by pool bootstrapping and pool contracts. -2. Initial reserves of token pair and a reemainder of LP tokens left after initial depositing are passed to the pool contract. Also pool NFT is issued. Thus the pool is created in an initial state. Correctness of the state is checked by the proxy contract (This is why it is important to check whether a pool was initialized through the proxy contract. Luckily it can be done easily using pool NFT ID). +1. Initial amounts of tokens are deposited to the pool bootstrapping proxy contract. For the initializing deposit the amount of LP tokens is calculated using special formula which is `LP = sqrt(X_deposited * Y_deposited)`. Also in order to track pro-rata LP shares of the total reserves of a new pair a unique token called "LP token" must be issued. As soon as tokens can’t be re-issued on Ergo the whole LP emission has to be done at once. +2. Initial reserves of token pair and a remainder of LP tokens left after initial depositing are passed to the pool contract. Also pool NFT is issued. Thus the pool is created in an initial state. Correctness of the state is checked by the proxy contract (This is why it is important to check whether a pool was initialized through the proxy contract. Luckily it can be done easily using pool NFT ID). In order to avoid blowing up the pool contract with a code which handles only specific intialization aspects a dedicated type of contract is used. @@ -263,12 +271,10 @@ R7[Long] | Pre-configured pool fee multiplier numerator R8[Long] | Miner fee R9[Coll[Byte]] | Initiator proposition bytes -#### Simple pool bootstrapping proxy contract +#### Pool bootstrapping contract ```scala { - val InitiallyLockedLP = 1000000000000000000L - val poolScriptHash = SELF.R5[Coll[Byte]].get val desiredSharesLP = SELF.R6[Long].get val poolFeeConfig = SELF.R7[Long].get @@ -281,10 +287,6 @@ R9[Coll[Byte]] | Initiator proposition bytes val tokenIdLP = selfLP._1 - // self checks - val validSelfLP = selfLP._2 == InitiallyLockedLP // Correct amount of LP tokens issued - val validSelfPoolFeeConfig = poolFeeConfig <= 1000L && poolFeeConfig > 750L // Correct pool fee config - val pool = OUTPUTS(0) val sharesRewardLP = OUTPUTS(1) @@ -311,7 +313,7 @@ R9[Coll[Byte]] | Initiator proposition bytes validTokens && validDeposit && validShares } - val validPool = validPoolContract && validPoolErgAmount && validPoolNFT && validInitialDepositing + val validPool = validPoolContract && validPoolErgAmount && validPoolNFT && validPoolConfig && validInitialDepositing val validSharesRewardLP = sharesRewardLP.propositionBytes == initiatorProp && @@ -332,12 +334,10 @@ tokens[2] | Asset X tokens[3] | Asset Y R4[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) -#### Simple pool contract +#### Pool contract ```scala { - val InitiallyLockedLP = 1000000000000000000L - val feeNum0 = SELF.R4[Long].get val FeeDenom = 1000 @@ -415,7 +415,7 @@ R4[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) } ``` -#### Simple swap proxy-contract +#### 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)` @@ -426,17 +426,6 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic ```scala { - val Pk = $pk - - val PoolScriptHash = $poolScriptHash - - val DexFeePerToken = $dexFeePerToken - val MinQuoteAmount = $minQuoteAmount - val QuoteId = $quoteId - - val MinerFee = $minerFee - - val FeeNum = $poolFeeNum val FeeDenom = 1000 val base = SELF.tokens(0) @@ -456,7 +445,7 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic OUTPUTS.exists { (box: Box) => val quoteAsset = box.tokens(0) val quoteAmount = quoteAsset._2 - val fairDexFee = box.value >= SELF.value - MinerFee - quoteAmount * DexFeePerToken + val fairDexFee = box.value >= SELF.value - quoteAmount * DexFeePerToken val relaxedInput = baseAmount - 1 val fairPrice = if (poolAssetX._1 == QuoteId) @@ -475,17 +464,12 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic } ``` -#### Generic depositing proxy-contract +#### Depositing proxy-contract Depositing contract ensures a liquidity provider gets fair amount of LP tokens. ```scala { - val InitiallyLockedLP = 1000000000000000000L - - val PoolNFT = $poolNFT - val Pk = $pk - val selfX = SELF.tokens(0) val selfY = SELF.tokens(1) @@ -500,8 +484,8 @@ Depositing contract ensures a liquidity provider gets fair amount of LP tokens. val supplyLP = InitiallyLockedLP - poolLP._2 val minimalReward = min( - selfX.toBigInt * supplyLP / reservesX, - selfY.toBigInt * supplyLP / reservesY + selfX._2.toBigInt * supplyLP / reservesX._2, + selfY._2.toBigInt * supplyLP / reservesY._2 ) val rewardOut = OUTPUTS(1) @@ -509,10 +493,48 @@ Depositing contract ensures a liquidity provider gets fair amount of LP tokens. val validRewardOut = rewardOut.propositionBytes == Pk.propBytes && - rewardOut.value >= SELF.value && + rewardOut.value >= SELF.value - DexFee && rewardLP._1 == poolLP._1 && rewardLP._2 >= minimalReward sigmaProp(Pk || (validPoolIn && validRewardOut)) } ``` + +#### Redemption proxy-contract + +Redemption contract ensures a liquidity provider gets fair amount of liquidity for LP tokens in exchange. + +```scala +{ + val selfLP = SELF.tokens(0) + + val poolIn = INPUTS(0) + + val validPoolIn = poolIn.tokens(0) == (PoolNFT, 1L) + + val poolLP = poolIn.tokens(1) + val reservesX = poolIn.tokens(2) + val reservesY = poolIn.tokens(2) + + val supplyLP = InitiallyLockedLP - poolLP._2 + + val minReturnX = selfLP._2.toBigInt * reservesX._2 / supplyLP + val minReturnY = selfLP._2.toBigInt * reservesY._2 / supplyLP + + val returnOut = OUTPUTS(1) + + val returnX = returnOut.tokens(0) + val returnY = returnOut.tokens(1) + + val validReturnOut = + returnOut.propositionBytes == Pk.propBytes && + returnOut.value >= SELF.value - DexFee && + returnX._1 == reservesX._1 && + returnY._1 == reservesY._1 && + returnX._2 >= minReturnX && + returnY._2 >= minReturnY + + sigmaProp(Pk || (validPoolIn && validReturnOut)) +} +``` From 5c8efd527dac4ebb73fcc26c68adc6c94218e9c6 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Mon, 31 May 2021 17:10:59 +0300 Subject: [PATCH 35/60] AMM bootstrapping simplified. --- eip-0014.md | 85 +++++++---------------------------------------------- 1 file changed, 11 insertions(+), 74 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 30246b04..c1c03b17 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -3,7 +3,7 @@ * Author: kushti, Ilya Oskin * Status: Proposed * Created: 12-Mar-2021 -* Last edited: 30-May-2021 +* Last edited: 31-May-2021 * License: CC0 * Track: Standards @@ -242,87 +242,24 @@ Pool NFT is created at pool initialization stage. The pool bootstrapping contrac A liquidity pool is bootstrapped in two steps: -1. Initial amounts of tokens are deposited to the pool bootstrapping proxy contract. For the initializing deposit the amount of LP tokens is calculated using special formula which is `LP = sqrt(X_deposited * Y_deposited)`. Also in order to track pro-rata LP shares of the total reserves of a new pair a unique token called "LP token" must be issued. As soon as tokens can’t be re-issued on Ergo the whole LP emission has to be done at once. -2. Initial reserves of token pair and a remainder of LP tokens left after initial depositing are passed to the pool contract. Also pool NFT is issued. Thus the pool is created in an initial state. Correctness of the state is checked by the proxy contract (This is why it is important to check whether a pool was initialized through the proxy contract. Luckily it can be done easily using pool NFT ID). +1. In order to track pro-rata LP shares of the total reserves of a new pair a unique token called "LP token" must be issued. As soon as tokens can’t be re-issued on Ergo the whole LP emission has to be done at once. +2. Initial reserves of token pair and a remainder of LP tokens left after initial depositing are locked with the pool contract. Also pool NFT is issued. Thus the pool is created in an initial state. Correctness of the state can be checked off-chain at any time by querying the genesis transaction (i.e. the transaction which created the pool). This can be done easily using pool NFT pointing to the first input of that transaction. Note: Any unknown pool must be validated by a client before use, it's critical for security. -In order to avoid blowing up the pool contract with a code which handles only specific intialization aspects a dedicated type of contract is used. +Off-chain pool validation rules: +1. `emission(NFT) == 1` +2. `emission(LP) == K`, where `K` is predefined total `LP` supply +3. `sqrt(GenesisBox.tokens[X] * GenesisBox.tokens[Y]) >= K - GenesisBox.tokens[LP]` - initial depositing is done according to `S = sqrt(X_deposited * Y_deposited)`, where `S` is initial LP reward. ``` InitialInput [X:N, Y:M] - 1. | + 1. Issue LP tokens | | - PoolBootProxy [LP:1000000000000000000, X:N, Y:M] - 2. | | - | | + InitialInput [LP:1000000000000000000, X:N, Y:M] +2a. Reward LP | 2b. Create pool | + | | LPRewardOut [LP:sqrt(N*M)] Pool [NFT:1, LP:1000000000000000000-sqrt(N*M), X:N, Y:M] ``` -#### Schema of the pool bootstrapping UTXO - -Section | Description ----------------|------------------------ -value | Constant amount of ERGs -tokens[0] | LP token reserves -tokens[1] | X token initial reserves -tokens[2] | Y token initial reserves -R5[Coll[Byte]] | Blake2b256 hash of the pool script -R6[Long] | Desired amount of shares LP expects to receive -R7[Long] | Pre-configured pool fee multiplier numerator -R8[Long] | Miner fee -R9[Coll[Byte]] | Initiator proposition bytes - -#### Pool bootstrapping contract - -```scala -{ - val poolScriptHash = SELF.R5[Coll[Byte]].get - val desiredSharesLP = SELF.R6[Long].get - val poolFeeConfig = SELF.R7[Long].get - val minerFeeNErgs = SELF.R8[Long].get - val initiatorProp = SELF.R9[Coll[Byte]].get - - val selfLP = SELF.tokens(0) - val selfX = SELF.tokens(1) - val selfY = SELF.tokens(2) - - val tokenIdLP = selfLP._1 - - val pool = OUTPUTS(0) - val sharesRewardLP = OUTPUTS(1) - - val maybePoolLP = pool.tokens(1) - val poolAmountLP = - if (maybePoolLP._1 == tokenIdLP) maybePoolLP._2 - else 0L - - val validPoolContract = blake2b256(pool.propositionBytes) == poolScriptHash - val validPoolErgAmount = pool.value == SELF.value - sharesRewardLP.value - minerFeeNErgs - val validPoolNFT = pool.tokens(0) == (SELF.id, 1L) - val validPoolConfig = pool.R4[Long].get == poolFeeConfig - - val validInitialDepositing = { - val tokenX = pool.tokens(2) - val tokenY = pool.tokens(3) - val depositedX = tokenX._2 - val depositedY = tokenY._2 - - val validTokens = tokenX == selfX && tokenY == selfY - val validDeposit = depositedX.toBigInt * depositedY >= desiredSharesLP.toBigInt * desiredSharesLP // S >= sqrt(X_deposited * Y_deposited) Deposits satisfy desired share - val validShares = poolAmountLP >= (InitiallyLockedLP - desiredSharesLP) // valid amount of liquidity shares taken from reserves - - validTokens && validDeposit && validShares - } - - val validPool = validPoolContract && validPoolErgAmount && validPoolNFT && validPoolConfig && validInitialDepositing - - val validSharesRewardLP = - sharesRewardLP.propositionBytes == initiatorProp && - sharesRewardLP.tokens(0) == (tokenIdLP, desiredSharesLP) - - sigmaProp(validSelfLP && validSelfPoolFeeConfig && validPool && validSharesRewardLP) -} -``` - #### Schema of the pool UTXO Section | Description From 76637912d149414c1e2cd3d2e8ddf6f88d915838 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Mon, 31 May 2021 19:00:54 +0300 Subject: [PATCH 36/60] AMM scheme improved. --- eip-0014.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index c1c03b17..7db0ff9c 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -251,13 +251,14 @@ Off-chain pool validation rules: 3. `sqrt(GenesisBox.tokens[X] * GenesisBox.tokens[Y]) >= K - GenesisBox.tokens[LP]` - initial depositing is done according to `S = sqrt(X_deposited * Y_deposited)`, where `S` is initial LP reward. ``` - InitialInput [X:N, Y:M] - 1. Issue LP tokens | - | - InitialInput [LP:1000000000000000000, X:N, Y:M] -2a. Reward LP | 2b. Create pool | - | | - LPRewardOut [LP:sqrt(N*M)] Pool [NFT:1, LP:1000000000000000000-sqrt(N*M), X:N, Y:M] + InitialInput#2 [X:N, Y:M] + 1. Issue LP tokens | + | + InitialInput#1 [LP:K] | + | | | + 2a. Reward LP | | 2b. Create pool | + | | | + LPRewardOut [LP:sqrt(N*M)] Pool [NFT:1, LP:K-sqrt(N*M), X:N, Y:M] ``` #### Schema of the pool UTXO From e6256b4b768f2fec01acab6e8a7d691a5e6a63da Mon Sep 17 00:00:00 2001 From: oskin1 Date: Mon, 7 Jun 2021 11:14:07 +0300 Subject: [PATCH 37/60] Contract constants description. --- eip-0014.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/eip-0014.md b/eip-0014.md index 7db0ff9c..1a22d945 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -276,7 +276,7 @@ R4[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) ```scala { - val feeNum0 = SELF.R4[Long].get + val InitiallyLockedLP = 0x7fffffffffffffffL val FeeDenom = 1000 val ergs0 = SELF.value @@ -287,6 +287,7 @@ R4[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) val successor = OUTPUTS(0) + val feeNum0 = SELF.R4[Long].get val feeNum1 = successor.R4[Long].get val ergs1 = successor.value @@ -362,6 +363,16 @@ Swap contract ensures a swap is executed fairly from a user's perspective. The c 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. +##### 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 +MinQuoteAmount | Long | Minimal amount of quote asset +DexFeePerToken | Long | DEX fee in nanoERGs per one unit of quote asset +PoolScriptHash | Coll[Byte] | A hash of a pool contract + ```scala { val FeeDenom = 1000 @@ -406,8 +417,17 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic Depositing contract ensures a liquidity provider gets fair amount of LP tokens. +##### Contract parameters: +Constant | Type | Description +---------------|------------|--------------- +Pk | ProveDLog | User PublicKey +DexFee | Long | DEX fee in nanoERGs +PoolNFT | Coll[Byte] | Pool NFT ID + ```scala { + val InitiallyLockedLP = 0x7fffffffffffffffL + val selfX = SELF.tokens(0) val selfY = SELF.tokens(1) @@ -443,8 +463,17 @@ Depositing contract ensures a liquidity provider gets fair amount of LP tokens. Redemption contract ensures a liquidity provider gets fair amount of liquidity for LP tokens in exchange. +##### Contract parameters: +Constant | Type | Description +---------------|------------|--------------- +Pk | ProveDLog | User PublicKey +DexFee | Long | DEX fee in nanoERGs +PoolNFT | Coll[Byte] | Pool NFT ID + ```scala { + val InitiallyLockedLP = 0x7fffffffffffffffL + val selfLP = SELF.tokens(0) val poolIn = INPUTS(0) From 36667cb8165740b314c1e721ca3f98d527248ca3 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Wed, 9 Jun 2021 11:48:04 +0300 Subject: [PATCH 38/60] NoMoreTokens check added. --- eip-0014.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/eip-0014.md b/eip-0014.md index 1a22d945..f96101ef 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -302,6 +302,8 @@ R4[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) val preservedPoolNFT = poolNFT1 == poolNFT0 val validLP = reservedLP1._1 == reservedLP0._1 val validPair = tokenX1._1 == tokenX0._1 && tokenY1._1 == tokenY0._1 + // since tokens can be repeated, we ensure for sanity that there are no more tokens + val noMoreTokens = successor.tokens.size == 4 val supplyLP0 = InitiallyLockedLP - reservedLP0._2 val supplyLP1 = InitiallyLockedLP - reservedLP1._2 @@ -349,6 +351,7 @@ R4[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) preservedPoolNFT && validLP && validPair && + noMoreTokens && validAction ) } From 448c15da31cb8a3e08b69acbd78cbad24aecf267 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Fri, 18 Jun 2021 09:08:28 +0300 Subject: [PATCH 39/60] AMM swap contract updated. --- eip-0014.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index f96101ef..e407e326 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -374,22 +374,22 @@ FeeNum | Long | Pool fee numerator (must taken from pool params) QuoteId | Coll[Byte] | Quote asset ID MinQuoteAmount | Long | Minimal amount of quote asset DexFeePerToken | Long | DEX fee in nanoERGs per one unit of quote asset -PoolScriptHash | Coll[Byte] | A hash of a pool contract - +PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a concrete unique pool) ```scala { val FeeDenom = 1000 - + val base = SELF.tokens(0) val baseId = base._1 val baseAmount = base._2 val poolInput = INPUTS(0) + val poolNFT = poolInput.tokens(0)._1 val poolAssetX = poolInput.tokens(2) val poolAssetY = poolInput.tokens(3) val validPoolInput = - blake2b256(poolInput.propositionBytes) == PoolScriptHash && + poolNFT == PoolNFT && (poolAssetX._1 == QuoteId || poolAssetY._1 == QuoteId) && (poolAssetX._1 == baseId || poolAssetY._1 == baseId) From c871026d193beb19c075d0b53313ea4364e2c2ae Mon Sep 17 00:00:00 2001 From: oskin1 Date: Fri, 25 Jun 2021 11:04:28 +0300 Subject: [PATCH 40/60] Fractional DEX fee support in Swap. --- eip-0014.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index e407e326..c831db43 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -367,14 +367,16 @@ Swap contract ensures a swap is executed fairly from a user's perspective. The c 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. ##### 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 -MinQuoteAmount | Long | Minimal amount of quote asset -DexFeePerToken | Long | 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) +Constant | Type | Description +--------------------|------------|--------------- +Pk | ProveDLog | User PublicKey +FeeNum | Long | Pool fee numerator (must taken from pool params) +QuoteId | Coll[Byte] | Quote asset ID +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 { val FeeDenom = 1000 @@ -397,7 +399,7 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a concr OUTPUTS.exists { (box: Box) => val quoteAsset = box.tokens(0) val quoteAmount = quoteAsset._2 - val fairDexFee = box.value >= SELF.value - quoteAmount * DexFeePerToken + val fairDexFee = box.value >= SELF.value - quoteAmount * DexFeePerTokenNum / DexFeePerTokenDenom val relaxedInput = baseAmount - 1 val fairPrice = if (poolAssetX._1 == QuoteId) From c6464014733fe3fdc5cfd8797a03464f45cb3d56 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Wed, 7 Jul 2021 18:16:11 +0300 Subject: [PATCH 41/60] Swap contract updated. --- eip-0014.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index c831db43..79b0cd71 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -397,15 +397,15 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a val validTrade = OUTPUTS.exists { (box: Box) => - val quoteAsset = box.tokens(0) - val quoteAmount = quoteAsset._2 - val fairDexFee = box.value >= SELF.value - quoteAmount * DexFeePerTokenNum / DexFeePerTokenDenom - val relaxedInput = baseAmount - 1 - val fairPrice = + val quoteAsset = box.tokens(0) + val quoteAmount = quoteAsset._2 + val fairDexFee = box.value >= SELF.value - quoteAmount * DexFeePerTokenNum / DexFeePerTokenDenom + val relaxedOutput = quoteAmount + 1 // handle rounding loss + val fairPrice = if (poolAssetX._1 == QuoteId) - poolAssetX._2.toBigInt * relaxedInput * FeeNum <= quoteAmount * (poolAssetY._2.toBigInt * FeeDenom + relaxedInput * FeeNum) + poolAssetX._2.toBigInt * baseAmount * FeeNum <= relaxedOutput * (poolAssetY._2.toBigInt * FeeDenom + baseAmount * FeeNum) else - poolAssetY._2.toBigInt * relaxedInput * FeeNum <= quoteAmount * (poolAssetX._2.toBigInt * FeeDenom + relaxedInput * FeeNum) + poolAssetY._2.toBigInt * baseAmount * FeeNum <= relaxedOutput * (poolAssetX._2.toBigInt * FeeDenom + baseAmount * FeeNum) box.propositionBytes == Pk.propBytes && quoteAsset._1 == QuoteId && 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 42/60] 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 43/60] 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 44/60] 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 45/60] 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 46/60] 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 From 194bc8c4f4f99d6bb812ada30fecb2b22bd75d08 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Sun, 18 Jul 2021 18:48:29 +0300 Subject: [PATCH 47/60] AMM Pool validation rules updated. --- eip-0014.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eip-0014.md b/eip-0014.md index 79b0cd71..fce02aef 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -247,7 +247,7 @@ A liquidity pool is bootstrapped in two steps: Off-chain pool validation rules: 1. `emission(NFT) == 1` -2. `emission(LP) == K`, where `K` is predefined total `LP` supply +2. `emission(LP) == K - Bi`, where `K` is predefined total `LP` supply, `Bi` is the amount of LP tokens to be burned initially. Initial LP burning is required in order to address so called "Donation Attack" - an attack when the smallest fraction of LP token becomes so overpriced, that small LPs can't provide liquidity to the pool anymore. 3. `sqrt(GenesisBox.tokens[X] * GenesisBox.tokens[Y]) >= K - GenesisBox.tokens[LP]` - initial depositing is done according to `S = sqrt(X_deposited * Y_deposited)`, where `S` is initial LP reward. ``` From b40b1d01493b67e2f7deea28939545062e34d760 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Thu, 22 Jul 2021 19:32:54 +0300 Subject: [PATCH 48/60] Uniqueness checks added to proxy scripts. --- eip-0014.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 06bf1ddb..c0f870fe 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -407,11 +407,14 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a else poolAssetY._2.toBigInt * baseAmount * FeeNum <= relaxedOutput * (poolAssetX._2.toBigInt * FeeDenom + baseAmount * FeeNum) + val uniqueOutput = box.R4[Int].map({(i: Int) => INPUTS(i).id == SELF.id}).getOrElse(false) // check if output is mapped 1 to 1 to the order + box.propositionBytes == Pk.propBytes && quoteAsset._1 == QuoteId && quoteAsset._2 >= MinQuoteAmount && fairDexFee && - fairPrice + fairPrice && + uniqueOutput } sigmaProp(Pk || (validPoolInput && validTrade)) @@ -454,11 +457,14 @@ PoolNFT | Coll[Byte] | Pool NFT ID val rewardOut = OUTPUTS(1) val rewardLP = rewardOut.tokens(0) + val uniqueOutput = rewardOut.R4[Int].map({(i: Int) => INPUTS(i).id == SELF.id}).getOrElse(false) + val validRewardOut = rewardOut.propositionBytes == Pk.propBytes && rewardOut.value >= SELF.value - DexFee && rewardLP._1 == poolLP._1 && - rewardLP._2 >= minimalReward + rewardLP._2 >= minimalReward && + uniqueOutput sigmaProp(Pk || (validPoolIn && validRewardOut)) } @@ -499,13 +505,16 @@ PoolNFT | Coll[Byte] | Pool NFT ID val returnX = returnOut.tokens(0) val returnY = returnOut.tokens(1) + val uniqueOutput = returnOut.R4[Int].map({(i: Int) => INPUTS(i).id == SELF.id}).getOrElse(false) + val validReturnOut = returnOut.propositionBytes == Pk.propBytes && returnOut.value >= SELF.value - DexFee && returnX._1 == reservesX._1 && returnY._1 == reservesY._1 && returnX._2 >= minReturnX && - returnY._2 >= minReturnY + returnY._2 >= minReturnY && + uniqueOutput sigmaProp(Pk || (validPoolIn && validReturnOut)) } From 5c7380bd70a7f067a5f51977af7b92545cb35d0e Mon Sep 17 00:00:00 2001 From: oskin1 Date: Sat, 24 Jul 2021 17:15:12 +0300 Subject: [PATCH 49/60] FeeNum type changed Long => Int in AMM Pool contracts. --- eip-0014.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index c0f870fe..6513b462 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -287,8 +287,8 @@ R4[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) val successor = OUTPUTS(0) - val feeNum0 = SELF.R4[Long].get - val feeNum1 = successor.R4[Long].get + val feeNum0 = SELF.R4[Int].get + val feeNum1 = successor.R4[Int].get val ergs1 = successor.value val poolNFT1 = successor.tokens(0) @@ -567,8 +567,8 @@ R4[Long] | Fee multiplier numerator (e.g. 0.3% fee -> 997 fee_num). This repres val successor = OUTPUTS(0) - val feeNum0 = SELF.R4[Long].get - val feeNum1 = successor.R4[Long].get + val feeNum0 = SELF.R4[Int].get + val feeNum1 = successor.R4[Int].get val poolNFT1 = successor.tokens(0) val reservedLP1 = successor.tokens(1) From bbb90fb5802340d8a334d1f638c09236834cac1b Mon Sep 17 00:00:00 2001 From: oskin1 Date: Fri, 30 Jul 2021 12:22:46 +0300 Subject: [PATCH 50/60] T2T Swap contract simplified. --- eip-0014.md | 54 ++++++++++++++++++++++++++--------------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 6513b462..a6bd5729 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -383,41 +383,39 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a val base = SELF.tokens(0) val baseId = base._1 - val baseAmount = base._2 + val baseAmount = base._2.toBigInt val poolInput = INPUTS(0) val poolNFT = poolInput.tokens(0)._1 val poolAssetX = poolInput.tokens(2) val poolAssetY = poolInput.tokens(3) - val validPoolInput = - poolNFT == PoolNFT && - (poolAssetX._1 == QuoteId || poolAssetY._1 == QuoteId) && - (poolAssetX._1 == baseId || poolAssetY._1 == baseId) - - val validTrade = - OUTPUTS.exists { (box: Box) => - val quoteAsset = box.tokens(0) - val quoteAmount = quoteAsset._2 - val fairDexFee = box.value >= SELF.value - quoteAmount * DexFeePerTokenNum / DexFeePerTokenDenom - val relaxedOutput = quoteAmount + 1 // handle rounding loss - val fairPrice = - if (poolAssetX._1 == QuoteId) - poolAssetX._2.toBigInt * baseAmount * FeeNum <= relaxedOutput * (poolAssetY._2.toBigInt * FeeDenom + baseAmount * FeeNum) - else - poolAssetY._2.toBigInt * baseAmount * FeeNum <= relaxedOutput * (poolAssetX._2.toBigInt * FeeDenom + baseAmount * FeeNum) - - val uniqueOutput = box.R4[Int].map({(i: Int) => INPUTS(i).id == SELF.id}).getOrElse(false) // check if output is mapped 1 to 1 to the order - - box.propositionBytes == Pk.propBytes && - quoteAsset._1 == QuoteId && - quoteAsset._2 >= MinQuoteAmount && - fairDexFee && - fairPrice && - uniqueOutput - } + val validPoolInput = poolNFT == PoolNFT + val noMoreInputs = INPUTS.size == 2 + + val rewardBox = OUTPUTS(1) + + val validTrade = { + val quoteAsset = rewardBox.tokens(0) + val quoteAmount = quoteAsset._2.toBigInt + val fairDexFee = rewardBox.value >= SELF.value - quoteAmount * DexFeePerTokenNum / DexFeePerTokenDenom + val relaxedOutput = quoteAmount + 1L // handle rounding loss + val poolX = poolAssetX._2.toBigInt + val poolY = poolAssetY._2.toBigInt + val fairPrice = + if (poolAssetX._1 == QuoteId) + poolX * baseAmount * FeeNum <= relaxedOutput * (poolY * FeeDenom + baseAmount * FeeNum) + else + poolY * baseAmount * FeeNum <= relaxedOutput * (poolX * FeeDenom + baseAmount * FeeNum) + + rewardBox.propositionBytes == Pk.propBytes && + quoteAsset._1 == QuoteId && + quoteAsset._2 >= MinQuoteAmount && + fairDexFee && + fairPrice + } - sigmaProp(Pk || (validPoolInput && validTrade)) + sigmaProp(Pk || (validPoolInput && noMoreInputs && validTrade)) } ``` From 393eb81df72bc77b0e93196411c31d0469aa5ab7 Mon Sep 17 00:00:00 2001 From: sh <23208922+scalahub@users.noreply.github.com> Date: Mon, 2 Aug 2021 01:20:39 +0530 Subject: [PATCH 51/60] Make N2T contracts logic similar to T2T --- eip-0014.md | 71 +++++++++++++++++++++++++++-------------------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index a6bd5729..3a39ca33 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -671,31 +671,29 @@ 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 == QuoteId + val noMoreInputs = INPUTS.size == 2 - 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 - BaseAmount - - val relaxedOutput = quoteAssetAmount + 1 // handle rounding loss - 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 + val rewardBox = OUTPUTS(1) + + val validTrade = { + val quoteAsset = rewardBox.tokens(0) + + val quoteAssetID = quoteAsset._1 + val quoteAssetAmount = quoteAsset._2 + + val fairDexFee = rewardBox.value >= SELF.value - quoteAssetAmount * DexFeePerTokenNum / DexFeePerTokenDenom - BaseAmount + val relaxedOutput = quoteAssetAmount + 1 // handle rounding loss + val fairPrice = poolReservesY.toBigInt * BaseAmount * FeeNum <= relaxedOutput * (poolReservesX.toBigInt * FeeDenom + BaseAmount * FeeNum) - box.propositionBytes == Pk.propBytes && - quoteAssetID == QuoteId && - quoteAssetAmount >= MinQuoteAmount && - fairDexFee && - fairPrice && - uniqueOutput // prevent multiple input boxes with same script mapping to one single output box - } + rewardBox.propositionBytes == Pk.propBytes && + quoteAssetID == QuoteId && + quoteAssetAmount >= MinQuoteAmount && + fairDexFee && + fairPrice + } - sigmaProp(Pk || (validPoolInput && validTrade)) + sigmaProp(Pk || (validPoolInput && noMoreInputs && validTrade)) } ``` @@ -729,22 +727,25 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a val poolReservesY = poolY_token._2 val validPoolInput = poolNFT == PoolNFT && poolY_tokenId == baseTokenId - 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 * baseAmount * FeeNum <= relaxedOutput * (poolReservesY.toBigInt * FeeDenom + baseAmount * FeeNum) + val noMoreInputs = INPUTS.size == 2 - val uniqueOutput = INPUTS(box.R4[Int].get).id == SELF.id // See https://www.ergoforum.org/t/ergoscript-design-patterns/222/15?u=scalahub + val rewardBox = OUTPUTS(1) - box.propositionBytes == Pk.propBytes && - quoteAssetAmount >= MinQuoteAmount && - fairPrice && - uniqueOutput // prevent multiple input boxes with same script mapping to one single output box - } + val validTrade = { + // bought nanoErgs are called quoteAssetAmount + val deltaNanoErgs = rewardBox.value - SELF.value // this is quoteAssetAmount - fee + val quoteAssetAmount = deltaNanoErgs * DexFeePerTokenDenom / (DexFeePerTokenDenom - DexFeePerTokenNum) + val relaxedOutput = quoteAssetAmount + 1 // handle rounding loss + val fairPrice = poolReservesX.toBigInt * baseAmount * FeeNum <= relaxedOutput * (poolReservesY.toBigInt * FeeDenom + baseAmount * FeeNum) + + val uniqueOutput = INPUTS(rewardBox.R4[Int].get).id == SELF.id // See https://www.ergoforum.org/t/ergoscript-design-patterns/222/15?u=scalahub + + rewardBox.propositionBytes == Pk.propBytes && + quoteAssetAmount >= MinQuoteAmount && + fairPrice && + uniqueOutput // prevent multiple input boxes with same script mapping to one single output box + } - sigmaProp(Pk || (validPoolInput && validTrade)) + sigmaProp(Pk || (validPoolInput && noMoreInputs && validTrade)) } ``` From 529ad2a8965cc62f2a2974ca1c4a833489054770 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Mon, 9 Aug 2021 19:10:58 +0300 Subject: [PATCH 52/60] AMM proxy contracts updated. --- eip-0014.md | 151 ++++++++++++++++++++++++++-------------------------- 1 file changed, 76 insertions(+), 75 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 3a39ca33..4e231b34 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -381,41 +381,42 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a { val FeeDenom = 1000 - val base = SELF.tokens(0) - val baseId = base._1 - val baseAmount = base._2.toBigInt - - val poolInput = INPUTS(0) - val poolNFT = poolInput.tokens(0)._1 - val poolAssetX = poolInput.tokens(2) - val poolAssetY = poolInput.tokens(3) - - val validPoolInput = poolNFT == PoolNFT - val noMoreInputs = INPUTS.size == 2 - - val rewardBox = OUTPUTS(1) - - val validTrade = { - val quoteAsset = rewardBox.tokens(0) - val quoteAmount = quoteAsset._2.toBigInt - val fairDexFee = rewardBox.value >= SELF.value - quoteAmount * DexFeePerTokenNum / DexFeePerTokenDenom - val relaxedOutput = quoteAmount + 1L // handle rounding loss - val poolX = poolAssetX._2.toBigInt - val poolY = poolAssetY._2.toBigInt - val fairPrice = - if (poolAssetX._1 == QuoteId) - poolX * baseAmount * FeeNum <= relaxedOutput * (poolY * FeeDenom + baseAmount * FeeNum) - else - poolY * baseAmount * FeeNum <= relaxedOutput * (poolX * FeeDenom + baseAmount * FeeNum) - - rewardBox.propositionBytes == Pk.propBytes && - quoteAsset._1 == QuoteId && - quoteAsset._2 >= MinQuoteAmount && - fairDexFee && - fairPrice - } + val poolIn = INPUTS(0) - sigmaProp(Pk || (validPoolInput && noMoreInputs && validTrade)) + val validTrade = + if (INPUTS.size == 2 && poolIn.tokens.size == 4) { + val base = SELF.tokens(0) + val baseId = base._1 + val baseAmount = base._2.toBigInt + + val poolNFT = poolIn.tokens(0)._1 + val poolAssetX = poolIn.tokens(2) + val poolAssetY = poolIn.tokens(3) + + val validPoolIn = poolNFT == PoolNFT + + val rewardBox = OUTPUTS(1) + val quoteAsset = rewardBox.tokens(0) + val quoteAmount = quoteAsset._2.toBigInt + val fairDexFee = rewardBox.value >= SELF.value - quoteAmount * DexFeePerTokenNum / DexFeePerTokenDenom + val relaxedOutput = quoteAmount + 1L // handle rounding loss + val poolX = poolAssetX._2.toBigInt + val poolY = poolAssetY._2.toBigInt + val fairPrice = + if (poolAssetX._1 == QuoteId) + poolX * baseAmount * FeeNum <= relaxedOutput * (poolY * FeeDenom + baseAmount * FeeNum) + else + poolY * baseAmount * FeeNum <= relaxedOutput * (poolX * FeeDenom + baseAmount * FeeNum) + + validPoolIn && + rewardBox.propositionBytes == Pk.propBytes && + quoteAsset._1 == QuoteId && + quoteAsset._2 >= MinQuoteAmount && + fairDexFee && + fairPrice + } else false + + sigmaProp(Pk || validTrade) } ``` @@ -439,32 +440,32 @@ PoolNFT | Coll[Byte] | Pool NFT ID val poolIn = INPUTS(0) - val validPoolIn = poolIn.tokens(0) == (PoolNFT, 1L) + val validDeposit = + if (INPUTS.size == 2 && poolIn.tokens.size == 4) { + val validPoolIn = poolIn.tokens(0) == (PoolNFT, 1L) - val poolLP = poolIn.tokens(1) - val reservesX = poolIn.tokens(2) - val reservesY = poolIn.tokens(2) + val poolLP = poolIn.tokens(1) + val reservesX = poolIn.tokens(2) + val reservesY = poolIn.tokens(3) - val supplyLP = InitiallyLockedLP - poolLP._2 + val supplyLP = InitiallyLockedLP - poolLP._2 - val minimalReward = min( - selfX._2.toBigInt * supplyLP / reservesX._2, - selfY._2.toBigInt * supplyLP / reservesY._2 - ) - - val rewardOut = OUTPUTS(1) - val rewardLP = rewardOut.tokens(0) + val minimalReward = min( + selfX._2.toBigInt * supplyLP / reservesX._2, + selfY._2.toBigInt * supplyLP / reservesY._2 + ) - val uniqueOutput = rewardOut.R4[Int].map({(i: Int) => INPUTS(i).id == SELF.id}).getOrElse(false) + val rewardOut = OUTPUTS(1) + val rewardLP = rewardOut.tokens(0) - val validRewardOut = - rewardOut.propositionBytes == Pk.propBytes && - rewardOut.value >= SELF.value - DexFee && - rewardLP._1 == poolLP._1 && - rewardLP._2 >= minimalReward && - uniqueOutput + validPoolIn && + rewardOut.propositionBytes == Pk.propBytes && + rewardOut.value >= SELF.value - DexFee && + rewardLP._1 == poolLP._1 && + rewardLP._2 >= minimalReward + } else false - sigmaProp(Pk || (validPoolIn && validRewardOut)) + sigmaProp(Pk || validDeposit) } ``` @@ -487,34 +488,34 @@ PoolNFT | Coll[Byte] | Pool NFT ID val poolIn = INPUTS(0) - val validPoolIn = poolIn.tokens(0) == (PoolNFT, 1L) - - val poolLP = poolIn.tokens(1) - val reservesX = poolIn.tokens(2) - val reservesY = poolIn.tokens(2) + val validRedeem = + if (INPUTS.size == 2 && poolIn.tokens.size == 4) { + val validPoolIn = poolIn.tokens(0) == (PoolNFT, 1L) - val supplyLP = InitiallyLockedLP - poolLP._2 + val poolLP = poolIn.tokens(1) + val reservesX = poolIn.tokens(2) + val reservesY = poolIn.tokens(3) - val minReturnX = selfLP._2.toBigInt * reservesX._2 / supplyLP - val minReturnY = selfLP._2.toBigInt * reservesY._2 / supplyLP + val supplyLP = InitiallyLockedLP - poolLP._2 - val returnOut = OUTPUTS(1) + val minReturnX = selfLP._2.toBigInt * reservesX._2 / supplyLP + val minReturnY = selfLP._2.toBigInt * reservesY._2 / supplyLP - val returnX = returnOut.tokens(0) - val returnY = returnOut.tokens(1) + val returnOut = OUTPUTS(1) - val uniqueOutput = returnOut.R4[Int].map({(i: Int) => INPUTS(i).id == SELF.id}).getOrElse(false) + val returnX = returnOut.tokens(0) + val returnY = returnOut.tokens(1) - val validReturnOut = - returnOut.propositionBytes == Pk.propBytes && - returnOut.value >= SELF.value - DexFee && - returnX._1 == reservesX._1 && - returnY._1 == reservesY._1 && - returnX._2 >= minReturnX && - returnY._2 >= minReturnY && - uniqueOutput + validPoolIn && + returnOut.propositionBytes == Pk.propBytes && + returnOut.value >= SELF.value - DexFee && + returnX._1 == reservesX._1 && + returnY._1 == reservesY._1 && + returnX._2 >= minReturnX && + returnY._2 >= minReturnY + } else false - sigmaProp(Pk || (validPoolIn && validReturnOut)) + sigmaProp(Pk || validRedeem) } ``` From 0839945e6e03dfa9b5b023b4260e33c12273fcc5 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Thu, 12 Aug 2021 12:48:59 +0300 Subject: [PATCH 53/60] Corrections. --- eip-0014.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 4e231b34..a8eb71f5 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -270,7 +270,7 @@ tokens[0] | Pool NFT tokens[1] | LP token reserves tokens[2] | Asset X tokens[3] | Asset Y -R4[Long] | Fee multiplier numerator (e.g. 0.003% fee -> 997 fee_num) +R4[Long] | Fee multiplier numerator (e.g. 0.3% fee -> 997 fee_num) #### Pool contract @@ -369,7 +369,7 @@ Once published swap contracts are tracked and executed by ErgoDEX bots automatic ##### Contract parameters: Constant | Type | Description --------------------|------------|--------------- -Pk | ProveDLog | User PublicKey +Pk | SigmaProp | User PublicKey FeeNum | Long | Pool fee numerator (must taken from pool params) QuoteId | Coll[Byte] | Quote asset ID MinQuoteAmount | Long | Minimal amount of quote asset @@ -427,7 +427,7 @@ Depositing contract ensures a liquidity provider gets fair amount of LP tokens. ##### Contract parameters: Constant | Type | Description ---------------|------------|--------------- -Pk | ProveDLog | User PublicKey +Pk | SigmaProp | User PublicKey DexFee | Long | DEX fee in nanoERGs PoolNFT | Coll[Byte] | Pool NFT ID @@ -476,7 +476,7 @@ Redemption contract ensures a liquidity provider gets fair amount of liquidity f ##### Contract parameters: Constant | Type | Description ---------------|------------|--------------- -Pk | ProveDLog | User PublicKey +Pk | SigmaProp | User PublicKey DexFee | Long | DEX fee in nanoERGs PoolNFT | Coll[Byte] | Pool NFT ID @@ -650,7 +650,7 @@ Until a swap is executed, it can be cancelled by a user who created it by simply ###### Contract parameters Constant | Type | Description --------------------|------------|--------------- -Pk | ProveDLog | User PublicKey +Pk | SigmaProp | 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 @@ -703,7 +703,7 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a ###### Contract parameters: Constant | Type | Description --------------------|------------|--------------- -Pk | ProveDLog | User PublicKey +Pk | SigmaProp | 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 From 5346a83697db03d43a99eddb3e1d725c26ff94db Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Sun, 15 Aug 2021 04:33:42 +0530 Subject: [PATCH 54/60] Require 2 inputs in N2T swap contracts --- eip-0014.md | 130 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 75 insertions(+), 55 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index a8eb71f5..6fd81811 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -651,7 +651,7 @@ Until a swap is executed, it can be cancelled by a user who created it by simply Constant | Type | Description --------------------|------------|--------------- Pk | SigmaProp | User PublicKey -FeeNum | Long | Pool fee numerator (must taken from pool params) +FeeNum | Long | Pool fee numerator (must be 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 BaseAmount | Long | The amount of nanoErgs to sell @@ -661,40 +661,51 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a ```scala { // contract to sell Ergs and buy Token - val FeeDenom = 1000 + val PoolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual NFT ID + val QuoteId = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual Quote token id + val DexFeePerTokenNum = 10L // TODO replace with actual Dex fee num + val DexFeePerTokenDenom = 1000L // TODO replace with actual Dex fee denom + val Pk = sigmaProp(true) // TODO replace with actual sigma prop + val FeeNum = 997L // TODO replace with actual feeNum (997 corresponds to 0.3% fee) + val BaseAmount = 100L // TODO replace with actual base amount + val MinQuoteAmount = 20 // TODO replace with actual minimum quote amount - val poolInput = INPUTS(0) - val poolNFT = poolInput.tokens(0)._1 - val poolY_token = poolInput.tokens(2) - val poolY_tokenId = poolY_token._1 + val validTrade = + if (INPUTS.size == 2) { // we don't permit more than one swap per tx + val FeeDenom = 1000 - val poolReservesX = poolInput.value - val poolReservesY = poolY_token._2 - val validPoolInput = poolNFT == PoolNFT && poolY_tokenId == QuoteId - val noMoreInputs = INPUTS.size == 2 + val poolIn = INPUTS(0) + val poolNFT = poolIn.tokens(0)._1 - val rewardBox = OUTPUTS(1) + val poolY_token = poolIn.tokens(2) + val poolY_tokenId = poolY_token._1 - val validTrade = { - val quoteAsset = rewardBox.tokens(0) - - val quoteAssetID = quoteAsset._1 - val quoteAssetAmount = quoteAsset._2 - - val fairDexFee = rewardBox.value >= SELF.value - quoteAssetAmount * DexFeePerTokenNum / DexFeePerTokenDenom - BaseAmount + val poolReservesX = poolIn.value + val poolReservesY = poolY_token._2 + val validPoolIn = poolNFT == PoolNFT && poolY_tokenId == QuoteId + val noMoreInputs = INPUTS.size == 2 - val relaxedOutput = quoteAssetAmount + 1 // handle rounding loss - val fairPrice = poolReservesY.toBigInt * BaseAmount * FeeNum <= relaxedOutput * (poolReservesX.toBigInt * FeeDenom + BaseAmount * FeeNum) + val rewardBox = OUTPUTS(1) - rewardBox.propositionBytes == Pk.propBytes && - quoteAssetID == QuoteId && - quoteAssetAmount >= MinQuoteAmount && - fairDexFee && - fairPrice - } + val quoteAsset = rewardBox.tokens(0) + + val quoteAssetID = quoteAsset._1 + val quoteAssetAmount = quoteAsset._2 + + val fairDexFee = rewardBox.value >= SELF.value - quoteAssetAmount * DexFeePerTokenNum / DexFeePerTokenDenom - BaseAmount + + val relaxedOutput = quoteAssetAmount + 1 // handle rounding loss + val fairPrice = poolReservesY.toBigInt * BaseAmount * FeeNum <= relaxedOutput * (poolReservesX.toBigInt * FeeDenom + BaseAmount * FeeNum) + + rewardBox.propositionBytes == Pk.propBytes && + quoteAssetID == QuoteId && + quoteAssetAmount >= MinQuoteAmount && + fairDexFee && + fairPrice + } else false - sigmaProp(Pk || (validPoolInput && noMoreInputs && validTrade)) + sigmaProp(Pk || validTrade) } ``` @@ -712,41 +723,50 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a ```scala { // contract to sell tokens and buy Ergs - val FeeDenom = 1000 - - 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 + val PoolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual NFT ID + val DexFeePerTokenNum = 10L // TODO replace with actual Dex fee num + val DexFeePerTokenDenom = 1000L // TODO replace with actual Dex fee denom + val Pk = sigmaProp(true) // TODO replace with actual sigma prop + val FeeNum = 997L // TODO replace with actual feeNum (997 corresponds to 0.3% fee) + val MinQuoteAmount = 20 // TODO replace with actual minimum quote amount - val poolY_token = poolInput.tokens(2) - val poolY_tokenId = poolY_token._1 + val validTrade = + if (INPUTS.size == 2) { + val FeeDenom = 1000 - val poolReservesX = poolInput.value - val poolReservesY = poolY_token._2 - val validPoolInput = poolNFT == PoolNFT && poolY_tokenId == baseTokenId + val baseToken = SELF.tokens(0) // token being sold + val baseTokenId = baseToken._1 + val baseAmount = baseToken._2 - val noMoreInputs = INPUTS.size == 2 + val poolIn = INPUTS(0) + val poolNFT = poolIn.tokens(0)._1 - val rewardBox = OUTPUTS(1) + val poolY_token = poolIn.tokens(2) + val poolY_tokenId = poolY_token._1 - val validTrade = { - // bought nanoErgs are called quoteAssetAmount - val deltaNanoErgs = rewardBox.value - SELF.value // this is quoteAssetAmount - fee - val quoteAssetAmount = deltaNanoErgs * DexFeePerTokenDenom / (DexFeePerTokenDenom - DexFeePerTokenNum) - val relaxedOutput = quoteAssetAmount + 1 // handle rounding loss - val fairPrice = poolReservesX.toBigInt * baseAmount * FeeNum <= relaxedOutput * (poolReservesY.toBigInt * FeeDenom + baseAmount * FeeNum) + val poolReservesX = poolIn.value + val poolReservesY = poolY_token._2 + val validPoolIn = poolNFT == PoolNFT && poolY_tokenId == baseTokenId - val uniqueOutput = INPUTS(rewardBox.R4[Int].get).id == SELF.id // See https://www.ergoforum.org/t/ergoscript-design-patterns/222/15?u=scalahub + val noMoreInputs = INPUTS.size == 2 - rewardBox.propositionBytes == Pk.propBytes && - quoteAssetAmount >= MinQuoteAmount && - fairPrice && - uniqueOutput // prevent multiple input boxes with same script mapping to one single output box - } + val rewardBox = OUTPUTS(1) + + // bought nanoErgs are called quoteAssetAmount + val deltaNanoErgs = rewardBox.value - SELF.value // this is quoteAssetAmount - fee + val quoteAssetAmount = deltaNanoErgs * DexFeePerTokenDenom / (DexFeePerTokenDenom - DexFeePerTokenNum) + val relaxedOutput = quoteAssetAmount + 1 // handle rounding loss + val fairPrice = poolReservesX.toBigInt * baseAmount * FeeNum <= relaxedOutput * (poolReservesY.toBigInt * FeeDenom + baseAmount * FeeNum) - sigmaProp(Pk || (validPoolInput && noMoreInputs && validTrade)) + val uniqueOutput = INPUTS(rewardBox.R4[Int].get).id == SELF.id // See https://www.ergoforum.org/t/ergoscript-design-patterns/222/15?u=scalahub + + validPoolIn && + rewardBox.propositionBytes == Pk.propBytes && + quoteAssetAmount >= MinQuoteAmount && + fairPrice && + uniqueOutput // prevent multiple input boxes with same script mapping to one single output box + } else false + + sigmaProp(Pk || validTrade) } ``` From 2d1020320c1760bd463defa19831667881221bb4 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Sun, 15 Aug 2021 05:14:59 +0530 Subject: [PATCH 55/60] Add N2T deposit and redeem contracts --- eip-0014.md | 123 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 114 insertions(+), 9 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 6fd81811..d9ba10e7 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -327,7 +327,7 @@ R4[Long] | Fee multiplier numerator (e.g. 0.3% fee -> 997 fee_num) val validRedemption = { val _deltaSupplyLP = deltaSupplyLP.toBigInt - // note: _deltaSupplyLP and deltaReservesX, deltaReservesY are negative + // note: _deltaSupplyLP, deltaReservesX and deltaReservesY are negative deltaReservesX.toBigInt * supplyLP0 >= _deltaSupplyLP * reservesX0 && deltaReservesY.toBigInt * supplyLP0 >= _deltaSupplyLP * reservesY0 } @@ -552,7 +552,7 @@ tokens[1] | Locked LP tokens tokens[2] | Asset Y reserves 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 +#### Pool contract (N2T) ```scala { @@ -606,7 +606,7 @@ R4[Long] | Fee multiplier numerator (e.g. 0.3% fee -> 997 fee_num). This repres val validRedemption = { val _deltaSupplyLP = deltaSupplyLP.toBigInt - // note: _deltaSupplyLP and deltaReservesX, deltaReservesY are negative + // note: _deltaSupplyLP, deltaReservesX and deltaReservesY are negative deltaReservesX.toBigInt * supplyLP0 >= _deltaSupplyLP * reservesX0 && deltaReservesY.toBigInt * supplyLP0 >= _deltaSupplyLP * reservesY0 } @@ -636,7 +636,7 @@ R4[Long] | Fee multiplier numerator (e.g. 0.3% fee -> 997 fee_num). This repres } ``` -#### Swap proxy-contract +#### Swap proxy-contract (N2T) 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)` @@ -739,13 +739,13 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a val baseAmount = baseToken._2 val poolIn = INPUTS(0) - val poolNFT = poolIn.tokens(0)._1 - - val poolY_token = poolIn.tokens(2) + val poolNFT = poolIn.tokens(0)._1 // first token id is NFT + // second token id LP token + val poolY_token = poolIn.tokens(2) // third token id is Y asset id val poolY_tokenId = poolY_token._1 - val poolReservesX = poolIn.value - val poolReservesY = poolY_token._2 + val poolReservesX = poolIn.value // nanoErgs is X asset amount + val poolReservesY = poolY_token._2 // third token amount is Y asset amount val validPoolIn = poolNFT == PoolNFT && poolY_tokenId == baseTokenId val noMoreInputs = INPUTS.size == 2 @@ -770,3 +770,108 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a sigmaProp(Pk || validTrade) } ``` +#### Depositing proxy-contract (N2T) + +Depositing contract ensures a liquidity provider gets fair amount of LP tokens. + +##### Contract parameters: +Constant | Type | Description +---------------|------------|--------------- +Pk | SigmaProp | User PublicKey +DexFee | Long | DEX fee in nanoERGs +PoolNFT | Coll[Byte] | Pool NFT ID +SelfXAmount | Long | NanoErgs to deposit + +```scala +{ + val InitiallyLockedLP = 0x7fffffffffffffffL + + val SelfXAmount = 100000000 // TODO replace with actual SelfX + val DexFee = 10000 // TODO replace with actual DexFEe + val PoolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual NFT ID + val Pk = sigmaProp(true) // TODO replace with actual sigma prop + + val validDeposit = + if (INPUTS.size == 2) { + val selfY = SELF.tokens(0) + val poolIn = INPUTS(0) + + val validPoolIn = poolIn.tokens(0)._1 == PoolNFT // No need to check amount, as we assume NFT is in quantity 1 + + val poolLP = poolIn.tokens(1) + val reservesXAmount = poolIn.value + val reservesY = poolIn.tokens(2) + + val supplyLP = InitiallyLockedLP - poolLP._2 + + val minimalReward = min( + SelfXAmount.toBigInt * supplyLP / reservesXAmount, + selfY._2.toBigInt * supplyLP / reservesY._2 + ) + + val rewardOut = OUTPUTS(1) + val rewardLP = rewardOut.tokens(0) + + validPoolIn && + rewardOut.propositionBytes == Pk.propBytes && + rewardOut.value >= SELF.value - DexFee - SelfXAmount && + rewardLP._1 == poolLP._1 && + rewardLP._2 >= minimalReward + } else false + + sigmaProp(Pk || validDeposit) +} +``` + +#### Redemption proxy-contract (N2T) + +Redemption contract ensures a liquidity provider gets fair amount of liquidity for LP tokens in exchange. + +##### Contract parameters: +Constant | Type | Description +---------------|------------|--------------- +Pk | SigmaProp | User PublicKey +DexFee | Long | DEX fee in nanoERGs +PoolNFT | Coll[Byte] | Pool NFT ID + +```scala +{ + val InitiallyLockedLP = 0x7fffffffffffffffL + + val DexFee = 10000 // TODO replace with actual DexFEe + val PoolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual NFT ID + val Pk = sigmaProp(true) // TODO replace with actual sigma prop + + val validRedeem = + if (INPUTS.size == 2) { + val selfLP = SELF.tokens(0) + val poolIn = INPUTS(0) + + val validPoolIn = poolIn.tokens(0)._1 == PoolNFT + + val poolLP = poolIn.tokens(1) + val reservesXAmount = poolIn.value + val reservesY = poolIn.tokens(2) + + val supplyLP = InitiallyLockedLP - poolLP._2 + + val minReturnX = selfLP._2.toBigInt * reservesXAmount / supplyLP + val minReturnY = selfLP._2.toBigInt * reservesY._2 / supplyLP + + val returnOut = OUTPUTS(1) + + val returnXAmount = returnOut.value - SELF.value + DexFee + val returnY = returnOut.tokens(0) + + validPoolIn && + returnOut.propositionBytes == Pk.propBytes && + returnOut.value >= SELF.value - DexFee && + returnY._1 == reservesY._1 && // token id matches + returnXAmount >= minReturnX && + returnY._2 >= minReturnY + } else false + + sigmaProp(Pk || validRedeem) +} +``` + From 32cedf08cbf4c00e3d3bfe40aad44014795c5760 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Sun, 15 Aug 2021 21:40:58 +0530 Subject: [PATCH 56/60] Add token existence check for validTrade --- eip-0014.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index d9ba10e7..5016ba60 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -670,12 +670,12 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a val BaseAmount = 100L // TODO replace with actual base amount val MinQuoteAmount = 20 // TODO replace with actual minimum quote amount + val poolIn = INPUTS(0) val validTrade = - if (INPUTS.size == 2) { // we don't permit more than one swap per tx + if (INPUTS.size == 2 && poolIn.tokens.size == 3) { // we don't permit more than one swap per tx val FeeDenom = 1000 - val poolIn = INPUTS(0) val poolNFT = poolIn.tokens(0)._1 val poolY_token = poolIn.tokens(2) @@ -730,15 +730,16 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a val FeeNum = 997L // TODO replace with actual feeNum (997 corresponds to 0.3% fee) val MinQuoteAmount = 20 // TODO replace with actual minimum quote amount + val poolIn = INPUTS(0) + val validTrade = - if (INPUTS.size == 2) { + if (INPUTS.size == 2 && poolIn.tokens.size == 3) { val FeeDenom = 1000 val baseToken = SELF.tokens(0) // token being sold val baseTokenId = baseToken._1 val baseAmount = baseToken._2 - val poolIn = INPUTS(0) val poolNFT = poolIn.tokens(0)._1 // first token id is NFT // second token id LP token val poolY_token = poolIn.tokens(2) // third token id is Y asset id @@ -791,10 +792,11 @@ SelfXAmount | Long | NanoErgs to deposit val PoolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual NFT ID val Pk = sigmaProp(true) // TODO replace with actual sigma prop + val poolIn = INPUTS(0) + val validDeposit = - if (INPUTS.size == 2) { + if (INPUTS.size == 2 && poolIn.tokens.size == 3) { val selfY = SELF.tokens(0) - val poolIn = INPUTS(0) val validPoolIn = poolIn.tokens(0)._1 == PoolNFT // No need to check amount, as we assume NFT is in quantity 1 @@ -842,10 +844,11 @@ PoolNFT | Coll[Byte] | Pool NFT ID val PoolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual NFT ID val Pk = sigmaProp(true) // TODO replace with actual sigma prop + val poolIn = INPUTS(0) + val validRedeem = - if (INPUTS.size == 2) { + if (INPUTS.size == 2 && poolIn.tokens.size == 3) { val selfLP = SELF.tokens(0) - val poolIn = INPUTS(0) val validPoolIn = poolIn.tokens(0)._1 == PoolNFT From 68d8466c41fcf5dc68cc99e77cea5302f284a12c Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Mon, 16 Aug 2021 00:27:44 +0530 Subject: [PATCH 57/60] Address comments by @oksin1 for N2T contracts --- eip-0014.md | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 5016ba60..5ae37831 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -683,8 +683,7 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a val poolReservesX = poolIn.value val poolReservesY = poolY_token._2 - val validPoolIn = poolNFT == PoolNFT && poolY_tokenId == QuoteId - val noMoreInputs = INPUTS.size == 2 + val validPoolIn = poolNFT == PoolNFT val rewardBox = OUTPUTS(1) @@ -698,6 +697,7 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a val relaxedOutput = quoteAssetAmount + 1 // handle rounding loss val fairPrice = poolReservesY.toBigInt * BaseAmount * FeeNum <= relaxedOutput * (poolReservesX.toBigInt * FeeDenom + BaseAmount * FeeNum) + validPoolIn && rewardBox.propositionBytes == Pk.propBytes && quoteAssetID == QuoteId && quoteAssetAmount >= MinQuoteAmount && @@ -747,9 +747,7 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a val poolReservesX = poolIn.value // nanoErgs is X asset amount val poolReservesY = poolY_token._2 // third token amount is Y asset amount - val validPoolIn = poolNFT == PoolNFT && poolY_tokenId == baseTokenId - - val noMoreInputs = INPUTS.size == 2 + val validPoolIn = poolNFT == PoolNFT val rewardBox = OUTPUTS(1) @@ -759,13 +757,10 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a val relaxedOutput = quoteAssetAmount + 1 // handle rounding loss val fairPrice = poolReservesX.toBigInt * baseAmount * FeeNum <= relaxedOutput * (poolReservesY.toBigInt * FeeDenom + baseAmount * FeeNum) - val uniqueOutput = INPUTS(rewardBox.R4[Int].get).id == SELF.id // See https://www.ergoforum.org/t/ergoscript-design-patterns/222/15?u=scalahub - validPoolIn && rewardBox.propositionBytes == Pk.propBytes && quoteAssetAmount >= MinQuoteAmount && - fairPrice && - uniqueOutput // prevent multiple input boxes with same script mapping to one single output box + fairPrice } else false sigmaProp(Pk || validTrade) From de30f94ace1c18a9772e1dd0f65f00caf774eea3 Mon Sep 17 00:00:00 2001 From: ScalaHub <23208922+scalahub@users.noreply.github.com> Date: Tue, 31 Aug 2021 23:43:18 +0530 Subject: [PATCH 58/60] Remove redundant check in N2T redeem contract 1. The check of "returnOut.value >= SELF.value - DexFee" is not needed, hence removed 2. Fix author list --- eip-0014.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 5ae37831..087f8853 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -1,6 +1,6 @@ # Automated Decentralized Exchange -* Authors: kushti, Ilya Oskin. scalahub +* Authors: kushti, Ilya Oskin, scalahub * Status: Proposed * Created: 12-Mar-2021 * Last edited: 31-May-2021 @@ -863,7 +863,6 @@ PoolNFT | Coll[Byte] | Pool NFT ID validPoolIn && returnOut.propositionBytes == Pk.propBytes && - returnOut.value >= SELF.value - DexFee && returnY._1 == reservesY._1 && // token id matches returnXAmount >= minReturnX && returnY._2 >= minReturnY From 51ab4412ac2eb77eceadd15a88750615bc244b60 Mon Sep 17 00:00:00 2001 From: oskin1 Date: Thu, 18 Nov 2021 12:38:23 +0300 Subject: [PATCH 59/60] AMM deposit proxy contracts updated. --- eip-0014.md | 78 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 16 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index 087f8853..acf62a43 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -448,19 +448,48 @@ PoolNFT | Coll[Byte] | Pool NFT ID val reservesX = poolIn.tokens(2) val reservesY = poolIn.tokens(3) + val reservesXAmount = reservesX._2 + val reservesYAmount = reservesY._2 + val supplyLP = InitiallyLockedLP - poolLP._2 - val minimalReward = min( - selfX._2.toBigInt * supplyLP / reservesX._2, - selfY._2.toBigInt * supplyLP / reservesY._2 - ) + val minByX = selfX._2.toBigInt * supplyLP / reservesXAmount + val minByY = selfY._2.toBigInt * supplyLP / reservesYAmount + + val minimalReward = min(minByX, minByY) val rewardOut = OUTPUTS(1) val rewardLP = rewardOut.tokens(0) + val validErgChange = rewardOut.value >= SELF.value - DexFee + + val validTokenChange = + if (minByX < minByY && rewardOut.tokens.size == 2) { + val diff = minByY - minByX + val excessY = diff * reservesYAmount / supplyLP + + val changeY = rewardOut.tokens(1) + + changeY._1 == reservesY._1 && + changeY._2 >= excessY + } else if (minByX > minByY && rewardOut.tokens.size == 2) { + val diff = minByX - minByY + val excessX = diff * reservesXAmount / supplyLP + + val changeX = rewardOut.tokens(1) + + changeX._1 == reservesX._1 && + changeX._2 >= excessX + } else if (minByX == minByY) { + true + } else { + false + } + validPoolIn && rewardOut.propositionBytes == Pk.propBytes && - rewardOut.value >= SELF.value - DexFee && + validErgChange && + validTokenChange && rewardLP._1 == poolLP._1 && rewardLP._2 >= minimalReward } else false @@ -782,36 +811,53 @@ SelfXAmount | Long | NanoErgs to deposit { val InitiallyLockedLP = 0x7fffffffffffffffL - val SelfXAmount = 100000000 // TODO replace with actual SelfX - val DexFee = 10000 // TODO replace with actual DexFEe - val PoolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual NFT ID - val Pk = sigmaProp(true) // TODO replace with actual sigma prop - val poolIn = INPUTS(0) val validDeposit = if (INPUTS.size == 2 && poolIn.tokens.size == 3) { val selfY = SELF.tokens(0) - val validPoolIn = poolIn.tokens(0)._1 == PoolNFT // No need to check amount, as we assume NFT is in quantity 1 + val validPoolIn = poolIn.tokens(0)._1 == PoolNFT val poolLP = poolIn.tokens(1) val reservesXAmount = poolIn.value val reservesY = poolIn.tokens(2) + val reservesYAmount = reservesY._2 val supplyLP = InitiallyLockedLP - poolLP._2 - val minimalReward = min( - SelfXAmount.toBigInt * supplyLP / reservesXAmount, - selfY._2.toBigInt * supplyLP / reservesY._2 - ) + val _selfX = SelfX + + val minByX = _selfX.toBigInt * supplyLP / reservesXAmount + val minByY = selfY._2.toBigInt * supplyLP / reservesYAmount + + val minimalReward = min(minByX, minByY) val rewardOut = OUTPUTS(1) val rewardLP = rewardOut.tokens(0) + val validChange = + if (minByX < minByY && rewardOut.tokens.size == 2) { + val diff = minByY - minByX + val excessY = diff * reservesYAmount / supplyLP + + val changeY = rewardOut.tokens(1) + + rewardOut.value >= SELF.value - DexFee - _selfX && + changeY._1 == reservesY._1 && + changeY._2 >= excessY + } else if (minByX >= minByY) { + val diff = minByX - minByY + val excessX = diff * reservesXAmount / supplyLP + + rewardOut.value >= SELF.value - DexFee - (_selfX - excessX) + } else { + false + } + validPoolIn && rewardOut.propositionBytes == Pk.propBytes && - rewardOut.value >= SELF.value - DexFee - SelfXAmount && + validChange && rewardLP._1 == poolLP._1 && rewardLP._2 >= minimalReward } else false From d2ee720821399adcffa1cd0ab05b0e47e466b4da Mon Sep 17 00:00:00 2001 From: oskin1 Date: Sat, 20 Nov 2021 15:19:43 +0300 Subject: [PATCH 60/60] Miner fee fixed in proxy contracts. --- eip-0014.md | 142 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 82 insertions(+), 60 deletions(-) diff --git a/eip-0014.md b/eip-0014.md index acf62a43..ced1fc61 100644 --- a/eip-0014.md +++ b/eip-0014.md @@ -375,7 +375,9 @@ QuoteId | Coll[Byte] | Quote asset ID 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 +MaxMinerFee | Long | Max miner fee allowed at execution stage PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a concrete unique pool) +MinerPropBytes | Coll[Byte] | Miner script ```scala { @@ -398,7 +400,8 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a val rewardBox = OUTPUTS(1) val quoteAsset = rewardBox.tokens(0) val quoteAmount = quoteAsset._2.toBigInt - val fairDexFee = rewardBox.value >= SELF.value - quoteAmount * DexFeePerTokenNum / DexFeePerTokenDenom + val dexFee = quoteAmount * DexFeePerTokenNum / DexFeePerTokenDenom + val fairDexFee = rewardBox.value >= SELF.value - dexFee val relaxedOutput = quoteAmount + 1L // handle rounding loss val poolX = poolAssetX._2.toBigInt val poolY = poolAssetY._2.toBigInt @@ -408,12 +411,17 @@ PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a else poolY * baseAmount * FeeNum <= relaxedOutput * (poolX * FeeDenom + baseAmount * FeeNum) + val validMinerFee = OUTPUTS.map { (o: Box) => + if (o.propositionBytes == MinerPropBytes) o.value else 0L + }.fold(0L, { (a: Long, b: Long) => a + b }) <= MaxMinerFee + validPoolIn && rewardBox.propositionBytes == Pk.propBytes && quoteAsset._1 == QuoteId && quoteAsset._2 >= MinQuoteAmount && fairDexFee && - fairPrice + fairPrice && + validMinerFee } else false sigmaProp(Pk || validTrade) @@ -430,6 +438,8 @@ Constant | Type | Description Pk | SigmaProp | User PublicKey DexFee | Long | DEX fee in nanoERGs PoolNFT | Coll[Byte] | Pool NFT ID +MaxMinerFee | Long | Max miner fee allowed at execution stage +MinerPropBytes | Coll[Byte] | Miner script ```scala { @@ -486,12 +496,17 @@ PoolNFT | Coll[Byte] | Pool NFT ID false } + val validMinerFee = OUTPUTS.map { (o: Box) => + if (o.propositionBytes == MinerPropBytes) o.value else 0L + }.fold(0L, { (a: Long, b: Long) => a + b }) <= MaxMinerFee + validPoolIn && rewardOut.propositionBytes == Pk.propBytes && validErgChange && validTokenChange && rewardLP._1 == poolLP._1 && - rewardLP._2 >= minimalReward + rewardLP._2 >= minimalReward && + validMinerFee } else false sigmaProp(Pk || validDeposit) @@ -508,6 +523,8 @@ Constant | Type | Description Pk | SigmaProp | User PublicKey DexFee | Long | DEX fee in nanoERGs PoolNFT | Coll[Byte] | Pool NFT ID +MaxMinerFee | Long | Max miner fee allowed at execution stage +MinerPropBytes | Coll[Byte] | Miner script ```scala { @@ -535,13 +552,18 @@ PoolNFT | Coll[Byte] | Pool NFT ID val returnX = returnOut.tokens(0) val returnY = returnOut.tokens(1) + val validMinerFee = OUTPUTS.map { (o: Box) => + if (o.propositionBytes == MinerPropBytes) o.value else 0L + }.fold(0L, { (a: Long, b: Long) => a + b }) <= MaxMinerFee + validPoolIn && returnOut.propositionBytes == Pk.propBytes && returnOut.value >= SELF.value - DexFee && returnX._1 == reservesX._1 && returnY._1 == reservesY._1 && returnX._2 >= minReturnX && - returnY._2 >= minReturnY + returnY._2 >= minReturnY && + validMinerFee } else false sigmaProp(Pk || validRedeem) @@ -686,52 +708,47 @@ MinQuoteAmount | Long | Minimal amount of quote asset 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 +MaxMinerFee | Long | Max miner fee allowed at execution stage PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a concrete unique pool) +MinerPropBytes | Coll[Byte] | Miner script ```scala -{ // contract to sell Ergs and buy Token - val PoolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual NFT ID - val QuoteId = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual Quote token id - val DexFeePerTokenNum = 10L // TODO replace with actual Dex fee num - val DexFeePerTokenDenom = 1000L // TODO replace with actual Dex fee denom - val Pk = sigmaProp(true) // TODO replace with actual sigma prop - val FeeNum = 997L // TODO replace with actual feeNum (997 corresponds to 0.3% fee) - val BaseAmount = 100L // TODO replace with actual base amount - val MinQuoteAmount = 20 // TODO replace with actual minimum quote amount +{ // ERG -> Token + val FeeDenom = 1000 val poolIn = INPUTS(0) val validTrade = - if (INPUTS.size == 2 && poolIn.tokens.size == 3) { // we don't permit more than one swap per tx - val FeeDenom = 1000 - + if (INPUTS.size == 2 && poolIn.tokens.size == 3) { val poolNFT = poolIn.tokens(0)._1 - val poolY_token = poolIn.tokens(2) - val poolY_tokenId = poolY_token._1 + val poolY = poolIn.tokens(2) - val poolReservesX = poolIn.value - val poolReservesY = poolY_token._2 + val poolReservesX = poolIn.value.toBigInt + val poolReservesY = poolY._2.toBigInt val validPoolIn = poolNFT == PoolNFT val rewardBox = OUTPUTS(1) - val quoteAsset = rewardBox.tokens(0) + val quoteAsset = rewardBox.tokens(0) + val quoteAmount = quoteAsset._2.toBigInt - val quoteAssetID = quoteAsset._1 - val quoteAssetAmount = quoteAsset._2 + val fairDexFee = rewardBox.value >= SELF.value - quoteAmount * DexFeePerTokenNum / DexFeePerTokenDenom - BaseAmount - val fairDexFee = rewardBox.value >= SELF.value - quoteAssetAmount * DexFeePerTokenNum / DexFeePerTokenDenom - BaseAmount + val relaxedOutput = quoteAmount + 1 // handle rounding loss + val fairPrice = poolReservesY * BaseAmount * FeeNum <= relaxedOutput * (poolReservesX * FeeDenom + BaseAmount * FeeNum) - val relaxedOutput = quoteAssetAmount + 1 // handle rounding loss - val fairPrice = poolReservesY.toBigInt * BaseAmount * FeeNum <= relaxedOutput * (poolReservesX.toBigInt * FeeDenom + BaseAmount * FeeNum) + val validMinerFee = OUTPUTS.map { (o: Box) => + if (o.propositionBytes == MinerPropBytes) o.value else 0L + }.fold(0L, { (a: Long, b: Long) => a + b }) <= MaxMinerFee validPoolIn && rewardBox.propositionBytes == Pk.propBytes && - quoteAssetID == QuoteId && - quoteAssetAmount >= MinQuoteAmount && + quoteAsset._1 == QuoteId && + quoteAmount >= MinQuoteAmount && fairDexFee && - fairPrice + fairPrice && + validMinerFee } else false sigmaProp(Pk || validTrade) @@ -748,48 +765,43 @@ FeeNum | Long | Pool fee numerator (must taken from pool para 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 +MaxMinerFee | Long | Max miner fee allowed at execution stage PoolNFT | Coll[Byte] | ID of the pool NFT (Used as a reference to a concrete unique pool) +MinerPropBytes | Coll[Byte] | Miner script ```scala -{ // contract to sell tokens and buy Ergs - val PoolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual NFT ID - val DexFeePerTokenNum = 10L // TODO replace with actual Dex fee num - val DexFeePerTokenDenom = 1000L // TODO replace with actual Dex fee denom - val Pk = sigmaProp(true) // TODO replace with actual sigma prop - val FeeNum = 997L // TODO replace with actual feeNum (997 corresponds to 0.3% fee) - val MinQuoteAmount = 20 // TODO replace with actual minimum quote amount +{ // Token -> ERG + val FeeDenom = 1000 - val poolIn = INPUTS(0) + val poolIn = INPUTS(0) val validTrade = if (INPUTS.size == 2 && poolIn.tokens.size == 3) { - val FeeDenom = 1000 - - val baseToken = SELF.tokens(0) // token being sold - val baseTokenId = baseToken._1 - val baseAmount = baseToken._2 + val baseAmount = SELF.tokens(0)._2 val poolNFT = poolIn.tokens(0)._1 // first token id is NFT - // second token id LP token - val poolY_token = poolIn.tokens(2) // third token id is Y asset id - val poolY_tokenId = poolY_token._1 - val poolReservesX = poolIn.value // nanoErgs is X asset amount - val poolReservesY = poolY_token._2 // third token amount is Y asset amount + val poolReservesX = poolIn.value.toBigInt // nanoErgs is X asset amount + val poolReservesY = poolIn.tokens(2)._2.toBigInt // third token amount is Y asset amount + val validPoolIn = poolNFT == PoolNFT val rewardBox = OUTPUTS(1) - // bought nanoErgs are called quoteAssetAmount - val deltaNanoErgs = rewardBox.value - SELF.value // this is quoteAssetAmount - fee - val quoteAssetAmount = deltaNanoErgs * DexFeePerTokenDenom / (DexFeePerTokenDenom - DexFeePerTokenNum) - val relaxedOutput = quoteAssetAmount + 1 // handle rounding loss - val fairPrice = poolReservesX.toBigInt * baseAmount * FeeNum <= relaxedOutput * (poolReservesY.toBigInt * FeeDenom + baseAmount * FeeNum) + val deltaNErgs = rewardBox.value - SELF.value // this is quoteAmount - fee + val quoteAmount = deltaNErgs.toBigInt * DexFeePerTokenDenom / (DexFeePerTokenDenom - DexFeePerTokenNum) + val relaxedOutput = quoteAmount + 1 // handle rounding loss + val fairPrice = poolReservesX * baseAmount * FeeNum <= relaxedOutput * (poolReservesY * FeeDenom + baseAmount * FeeNum) + + val validMinerFee = OUTPUTS.map { (o: Box) => + if (o.propositionBytes == MinerPropBytes) o.value else 0L + }.fold(0L, { (a: Long, b: Long) => a + b }) <= MaxMinerFee validPoolIn && rewardBox.propositionBytes == Pk.propBytes && - quoteAssetAmount >= MinQuoteAmount && - fairPrice + quoteAmount >= MinQuoteAmount && + fairPrice && + validMinerFee } else false sigmaProp(Pk || validTrade) @@ -806,6 +818,8 @@ Pk | SigmaProp | User PublicKey DexFee | Long | DEX fee in nanoERGs PoolNFT | Coll[Byte] | Pool NFT ID SelfXAmount | Long | NanoErgs to deposit +MaxMinerFee | Long | Max miner fee allowed at execution stage +MinerPropBytes | Coll[Byte] | Miner script ```scala { @@ -855,11 +869,16 @@ SelfXAmount | Long | NanoErgs to deposit false } + val validMinerFee = OUTPUTS.map { (o: Box) => + if (o.propositionBytes == MinerPropBytes) o.value else 0L + }.fold(0L, { (a: Long, b: Long) => a + b }) <= MaxMinerFee + validPoolIn && rewardOut.propositionBytes == Pk.propBytes && validChange && rewardLP._1 == poolLP._1 && - rewardLP._2 >= minimalReward + rewardLP._2 >= minimalReward && + validMinerFee } else false sigmaProp(Pk || validDeposit) @@ -876,15 +895,13 @@ Constant | Type | Description Pk | SigmaProp | User PublicKey DexFee | Long | DEX fee in nanoERGs PoolNFT | Coll[Byte] | Pool NFT ID +MaxMinerFee | Long | Max miner fee allowed at execution stage +MinerPropBytes | Coll[Byte] | Miner script ```scala { val InitiallyLockedLP = 0x7fffffffffffffffL - val DexFee = 10000 // TODO replace with actual DexFEe - val PoolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual NFT ID - val Pk = sigmaProp(true) // TODO replace with actual sigma prop - val poolIn = INPUTS(0) val validRedeem = @@ -907,11 +924,16 @@ PoolNFT | Coll[Byte] | Pool NFT ID val returnXAmount = returnOut.value - SELF.value + DexFee val returnY = returnOut.tokens(0) + val validMinerFee = OUTPUTS.map { (o: Box) => + if (o.propositionBytes == MinerPropBytes) o.value else 0L + }.fold(0L, { (a: Long, b: Long) => a + b }) <= MaxMinerFee + validPoolIn && returnOut.propositionBytes == Pk.propBytes && returnY._1 == reservesY._1 && // token id matches returnXAmount >= minReturnX && - returnY._2 >= minReturnY + returnY._2 >= minReturnY && + validMinerFee } else false sigmaProp(Pk || validRedeem)