-
Notifications
You must be signed in to change notification settings - Fork 34
Add N2T contracts #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
bcee5ab
ca679c3
4c92df8
3429eb8
520f05f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -510,3 +510,239 @@ 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 | ||
scalahub marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why permanently locked? Why LP can't redeem all the liquidity and destroy the pool? Otherwise unless we consider this locked reserves in deposit / redeem formulas there will be some portion of LP tokens that can never be returned.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redeem complete liquidity and destroying the pool can never happen even in the T2T contracts. The moment you have successor condition, the contract will fail if there is no successor, and so we will just let that small amount of LP stuck till miner can claim it. If we can modify the normal T2T contracts to accommodate destruction, we can port same logic here. We can think of adding more checks to allow complete LP redemption but that will make it more complex, so I suggest we leave it. |
||
|
|
||
| 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 | ||
|
|
||
| ###### 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 | ||
scalahub marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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)) | ||
| } | ||
| ``` | ||
Uh oh!
There was an error while loading. Please reload this page.