Zdecentralizowane kontrakty wymiany na Ergo
31 lipca 2020

Ergo ma ekspresywne inteligentne kontrakty i model transakcyjny, który pozwala na wdrożenie protokołu DEX bez zaufania, w którym podpisane zlecenia kupna i sprzedaży mogą być umieszczane w blockchainie niezależnie przez kupujących i sprzedających. Usługa dopasowywania off-chain może obserwować blockchain Ergo, znajdować dopasowane zlecenia i przesyłać transakcję wymiany bez znajomości jakichkolwiek sekretów. Dopasowanie może być motywowane nagrodą DEX wypłacaną jako część transakcji wymiany. Każdy, kto jako pierwszy odkryje dopasowanie dwóch zleceń, może stworzyć transakcję wymiany i otrzymać nagrodę w ERG. Wspierane jest częściowe dopasowanie, co oznacza, że zlecenie docelowe (kupna/sprzedaży) może być realizowane częściowo, w takim przypadku nowe zlecenie "residualne" (box) musi być utworzone w tej samej transakcji wymiany. Każde zlecenie może być anulowane w dowolnym momencie przez "właściciela".
Kontrakt zlecenia sprzedaży źródło.
Kontrakt zlecenia kupna źródło.
Częściowe dopasowanie
Oba kontrakty mają parametry ceny tokena i opłaty DEX zakodowane podczas kompilacji. Umożliwia to sprawdzenie aktywów zlecenia "residualnego", ERG dla zlecenia kupna i tokenów dla zlecenia sprzedaży.
W kontrakcie zlecenia kupna, szukamy residualnego boxa, sprawdzając, czy ma poprawne parametry i aktywa.
// w przypadku częściowego dopasowania nowy box zlecenia kupna powinien być utworzony z funduszami, które nie są dopasowane w tej transakcji
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
}
Następnie sprawdzamy, czy spełnione są następujące właściwości:
- Wartość (ERG) boxa zlecenia "residualnego" to wartość bieżącego boxa (zlecenia) minus wartość ERG tokenów, które otrzymujemy w tej transakcji wymiany oraz minus opłata DEX za tę transakcję wymiany.
- Tylko jeden box zlecenia "residualnego" jest tworzony w tej transakcji wymiany.
// ERG zapłacone za zakupione tokeny
val returnTokenValue = returnTokenAmount * tokenPrice
// gałąź dla całkowitego dopasowania (wszystkie ERG są wydane, a poprawna ilość tokenów jest kupiona)
val totalMatching = (SELF.value - expectedDexFee) == returnTokenValue &&
returnBox.value >= fullSpread
// gałąź dla częściowego dopasowania, np. oprócz zakupionych tokenów żądamy nowego zlecenia kupna z ERG za
// niedopasowaną część tego zlecenia
val partialMatching = {
val correctResidualOrderBoxValue = (SELF.value - returnTokenValue - expectedDexFee)
foundResidualOrderBoxes.size == 1 &&
foundResidualOrderBoxes(0).value == correctResidualOrderBoxValue &&
returnBox.value >= fullSpread
}
W kontrakcie zlecenia sprzedaży, szukamy residualnego boxa, sprawdzając, czy ma poprawne parametry i aktywa.
// w przypadku częściowego dopasowania nowy box zlecenia sprzedaży powinien być utworzony z tokenami, które nie są dopasowane w tej transakcji
// sprawdź, że tylko jeden taki box jest tworzony później w kodzie
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
}
Następnie sprawdzamy, czy spełnione są następujące właściwości:
- Różnica między ilością tokenów w bieżącym boxie (zleceniu) a boxem zlecenia "residualnego" określa ilość ERG, które sprzedawca otrzymuje za tokeny "sprzedane" w tej transakcji wymiany (
soldTokenAmount * tokenPrice). - Wartość (ERG) boxa zlecenia "residualnego" to wartość bieżącego boxa (zlecenia) minus opłata DEX za tę transakcję wymiany.
- Tylko jeden box zlecenia "residualnego" jest tworzony w tej transakcji wymiany.
// gałąź dla częściowego dopasowania, np. oprócz otrzymanych ERG żądamy nowego zlecenia sprzedaży z tokenami za
// niedopasowaną część tego zlecenia
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
}
}
Całkowite dopasowanie
Zarówno zlecenia sprzedaży, jak i kupna mogą być realizowane w transakcji wymiany w całości. W takim przypadku nie ma wymogu dla boxa zlecenia "residualnego".
Dla tej ścieżki sprawdzamy, czy spełnione są następujące właściwości.
Dla zlecenia sprzedaży:
- Ilość ERG, którą sprzedawca otrzymuje w tej transakcji wymiany, musi być równa ilości tokenów w bieżącym zleceniu pomnożonej przez cenę tokena.
val totalMatching = (returnBox.value == selfTokenAmount * tokenPrice + fullSpread(selfTokenAmount))
źródło
Dla zlecenia kupna:
- Wartość tokena (ilość tokenów * cena tokena, w ERG), którą kupujący otrzymuje w tej transakcji wymiany, musi być równa wartości bieżącego boxa (zlecenia) minus opłata DEX.
val totalMatching = (SELF.value - expectedDexFee) == (returnTokenAmount * tokenPrice) && returnBox.value >= fullSpread
źródło
Spread bid-ask
Sprawdzanie sortowania zleceń kontrujących
Spread to różnica między ceną zlecenia kupna (bid) a ceną zlecenia sprzedaży (ask). Chcemy upewnić się, że jeśli istnieje spread, "starsze" zlecenie go otrzymuje.
Dla tego kontraktu wymaga, aby zlecenia kontrujące (zlecenia wydające) były uporządkowane według wysokości spreadu. Tak, aby te z większym spreadem były "zużywane" jako pierwsze.
W kontrakcie zlecenia kupna:
// sprawdź, czy to zlecenie powinno otrzymać spread dla danego zlecenia kontrującego (wysokości)
val spreadIsMine = { (counterOrderBoxHeight: Int) =>
// większe lub równe, ponieważ tylko ściśle większe daje wygraną w kontrakcie zlecenia sprzedaży
// Denys: musimy zdecydować, kto dostaje spread, jeśli wysokość jest równa, bez żadnego powodu wybrałem zlecenie kupna
counterOrderBoxHeight >= SELF.creationInfo._1
}
// sprawdź, czy zlecenia kontrujące (sprzedaży) są posortowane według spreadu w INPUTS
// tak, aby większy (górny) spread był "zużywany" jako pierwszy
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
}
}
Sprawdzamy również, czy zadeklarowana cena tokena w rejestrze R5 zleceń sprzedaży kontrujących jest w poprawnym zakresie, aby zapobiec wykorzystaniu przepełnienia arytmetycznego i innych podobnych ataków.
W kontrakcie zlecenia sprzedaży:
// sprawdź, czy to zlecenie powinno otrzymać spread dla danego zlecenia kontrującego (wysokości)
val spreadIsMine = { (counterOrderBoxHeight: Int) =>
// ściśle większe, ponieważ równość daje wygraną w kontrakcie zlecenia kupna
// Denys: musimy zdecydować, kto dostaje spread, jeśli wysokość jest równa, bez żadnego powodu wybrałem zlecenie kupna
counterOrderBoxHeight > SELF.creationInfo._1
}
// sprawdź, czy zlecenia kontrujące (kupna) są posortowane według spreadu w INPUTS
// tak, aby większy (górny) spread był "zużywany" jako pierwszy
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)
// chociaż opłata DEX zlecenia kupna nie jest tutaj używana, sprawdzamy, czy jest dodatnia jako część sprawdzenia poprawności
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
}
}
Sprawdzamy również, czy zadeklarowana cena tokena w rejestrze R5 oraz opłata DEX za token w R6 zleceń kupna kontrujących jest w poprawnym zakresie.
Obliczanie spreadu
Aby sprawdzić, czy bieżące zlecenie otrzymuje swój spread, musimy najpierw go obliczyć. Z zleceniami kontrującymi posortowanymi według wysokości spreadu, zaczynamy je "zużywać" w tej kolejności, zmniejszając liczbę tokenów pozostałych w tym dopasowaniu.
W kontrakcie zlecenia kupna:
// skumulowany spread, który otrzymujemy ze wszystkich zleceń kontrujących (sprzedaży)
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)) {
// spread jest nasz
val spreadPerToken = tokenPrice - sellOrderTokenPrice
val sellOrderSpread = spreadPerToken * tokenAmountFromThisOrder
(returnTokensLeft - tokenAmountFromThisOrder, accumulatedFullSpread + sellOrderSpread)
}
else {
// spread nie jest nasz
(returnTokensLeft - tokenAmountFromThisOrder, accumulatedFullSpread)
}
})._2
}
W kontrakcie zlecenia sprzedaży musimy polegać zarówno na cenie tokena, jak i na wysokości opłaty DEX, aby obliczyć, ile tokenów znajduje się w tym zleceniu kupna. Oprócz tego, ponieważ nie możemy wydedukować ilości tokenów "sprzedanych" w tej transakcji wymiany z wartości boxa zwrotnego, obliczamy spread parametryzowany konkretną ilością tokenów, którą poznamy później w kodzie:
// skumulowany spread, który otrzymujemy ze wszystkich zleceń kontrujących (kupna)
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)) {
// spread jest nasz
val spreadPerToken = buyOrderTokenPrice - tokenPrice
val buyOrderSpread = spreadPerToken * tokenAmountInThisOrder
(returnTokensLeft - tokenAmountInThisOrder, accumulatedFullSpread + buyOrderSpread)
}
else {
// spread nie jest nasz
(returnTokensLeft - tokenAmountInThisOrder, accumulatedFullSpread)
}
})._2
}
Sprawdzenie otrzymanego spreadu
Mając określoną wysokość spreadu, musimy sprawdzić, czy bieżące zlecenie rzeczywiście otrzymało spread.
W kontrakcie zlecenia kupna sprawdzamy, czy jest on uwzględniony w wartości boxa zwrotnego:
// gałąź dla całkowitego dopasowania (wszystkie ERG są wydane, a poprawna ilość tokenów jest kupiona)
val totalMatching = (SELF.value - expectedDexFee) == returnTokenValue &&
returnBox.value >= fullSpread
// gałąź dla częściowego dopasowania, np. oprócz zakupionych tokenów żądamy nowego zlecenia kupna z ERG za
// niedopasowaną część tego zlecenia
val partialMatching = {
val correctResidualOrderBoxValue = (SELF.value - returnTokenValue - expectedDexFee)
foundResidualOrderBoxes.size == 1 &&
foundResidualOrderBoxes(0).value == correctResidualOrderBoxValue &&
returnBox.value >= fullSpread
}
W kontrakcie zlecenia sprzedaży, gdy tylko znamy ilość tokenów "sprzedanych" w tej transakcji wymiany, sprawdzamy, czy wartość boxa zwrotnego zawiera spread.
W przypadku całkowitego dopasowania używamy całkowitej ilości tokenów w bieżącym zleceniu:
// gałąź dla całkowitego dopasowania (wszystkie tokeny są sprzedane, a pełna ilość ERG otrzymana)
val totalMatching = (returnBox.value == selfTokenAmount * tokenPrice + fullSpread(selfTokenAmount))
W przypadku częściowego dopasowania znamy ilość tokenów "sprzedanych" z zlecenia residualnego (val soldTokenAmount = selfTokenAmount - residualOrderTokenAmount) i sprawdzamy, czy spread jest uwzględniony w wartości boxa zwrotnego:
val returnBoxValueIsCorrect = returnBox.value == soldTokenErgValue + fullSpread(soldTokenAmount)
Share post
13 sierpnia 2025
12 maja 2025






