Децентрализованные контракты обмена на Ergo

This page is machine-translated.
Denys Zadorozhnyi

31 июля 2020 г.

Ergo имеет выразительные смарт-контракты и транзакционную модель, которая позволяет реализовать протокол DEX без доверия, в котором подписанные ордера на покупку и продажу могут быть помещены в блокчейн независимо от покупателей и продавцов. Внецепочечный сервис сопоставления может наблюдать за блокчейном Ergo, находить совпадающие заказы и отправлять транзакцию обмена, не зная никаких секретов. Сопоставление может быть стимулировано вознаграждением DEX, выплачиваемым как часть транзакции обмена. Любой, кто первым обнаружит совпадение двух заказов, может создать транзакцию обмена и получить вознаграждение в ERG. Поддерживается частичное сопоставление, что означает, что целевой (покупка/продажа) заказ может быть выполнен частично, в этом случае новый "остаточный" заказ (коробка) должен быть создан в той же транзакции обмена. Любой заказ может быть отменен в любое время "владельцем".

Контракт на продажу source.

Контракт на покупку source.

Частичное сопоставление

Оба контракта имеют параметры цены токена и комиссии DEX, закодированные при компиляции. Это позволяет нам проверять активы "остаточного" заказа, ERG для заказа на покупку и токены для заказа на продажу.

В контракте на покупку мы ищем остаточную коробку, проверяя, что у нее правильные параметры и активы.

// в случае частичного сопоставления новая коробка заказа на покупку должна быть создана с фондами, которые не совпадают в этой транзакции
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
}

source

Затем мы проверяем, что выполняются следующие свойства:

  • Значение (ERG) "остаточной" коробки заказа равно значению текущей коробки (заказа) минус значение ERG токенов, которые мы получаем в этой транзакции обмена, и минус комиссия DEX за эту транзакцию обмена.
  • В этой транзакции обмена создается только одна "остаточная" коробка заказа.
// ERG, выплаченные за купленные токены
val returnTokenValue = returnTokenAmount * tokenPrice
// ветвь для полного сопоставления (все ERG потрачены и правильное количество токенов куплено)
val totalMatching = (SELF.value - expectedDexFee) == returnTokenValue && 
  returnBox.value >= fullSpread
// ветвь для частичного сопоставления, например, помимо купленных токенов мы требуем новый заказ на покупку с ERG для 
// несопоставленной части этого заказа
val partialMatching = {
  val correctResidualOrderBoxValue = (SELF.value - returnTokenValue - expectedDexFee)
  foundResidualOrderBoxes.size == 1 && 
    foundResidualOrderBoxes(0).value == correctResidualOrderBoxValue && 
    returnBox.value >= fullSpread
}

source

В контракте на продажу мы ищем остаточную коробку, проверяя, что у нее правильные параметры и активы.

// в случае частичного сопоставления новая коробка заказа на продажу должна быть создана с токенами, которые не совпадают в этой транзакции
// проверьте, что только одна такая коробка создается позже в коде
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
}

source

Затем мы проверяем, что выполняются следующие свойства:

  • Разница между количеством токенов в текущей коробке (заказе) и "остаточной" коробкой заказа определяет количество ERG, которые продавец получает за токены, "проданные" в этой транзакции обмена (soldTokenAmount * tokenPrice).
  • Значение (ERG) "остаточной" коробки заказа равно значению текущей коробки (заказа) минус комиссия DEX за эту транзакцию обмена.
  • В этой транзакции обмена создается только одна "остаточная" коробка заказа.
// ветвь для частичного сопоставления, например, помимо полученных ERG мы требуем новый заказ на продажу с токенами для 
// несопоставленной части этого заказа
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
  }
}

source

Полное сопоставление

Оба заказа на продажу и покупку могут быть выполнены в транзакции обмена полностью. В этом случае нет требования к "остаточной" коробке заказа.
Для этого пути мы проверяем, что выполняются следующие свойства.
Для заказа на продажу:

  • Количество ERG, которое продавец получает в этой транзакции обмена, должно быть равно количеству токенов в текущем заказе, умноженному на цену токена.
    val totalMatching = (returnBox.value == selfTokenAmount * tokenPrice + fullSpread(selfTokenAmount))
    source

Для заказа на покупку:

  • Значение токена (количество токенов * цена токена, в ERG), которое покупатель получает в этой транзакции обмена, должно быть равно значению текущей коробки (заказа) минус комиссия DEX.
    val totalMatching = (SELF.value - expectedDexFee) == (returnTokenAmount * tokenPrice) && returnBox.value >= fullSpread
    source

Спред бид-аск

Проверка сортировки контрзаказов

Спред — это разница между ценой заказа на покупку (бид) и ценой заказа на продажу (аск). Мы хотим убедиться, что если есть спред, "старший" заказ получает его.
Для этого контракта требуется, чтобы контрзаказы (расходные заказы) были отсортированы по размеру спреда. Таким образом, те, у кого больший спред, будут "потребляться" первыми.
В контракте на покупку:

// проверьте, должен ли этот заказ получить спред для данного контрзаказа (высота)
val spreadIsMine = { (counterOrderBoxHeight: Int) => 
// больше или равно, так как только строгое больше дает выигрыш в контракте на продажу
// Денис: мы должны решить, кто получает спред, если высота равна, без какой-либо причины я выбрал заказ на покупку
counterOrderBoxHeight >= SELF.creationInfo._1 
}

// проверьте, что контрзаказы (продажа) отсортированы по спреду в INPUTS
// так что больший (верхний) спред будет "потребляться" первым
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 
}
}

source

Мы также проверяем, что заявленная цена токена в регистре R5 контрзаказов на продажу находится в правильном диапазоне, чтобы предотвратить эксплуатацию арифметического переполнения и другие подобные атаки.

В контракте на продажу:

// проверьте, должен ли этот заказ получить спред для данного контрзаказа (высота)
val spreadIsMine = { (counterOrderBoxHeight: Int) => 
// строго больше, так как равенство дает выигрыш в контракте на покупку
// Денис: мы должны решить, кто получает спред, если высота равна, без какой-либо причины я выбрал заказ на покупку
counterOrderBoxHeight > SELF.creationInfo._1 
}

// проверьте, что контрзаказы (покупка) отсортированы по спреду в INPUTS
// так что больший (верхний) спред будет "потребляться" первым
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)
    // хотя комиссия DEX за заказ на покупку здесь не используется, мы проверяем, является ли она положительной как часть проверки
    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 
}
}

source

Мы также проверяем, что заявленная цена токена в регистре R5, и комиссия DEX за токен в R6 контрзаказов на покупку находятся в правильном диапазоне.

Расчет спреда

Чтобы проверить, что текущий заказ получает свой спред, нам нужно сначала его рассчитать. С контрзаказами, отсортированными по размеру спреда, мы начинаем "потреблять" их в этом порядке, уменьшая количество токенов, оставшихся в этом совпадении.
В контракте на покупку:

// агрегированный спред, который мы получаем от всех контрзаказов (продажа)
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)) {
      // спред наш
      val spreadPerToken = tokenPrice - sellOrderTokenPrice
      val sellOrderSpread = spreadPerToken * tokenAmountFromThisOrder
      (returnTokensLeft - tokenAmountFromThisOrder, accumulatedFullSpread + sellOrderSpread)
    }
    else {
      // спред не наш
      (returnTokensLeft - tokenAmountFromThisOrder, accumulatedFullSpread)
    }
  })._2
}

source

В контракте на продажу нам нужно полагаться как на цену токена, так и на сумму комиссии DEX, чтобы рассчитать, сколько токенов в этом заказе на покупку. Кроме того, поскольку мы не можем вывести количество токенов, "проданных" в этой транзакции обмена, из значения возвращаемой коробки, мы делаем расчет спреда параметризованным конкретным количеством токенов, которое мы узнаем позже в коде:

// агрегированный спред, который мы получаем от всех контрзаказов (покупка)
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)) {
      // спред наш
      val spreadPerToken = buyOrderTokenPrice - tokenPrice
      val buyOrderSpread = spreadPerToken * tokenAmountInThisOrder
      (returnTokensLeft - tokenAmountInThisOrder, accumulatedFullSpread + buyOrderSpread)
    }
    else {
      // спред не наш
      (returnTokensLeft - tokenAmountInThisOrder, accumulatedFullSpread)
    }
  })._2
}

source

Проверка полученного спреда

С определенной суммой спреда нам нужно проверить, действительно ли текущий заказ получил спред.
В контракте на покупку мы проверяем, что он включен в значение возвращаемой коробки:

// ветвь для полного сопоставления (все ERG потрачены и правильное количество токенов куплено)
val totalMatching = (SELF.value - expectedDexFee) == returnTokenValue && 
  returnBox.value >= fullSpread
// ветвь для частичного сопоставления, например, помимо купленных токенов мы требуем новый заказ на покупку с ERG для 
// несопоставленной части этого заказа
val partialMatching = {
  val correctResidualOrderBoxValue = (SELF.value - returnTokenValue - expectedDexFee)
  foundResidualOrderBoxes.size == 1 && 
    foundResidualOrderBoxes(0).value == correctResidualOrderBoxValue && 
    returnBox.value >= fullSpread
}

source

В контракте на продажу, как только мы знаем количество токенов, "проданных" в этой транзакции обмена, мы проверяем, что значение возвращаемой коробки включает спред.
В случае полного сопоставления мы используем общее количество токенов в текущем заказе:

// ветвь для полного сопоставления (все токены проданы и полное количество ERG получено)
val totalMatching = (returnBox.value == selfTokenAmount * tokenPrice + fullSpread(selfTokenAmount))

source

В случае частичного сопоставления мы знаем количество токенов, "проданных" из остаточного заказа (val soldTokenAmount = selfTokenAmount - residualOrderTokenAmount) и проверяем, что спред включен в значение возвращаемой коробки:

val returnBoxValueIsCorrect = returnBox.value == soldTokenErgValue + fullSpread(soldTokenAmount)

source

Share post

Ergo Infrastructure DAO: Децентрализация основного каркаса экосистемы Ergo

Ergo Infrastructure DAO: Децентрализация основного каркаса экосистемы Ergo

Миссия Ergo всегда была основана на децентрализации, не только на уровне консенсуса, но и на всем стеке.

Ergo Platform

13 августа 2025 г.

Mew Finance: Игровой DeFi инструмент для экосистемы Ergo

Mew Finance: Игровой DeFi инструмент для экосистемы Ergo

Mew Finance — это децентрализованный набор приложений на блокчейне Ergo.

Ergo Platform

12 августа 2025 г.

Lithos: Децентрализация майнинга с помощью ончейн пулов

Lithos: Децентрализация майнинга с помощью ончейн пулов

Lithos — это новый протокол, разработанный для изменения работы майнинг-пулов, перемещая их в ончейн, предоставляя майнерам полный.

Ergo Platform

24 июля 2025 г.

Sigma 6.0: Более умный и гибкий Ergo

Sigma 6.0: Более умный и гибкий Ergo

Sigma 6.0 — это значительное предложенное обновление для блокчейна Ergo.

Ergo Platform

23 июля 2025 г.

Формирование будущего Rosen: Общественный призыв к пяти ключевым предложениям казначейства

Формирование будущего Rosen: Общественный призыв к пяти ключевым предложениям казначейства

Соучредитель Rosen, Armeanio, представил пять новых предложений в казначейство Rosen.

Ergo Platform

9 июля 2025 г.

Расширенный UTXO Ergo и восход искусственного экономического интеллекта

Расширенный UTXO Ergo и восход искусственного экономического интеллекта

Практическое видение автономных экономических агентов Автономные экономические агенты на блокчейне Ergo выполняют полезную работу.

Ergo Platform

12 мая 2025 г.

ErgoHACK X: Искусственный Интеллект на Блокчейне Ergo

ErgoHACK X: Искусственный Интеллект на Блокчейне Ergo

Празднование Десятилетия Децентрализованных Инноваций Присоединяйтесь к 10-летнему юбилею ErgoHACK и будьте на переднем крае револ.

Ergo Platform

10 апреля 2025 г.