Decentralizované burzové zmluvy na Ergo
31. júla 2020

Ergo má expresívne inteligentné zmluvy a transakčný model, ktorý umožňuje implementáciu bezdôvodného DEX protokolu, v ktorom môžu byť podpísané nákupné a predajné objednávky nezávisle vložené do blockchainu kupujúcimi a predávajúcimi. Služba na zladenie mimo reťazca môže sledovať blockchain Ergo, nájsť zladené objednávky a predložiť transakciu výmeny bez toho, aby poznala akékoľvek tajomstvá. Zladenie môže byť motivované odmenou DEX vyplatenou ako súčasť transakcie výmeny. Každý, kto prvý objaví zladenie dvoch objednávok, môže vytvoriť transakciu výmeny a získať odmenu v ERG. Podporuje sa čiastočné zladenie, čo znamená, že cieľová (nákupná/predajná) objednávka môže byť vykonaná čiastočne, pričom v takom prípade musí byť vytvorená nová "reziduálna" objednávka (box) v tej istej transakcii výmeny. Akúkoľvek objednávku môže kedykoľvek zrušiť "majiteľ".
Zmluva o predajnej objednávke zdroj.
Zmluva o nákupnej objednávke zdroj.
Čiastočné zladenie
Obe zmluvy majú parametre ceny tokenu a poplatku DEX zakódované pri kompilácii. To nám umožňuje skontrolovať aktíva "reziduálnej" objednávky, ERG pre nákupnú objednávku a tokeny pre predajnú objednávku.
V zmluve o nákupnej objednávke hľadáme reziduálny box, pričom kontrolujeme, že má správne parametre a aktíva.
// v prípade čiastočného zladenia by mal byť vytvorený nový box nákupnej objednávky s prostriedkami, ktoré nie sú zladené v tejto transakcii
val foundResidualOrderBoxes = OUTPUTS.filter { (b: Box) =>
val tokenIdParamIsCorrect = b.R4[Coll[Byte]].isDefined && b.R4[Coll[Byte]].get == tokenId
val tokenPriceParamIsCorrect = b.R5[Long].isDefined && b.R5[Long].get == tokenPrice
val dexFeePerTokenParamIsCorrect = b.R6[Long].isDefined && b.R6[Long].get == dexFeePerToken
val contractParamsAreCorrect = tokenIdParamIsCorrect &&
tokenPriceParamIsCorrect && dexFeePerTokenParamIsCorrect
val referenceMe = b.R7[Coll[Byte]].isDefined && b.R7[Coll[Byte]].get == SELF.id
val guardedByTheSameContract = b.propositionBytes == SELF.propositionBytes
contractParamsAreCorrect && referenceMe && guardedByTheSameContract
}
Potom kontrolujeme, že nasledujúce vlastnosti platia:
- Hodnota (ERG) "reziduálnej" objednávky je hodnota aktuálneho boxu (objednávky) mínus hodnota ERG tokenov, ktoré dostávame v tejto transakcii výmeny a mínus poplatok DEX za túto transakciu výmeny.
- V tejto transakcii výmeny je vytvorený iba jeden "reziduálny" objednávkový box.
// ERG zaplatené za zakúpené tokeny
val returnTokenValue = returnTokenAmount * tokenPrice
// vetva pre celkové zladenie (všetky ERG sú minulé a správne množstvo tokenov je zakúpené)
val totalMatching = (SELF.value - expectedDexFee) == returnTokenValue &&
returnBox.value >= fullSpread
// vetva pre čiastočné zladenie, napr. okrem zakúpených tokenov požadujeme novú nákupnú objednávku s ERG pre
// nezladenú časť tejto objednávky
val partialMatching = {
val correctResidualOrderBoxValue = (SELF.value - returnTokenValue - expectedDexFee)
foundResidualOrderBoxes.size == 1 &&
foundResidualOrderBoxes(0).value == correctResidualOrderBoxValue &&
returnBox.value >= fullSpread
}
V zmluve o predajnej objednávke hľadáme reziduálny box, pričom kontrolujeme, že má správne parametre a aktíva.
// v prípade čiastočného zladenia by mal byť vytvorený nový box predajnej objednávky s tokenmi, ktoré nie sú zladené v tejto transakcii
// kontrola, že neskôr v kóde je vytvorený iba jeden taký box
val foundResidualOrderBoxes = OUTPUTS.filter { (b: Box) =>
val tokenIdParamIsCorrect = b.R4[Coll[Byte]].isDefined && b.R4[Coll[Byte]].get == tokenId
val tokenPriceParamIsCorrect = b.R5[Long].isDefined && b.R5[Long].get == tokenPrice
val dexFeePerTokenParamIsCorrect = b.R6[Long].isDefined && b.R6[Long].get == dexFeePerToken
val contractParamsAreCorrect = tokenIdParamIsCorrect &&
tokenPriceParamIsCorrect &&
dexFeePerTokenParamIsCorrect
val referenceMe = b.R7[Coll[Byte]].isDefined && b.R7[Coll[Byte]].get == SELF.id
val guardedByTheSameContract = b.propositionBytes == SELF.propositionBytes
contractParamsAreCorrect && referenceMe && guardedByTheSameContract
}
Potom kontrolujeme, že nasledujúce vlastnosti platia:
- Rozdiel medzi množstvom tokenov v aktuálnom boxe (objednávke) a "reziduálnym" objednávkovým boxom určuje množstvo ERG, ktoré predávajúci dostáva za tokeny "predané" v tejto transakcii výmeny (
soldTokenAmount * tokenPrice). - Hodnota (ERG) "reziduálnej" objednávky je hodnota aktuálneho boxu (objednávky) mínus poplatok DEX za túto transakciu výmeny.
- V tejto transakcii výmeny je vytvorený iba jeden "reziduálny" objednávkový box.
// vetva pre čiastočné zladenie, napr. okrem prijatých ERG požadujeme novú predajnú objednávku s tokenmi pre
// nezladenú časť tejto objednávky
val partialMatching = {
foundResidualOrderBoxes.size == 1 && {
val residualOrderBox = foundResidualOrderBoxes(0)
val residualOrderTokenData = residualOrderBox.tokens(0)
val residualOrderTokenAmount = residualOrderTokenData._2
val soldTokenAmount = selfTokenAmount - residualOrderTokenAmount
val soldTokenErgValue = soldTokenAmount * tokenPrice
val expectedDexFee = dexFeePerToken * soldTokenAmount
val residualOrderTokenId = residualOrderTokenData._1
val tokenIdIsCorrect = residualOrderTokenId == tokenId
val residualOrderValueIsCorrect = residualOrderBox.value == (SELF.value - expectedDexFee)
val returnBoxValueIsCorrect = returnBox.value == soldTokenErgValue + fullSpread(soldTokenAmount)
tokenIdIsCorrect &&
soldTokenAmount >= 1 &&
residualOrderValueIsCorrect &&
returnBoxValueIsCorrect
}
}
Celkové zladenie
Obe predajné a nákupné objednávky môžu byť vykonané v transakcii výmeny úplne. V tomto prípade nie je požiadavka na "reziduálny" objednávkový box.
Pre túto cestu kontrolujeme, že nasledujúce vlastnosti platia.
Pre predajnú objednávku:
- Množstvo ERG, ktoré predávajúci dostáva v tejto transakcii výmeny, musí byť rovné množstvu tokenov v aktuálnej objednávke krát cena tokenu.
val totalMatching = (returnBox.value == selfTokenAmount * tokenPrice + fullSpread(selfTokenAmount))
zdroj
Pre nákupnú objednávku:
- Hodnota tokenu (množstvo tokenov * cena tokenu, v ERG) ktorú kupujúci dostáva v tejto transakcii výmeny, musí byť rovná hodnote aktuálneho boxu (objednávky) mínus poplatok DEX.
val totalMatching = (SELF.value - expectedDexFee) == (returnTokenAmount * tokenPrice) && returnBox.value >= fullSpread
zdroj
Rozpätie ponuky a dopytu
Kontrola triedenia protichodných objednávok
Rozpätie je rozdiel medzi cenou nákupnej (ponukovej) objednávky a cenou predajnej (dopytovej) objednávky. Chceme sa uistiť, že ak existuje rozpätie, "staršia" objednávka ho dostane.
Pre túto zmluvu sa vyžaduje, aby protichodné objednávky (objednávky na výdavky) boli zoradené podľa výšky rozpätia. Tak, aby tie s väčším rozpätím boli "spotrebované" ako prvé.
V zmluve o nákupnej objednávke:
// kontrola, či by táto objednávka mala získať rozpätie pre danú protichodnú objednávku (výšku)
val spreadIsMine = { (counterOrderBoxHeight: Int) =>
// väčšie alebo rovné, pretože iba prísne väčšie dáva výhru v zmluve o predajnej objednávke
// Denys: musíme sa rozhodnúť, kto dostane rozpätie, ak je výška rovná, bez akéhokoľvek dôvodu som si vybral nákupnú objednávku
counterOrderBoxHeight >= SELF.creationInfo._1
}
// kontrola, že protichodné (predajné) objednávky sú zoradené podľa rozpätia v INPUTS
// tak, aby väčšie (horné) rozpätie bolo "spotrebované" ako prvé
val sellOrderBoxesAreSortedBySpread = { (boxes: Coll[Box]) =>
boxes.size > 0 && {
val alledgedlyTopSpread = if (spreadIsMine(boxes(0).creationInfo._1)) {
tokenPrice - boxes(0).R5[Long].getOrElse(0L)
} else { 0L }
boxes.fold((alledgedlyTopSpread, true), { (t: (Long, Boolean), box: Box) =>
val prevSpread = t._1
val isSorted = t._2
val boxTokenPrice = box.R5[Long].getOrElse(0L)
val boxTokenPriceIsCorrect = boxTokenPrice > 0 && boxTokenPrice <= tokenPrice
val spread = if (spreadIsMine(box.creationInfo._1)) {
tokenPrice - boxTokenPrice
} else { 0L }
(spread, isSorted && boxTokenPriceIsCorrect && spread <= prevSpread)
})._2
}
}
Taktiež kontrolujeme, že deklarovaná cena tokenu v registri R5 protichodných predajných objednávok je v správnom rozsahu, aby sme predišli zneužívaniu aritmetického pretečenia a iným podobným útokom.
V zmluve o predajnej objednávke:
// kontrola, či by táto objednávka mala získať rozpätie pre danú protichodnú objednávku (výšku)
val spreadIsMine = { (counterOrderBoxHeight: Int) =>
// prísne väčšie, pretože rovnosť dáva výhru v zmluve o nákupnej objednávke
// Denys: musíme sa rozhodnúť, kto dostane rozpätie, ak je výška rovná, bez akéhokoľvek dôvodu som si vybral nákupnú objednávku
counterOrderBoxHeight > SELF.creationInfo._1
}
// kontrola, že protichodné (nákupné) objednávky sú zoradené podľa rozpätia v INPUTS
// tak, aby väčšie (horné) rozpätie bolo "spotrebované" ako prvé
val buyOrderBoxesAreSortedBySpread = { (boxes: Coll[Box]) =>
boxes.size > 0 && {
val alledgedlyTopSpread = if (spreadIsMine(boxes(0).creationInfo._1)) {
boxes(0).R5[Long].getOrElse(0L) - tokenPrice
} else { 0L }
boxes.fold((alledgedlyTopSpread, true), { (t: (Long, Boolean), box: Box) =>
val prevSpread = t._1
val isSorted = t._2
val boxTokenPrice = box.R5[Long].getOrElse(0L)
// hoci poplatok DEX za nákupnú objednávku sa tu nepoužíva, kontrolujeme, či je kladný ako súčasť sanity check
val boxDexFeePerToken = box.R6[Long].getOrElse(0L)
val spread = if (spreadIsMine(box.creationInfo._1)) { boxTokenPrice - tokenPrice } else { 0L }
(spread, isSorted && boxTokenPrice >= tokenPrice && boxDexFeePerToken > 0L && spread <= prevSpread)
})._2
}
}
Taktiež kontrolujeme, že deklarovaná cena tokenu v registri R5 a poplatok DEX za token v R6 protichodných nákupných objednávok je v správnom rozsahu.
Výpočet rozpätia
Aby sme skontrolovali, že aktuálna objednávka dostane svoje rozpätie, musíme ho najprv vypočítať. S protichodnými objednávkami zoradenými podľa výšky rozpätia začíname "spotrebovávať" ich v tomto poradí, znižujúc počet tokenov, ktoré zostávajú v tejto zhode.
V zmluve o nákupnej objednávke:
// agregované rozpätie, ktoré dostávame zo všetkých protichodných (predajných) objednávok
val fullSpread = {
spendingSellOrders.fold((returnTokenAmount, 0L), { (t: (Long, Long), sellOrder: Box) =>
val returnTokensLeft = t._1
val accumulatedFullSpread = t._2
val sellOrderTokenPrice = sellOrder.R5[Long].get
val sellOrderTokenAmount = sellOrder.tokens(0)._2
val tokenAmountFromThisOrder = min(returnTokensLeft, sellOrderTokenAmount)
if (spreadIsMine(sellOrder.creationInfo._1)) {
// rozpätie je naše
val spreadPerToken = tokenPrice - sellOrderTokenPrice
val sellOrderSpread = spreadPerToken * tokenAmountFromThisOrder
(returnTokensLeft - tokenAmountFromThisOrder, accumulatedFullSpread + sellOrderSpread)
}
else {
// rozpätie nie je naše
(returnTokensLeft - tokenAmountFromThisOrder, accumulatedFullSpread)
}
})._2
}
V zmluve o predajnej objednávke sa musíme spoliehať na cenu tokenu a výšku poplatku DEX, aby sme vypočítali, koľko tokenov je v tejto nákupnej objednávke. Okrem toho, keďže nemôžeme odvodzovať množstvo tokenov "predaných" v tejto transakcii výmeny z hodnoty návratového boxu, robíme výpočet rozpätia parametrizovaný konkrétnym množstvom tokenov, ktoré budeme neskôr poznať v kóde:
// agregované rozpätie, ktoré dostávame zo všetkých protichodných (nákupných) objednávok
val fullSpread = { (tokenAmount: Long) =>
spendingBuyOrders.fold((tokenAmount, 0L), { (t: (Long, Long), buyOrder: Box) =>
val returnTokensLeft = t._1
val accumulatedFullSpread = t._2
val buyOrderTokenPrice = buyOrder.R5[Long].get
val buyOrderDexFeePerToken = buyOrder.R6[Long].get
val buyOrderTokenAmountCapacity = buyOrder.value / (buyOrderTokenPrice + buyOrderDexFeePerToken)
val tokenAmountInThisOrder = min(returnTokensLeft, buyOrderTokenAmountCapacity)
if (spreadIsMine(buyOrder.creationInfo._1)) {
// rozpätie je naše
val spreadPerToken = buyOrderTokenPrice - tokenPrice
val buyOrderSpread = spreadPerToken * tokenAmountInThisOrder
(returnTokensLeft - tokenAmountInThisOrder, accumulatedFullSpread + buyOrderSpread)
}
else {
// rozpätie nie je naše
(returnTokensLeft - tokenAmountInThisOrder, accumulatedFullSpread)
}
})._2
}
Kontrola prijatého rozpätia
S určenou výškou rozpätia musíme skontrolovať, či aktuálna objednávka skutočne dostala rozpätie.
V zmluve o nákupnej objednávke kontrolujeme, že je zahrnuté v hodnote návratového boxu:
// vetva pre celkové zladenie (všetky ERG sú minulé a správne množstvo tokenov je zakúpené)
val totalMatching = (SELF.value - expectedDexFee) == returnTokenValue &&
returnBox.value >= fullSpread
// vetva pre čiastočné zladenie, napr. okrem zakúpených tokenov požadujeme novú nákupnú objednávku s ERG pre
// nezladenú časť tejto objednávky
val partialMatching = {
val correctResidualOrderBoxValue = (SELF.value - returnTokenValue - expectedDexFee)
foundResidualOrderBoxes.size == 1 &&
foundResidualOrderBoxes(0).value == correctResidualOrderBoxValue &&
returnBox.value >= fullSpread
}
V zmluve o predajnej objednávke, akonáhle vieme množstvo tokenov "predaných" v tejto transakcii výmeny, kontrolujeme, že hodnota návratového boxu obsahuje rozpätie.
V prípade celkového zladenia používame celkové množstvo tokenov v aktuálnej objednávke:
// vetva pre celkové zladenie (všetky tokeny sú predané a plná suma ERG je prijatá)
val totalMatching = (returnBox.value == selfTokenAmount * tokenPrice + fullSpread(selfTokenAmount))
V prípade čiastočného zladenia vieme množstvo tokenov "predaných" z reziduálnej objednávky (val soldTokenAmount = selfTokenAmount - residualOrderTokenAmount) a kontrolujeme, že rozpätie je zahrnuté v hodnote návratového boxu:
val returnBoxValueIsCorrect = returnBox.value == soldTokenErgValue + fullSpread(soldTokenAmount)
Share post
9. júla 2025







