Skip to content
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 236 additions & 0 deletions eip-0014.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

@scalahub scalahub Jul 15, 2021

Choose a reason for hiding this comment

The 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
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))
}
```