Децентрализованные контракты обмена на Ergo
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
}
Затем мы проверяем, что выполняются следующие свойства:
- Значение (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
}
В контракте на продажу мы ищем остаточную коробку, проверяя, что у нее правильные параметры и активы.
// в случае частичного сопоставления новая коробка заказа на продажу должна быть создана с токенами, которые не совпадают в этой транзакции
// проверьте, что только одна такая коробка создается позже в коде
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
}
Затем мы проверяем, что выполняются следующие свойства:
- Разница между количеством токенов в текущей коробке (заказе) и "остаточной" коробкой заказа определяет количество 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
}
}
Полное сопоставление
Оба заказа на продажу и покупку могут быть выполнены в транзакции обмена полностью. В этом случае нет требования к "остаточной" коробке заказа.
Для этого пути мы проверяем, что выполняются следующие свойства.
Для заказа на продажу:
- Количество 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
}
}
Мы также проверяем, что заявленная цена токена в регистре 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
}
}
Мы также проверяем, что заявленная цена токена в регистре 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
}
В контракте на продажу нам нужно полагаться как на цену токена, так и на сумму комиссии 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
}
Проверка полученного спреда
С определенной суммой спреда нам нужно проверить, действительно ли текущий заказ получил спред.
В контракте на покупку мы проверяем, что он включен в значение возвращаемой коробки:
// ветвь для полного сопоставления (все 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
}
В контракте на продажу, как только мы знаем количество токенов, "проданных" в этой транзакции обмена, мы проверяем, что значение возвращаемой коробки включает спред.
В случае полного сопоставления мы используем общее количество токенов в текущем заказе:
// ветвь для полного сопоставления (все токены проданы и полное количество ERG получено)
val totalMatching = (returnBox.value == selfTokenAmount * tokenPrice + fullSpread(selfTokenAmount))
В случае частичного сопоставления мы знаем количество токенов, "проданных" из остаточного заказа (val soldTokenAmount = selfTokenAmount - residualOrderTokenAmount) и проверяем, что спред включен в значение возвращаемой коробки:
val returnBoxValueIsCorrect = returnBox.value == soldTokenErgValue + fullSpread(soldTokenAmount)
Share post
13 августа 2025 г.
9 июля 2025 г.
12 мая 2025 г.






