Zdecentralizowane kontrakty wymiany na Ergo

This page is machine-translated.
Denys Zadorozhnyi

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
}

źródło

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
}

źródło

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
}

źródło

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
  }
}

źródło

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 
}
}

źródło

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 
}
}

źródło

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
}

źródło

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
}

źródło

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
}

źródło

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))

źródło

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)

źródło

Share post

Ergo Infrastructure DAO: Decentralizacja Kręgosłupa Ekosystemu Ergo

Ergo Infrastructure DAO: Decentralizacja Kręgosłupa Ekosystemu Ergo

Misja Ergo zawsze była zakorzeniona w decentralizacji, nie tylko na warstwie konsensusu, ale w całym stosie.

Ergo Platform

13 sierpnia 2025

Mew Finance: Zabawne narzędzie DeFi dla ekosystemu Ergo

Mew Finance: Zabawne narzędzie DeFi dla ekosystemu Ergo

Mew Finance to zestaw aplikacji zdecentralizowanych na blockchainie Ergo.

Ergo Platform

12 sierpnia 2025

Lithos: Decentralizacja wydobycia z użyciem pul on-chain

Lithos: Decentralizacja wydobycia z użyciem pul on-chain

Lithos to nowy protokół zaprojektowany w celu przekształcenia sposobu działania pul wydobywczych poprzez przeniesienie ich na łańc.

Ergo Platform

24 lipca 2025

Sigma 6.0: Mądrzejszy, bardziej elastyczny Ergo

Sigma 6.0: Mądrzejszy, bardziej elastyczny Ergo

Sigma 6.0 to główna proponowana aktualizacja blockchaina Ergo.

Ergo Platform

23 lipca 2025

Kształtowanie przyszłości Rosen: Wezwanie społeczności w sprawie pięciu kluczowych propozycji skarbowych

Kształtowanie przyszłości Rosen: Wezwanie społeczności w sprawie pięciu kluczowych propozycji skarbowych

Współzałożyciel Rosen, Armeanio, złożył pięć nowych propozycji do Skarbu Rosen.

Ergo Platform

9 lipca 2025

Rozszerzony UTXO Ergo i Wzrost Sztucznej Inteligencji Ekonomicznej

Rozszerzony UTXO Ergo i Wzrost Sztucznej Inteligencji Ekonomicznej

Praktyczna Wizja dla Autonomicznych Agentów Ekonomicznych Autonomiczne agenty ekonomiczne na blockchainie Ergo wykonują użyteczną.

Ergo Platform

12 maja 2025

ErgoHACK X: Sztuczna Inteligencja na Blockchainie Ergo

ErgoHACK X: Sztuczna Inteligencja na Blockchainie Ergo

Świętowanie Dekady Zdecentralizowanej Innowacji Dołącz do 10.

Ergo Platform

10 kwietnia 2025