Dezentralisierte Austauschverträge auf Ergo
31. Juli 2020

Ergo verfügt über ausdrucksstarke Smart Contracts und ein Transaktionsmodell, das die Implementierung eines vertrauenslosen DEX-Protokolls ermöglicht, bei dem unterzeichnete Kauf- und Verkaufsaufträge unabhängig von Käufern und Verkäufern in die Blockchain eingegeben werden können. Ein Off-Chain-Matching-Service kann die Ergo-Blockchain beobachten, passende Aufträge finden und die Swap-Transaktion einreichen, ohne irgendwelche Geheimnisse zu kennen. Das Matching kann durch DEX-Belohnungen incentiviert werden, die als Teil einer Swap-Transaktion gezahlt werden. Jeder, der zuerst das Matching der beiden Aufträge entdeckt, kann die Swap-Transaktion erstellen und eine Belohnung in ERGs erhalten. Teilweises Matching wird unterstützt, was bedeutet, dass der Ziel-(Kauf-/Verkaufs-)Auftrag teilweise ausgeführt werden kann, in diesem Fall muss ein neuer "Rest"-Auftrag (Box) in derselben Swap-Transaktion erstellt werden. Jeder Auftrag kann jederzeit vom "Besitzer" storniert werden.
Verkaufauftragvertrag Quelle.
Kaufauftragvertrag Quelle.
Teilweises Matching
Beide Verträge haben Tokenpreis- und DEX-Gebührenparameter, die bei der Kompilierung kodiert sind. Dies ermöglicht es uns, die "Rest"-Auftragswerte, ERGs für einen Kaufauftrag und Tokens für einen Verkaufsauftrag zu überprüfen.
Im Kaufauftragvertrag suchen wir nach einer Restbox und überprüfen, ob sie die richtigen Parameter und Vermögenswerte hat.
// im Falle eines teilweisen Matchings sollte eine neue Kaufauftragbox mit Mitteln erstellt werden, die in dieser Transaktion nicht übereinstimmen
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
}
Dann überprüfen wir, ob die folgenden Eigenschaften gelten:
- Der Wert (ERGs) der "Rest"-Auftragsbox ist der Wert der aktuellen Box (Auftrag) minus dem ERGs-Wert der Tokens, die wir in dieser Swap-Transaktion erhalten, und minus der DEX-Gebühr für diese Swap-Transaktion.
- Es wird nur eine "Rest"-Auftragsbox in dieser Swap-Transaktion erstellt.
// ERGs, die für die gekauften Tokens bezahlt wurden
val returnTokenValue = returnTokenAmount * tokenPrice
// Zweig für vollständiges Matching (alle ERGs werden ausgegeben und die korrekte Anzahl an Tokens wird gekauft)
val totalMatching = (SELF.value - expectedDexFee) == returnTokenValue &&
returnBox.value >= fullSpread
// Zweig für teilweises Matching, z.B. neben gekauften Tokens verlangen wir einen neuen Kaufauftrag mit ERGs für
// den nicht übereinstimmenden Teil dieses Auftrags
val partialMatching = {
val correctResidualOrderBoxValue = (SELF.value - returnTokenValue - expectedDexFee)
foundResidualOrderBoxes.size == 1 &&
foundResidualOrderBoxes(0).value == correctResidualOrderBoxValue &&
returnBox.value >= fullSpread
}
Im Verkaufsauftragvertrag suchen wir nach einer Restbox und überprüfen, ob sie die richtigen Parameter und Vermögenswerte hat.
// im Falle eines teilweisen Matchings sollte eine neue Verkaufsauftragbox mit Tokens erstellt werden, die in dieser Transaktion nicht übereinstimmen
// überprüfen, dass später im Code nur eine solche Box erstellt wird
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
}
Dann überprüfen wir, ob die folgenden Eigenschaften gelten:
- Der Unterschied zwischen der Tokenanzahl in der aktuellen Box (Auftrag) und der "Rest"-Auftragsbox bestimmt die Menge an ERGs, die der Verkäufer für die in dieser Swap-Transaktion "verkauften" Tokens erhält (
soldTokenAmount * tokenPrice). - Der Wert (ERGs) der "Rest"-Auftragsbox ist der Wert der aktuellen Box (Auftrag) minus der DEX-Gebühr für diese Swap-Transaktion.
- Es wird nur eine "Rest"-Auftragsbox in dieser Swap-Transaktion erstellt.
// Zweig für teilweises Matching, z.B. neben erhaltenen ERGs verlangen wir einen neuen Verkaufsauftrag mit Tokens für
// den nicht übereinstimmenden Teil dieses Auftrags
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
}
}
Vollständiges Matching
Sowohl Verkaufs- als auch Kaufaufträge können in der Swap-Transaktion vollständig ausgeführt werden. In diesem Fall gibt es keine Anforderung für die "Rest"-Auftragsbox.
Für diesen Pfad überprüfen wir, ob die folgenden Eigenschaften gelten.
Für Verkaufsauftrag:
- Die ERGs-Menge, die der Verkäufer in dieser Swap-Transaktion erhält, muss der Menge an Tokens im aktuellen Auftrag multipliziert mit dem Tokenpreis entsprechen.
val totalMatching = (returnBox.value == selfTokenAmount * tokenPrice + fullSpread(selfTokenAmount))
Quelle
Für Kaufauftrag:
- Der Tokenwert (Tokenanzahl * Tokenpreis, in ERGs), den der Käufer in dieser Swap-Transaktion erhält, muss dem Wert der aktuellen Box (Auftrag) minus der DEX-Gebühr entsprechen.
val totalMatching = (SELF.value - expectedDexFee) == (returnTokenAmount * tokenPrice) && returnBox.value >= fullSpread
Quelle
Bid-Ask-Spanne
Überprüfung der Sortierung der Gegenaufträge
Die Spanne ist der Unterschied zwischen dem Kauf- (Bid) und dem Verkaufs- (Ask) Auftragskurs. Wir möchten sicherstellen, dass, wenn es eine Spanne gibt, der "ältere" Auftrag sie erhält.
Für diesen Vertrag müssen die Gegenaufträge (Ausgabeaufträge) nach der Spannenhöhe sortiert sein. Damit werden die mit einer größeren Spanne zuerst "verbraucht".
Im Kaufauftragvertrag:
// überprüfen, ob dieser Auftrag die Spanne für einen gegebenen Gegenauftrag (Höhe) erhalten sollte
val spreadIsMine = { (counterOrderBoxHeight: Int) =>
// größer oder gleich, da nur ein striktes Größer im Verkaufsauftragvertrag gewinnt
// Denys: wir müssen entscheiden, wer die Spanne erhält, wenn die Höhe gleich ist, ohne Grund habe ich den Kaufauftrag gewählt
counterOrderBoxHeight >= SELF.creationInfo._1
}
// überprüfen, dass die Gegen-(Verkaufs-)Aufträge in INPUTS nach Spanne sortiert sind
// damit die größere (obere) Spanne zuerst "verbraucht" wird
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
}
}
Wir überprüfen auch, ob der erklärte Tokenpreis im R5-Register der Gegen-Verkaufsaufträge im richtigen Bereich liegt, um auszuschließen, dass arithmetische Überläufe und andere ähnliche Angriffe ausgenutzt werden.
Im Verkaufsauftragvertrag:
// überprüfen, ob dieser Auftrag die Spanne für einen gegebenen Gegenauftrag (Höhe) erhalten sollte
val spreadIsMine = { (counterOrderBoxHeight: Int) =>
// strikt größer, da Gleichheit im Kaufauftragvertrag gewinnt
// Denys: wir müssen entscheiden, wer die Spanne erhält, wenn die Höhe gleich ist, ohne Grund habe ich den Kaufauftrag gewählt
counterOrderBoxHeight > SELF.creationInfo._1
}
// überprüfen, dass die Gegen-(Kauf-)Aufträge in INPUTS nach Spanne sortiert sind
// damit die größere (obere) Spanne zuerst "verbraucht" wird
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)
// obwohl die DEX-Gebühr des Kaufauftrags hier nicht verwendet wird, überprüfen wir, ob sie positiv ist, als Teil der Plausibilitätsprüfung
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
}
}
Wir überprüfen auch, ob der erklärte Tokenpreis im R5-Register und die DEX-Gebühr pro Token im R6 der Gegen-Kaufaufträge im richtigen Bereich liegen.
Spannenberechnung
Um zu überprüfen, dass der aktuelle Auftrag seine Spanne erhält, müssen wir sie zuerst berechnen. Mit den nach Spannen sortierten Gegenaufträgen beginnen wir, sie in dieser Reihenfolge zu "verbrauchen", wobei die Anzahl der verbleibenden Tokens in diesem Match verringert wird.
Im Kaufauftragvertrag:
// aggregierte Spanne, die wir aus allen Gegen-(Verkaufs-)Aufträgen erhalten
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)) {
// die Spanne gehört uns
val spreadPerToken = tokenPrice - sellOrderTokenPrice
val sellOrderSpread = spreadPerToken * tokenAmountFromThisOrder
(returnTokensLeft - tokenAmountFromThisOrder, accumulatedFullSpread + sellOrderSpread)
}
else {
// die Spanne gehört uns nicht
(returnTokensLeft - tokenAmountFromThisOrder, accumulatedFullSpread)
}
})._2
}
Im Verkaufsauftragvertrag müssen wir sowohl den Tokenpreis als auch die DEX-Gebühr berücksichtigen, um zu berechnen, wie viele Tokens in diesem Kaufauftrag sind. Darüber hinaus, da wir die Anzahl der "verkauften" Tokens in dieser Swap-Transaktion nicht aus dem Rückgabewert der Box ableiten können, parametrisieren wir die Spannenberechnung mit der konkreten Tokenanzahl, die wir später im Code wissen werden:
// aggregierte Spanne, die wir aus allen Gegen-(Kauf-)Aufträgen erhalten
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)) {
// die Spanne gehört uns
val spreadPerToken = buyOrderTokenPrice - tokenPrice
val buyOrderSpread = spreadPerToken * tokenAmountInThisOrder
(returnTokensLeft - tokenAmountInThisOrder, accumulatedFullSpread + buyOrderSpread)
}
else {
// die Spanne gehört uns nicht
(returnTokensLeft - tokenAmountInThisOrder, accumulatedFullSpread)
}
})._2
}
Überprüfung der erhaltenen Spanne
Mit der bestimmten Spannenhöhe müssen wir überprüfen, ob der aktuelle Auftrag tatsächlich die Spanne erhalten hat.
Im Kaufauftragvertrag überprüfen wir, dass sie im Rückgabewert der Box enthalten ist:
// Zweig für vollständiges Matching (alle ERGs werden ausgegeben und die korrekte Anzahl an Tokens wird gekauft)
val totalMatching = (SELF.value - expectedDexFee) == returnTokenValue &&
returnBox.value >= fullSpread
// Zweig für teilweises Matching, z.B. neben gekauften Tokens verlangen wir einen neuen Kaufauftrag mit ERGs für
// den nicht übereinstimmenden Teil dieses Auftrags
val partialMatching = {
val correctResidualOrderBoxValue = (SELF.value - returnTokenValue - expectedDexFee)
foundResidualOrderBoxes.size == 1 &&
foundResidualOrderBoxes(0).value == correctResidualOrderBoxValue &&
returnBox.value >= fullSpread
}
Im Verkaufsauftragvertrag, sobald wir die Anzahl der "verkauften" Tokens in dieser Swap-Transaktion kennen, überprüfen wir, ob der Rückgabewert der Box die Spanne enthält.
Im Fall des vollständigen Matchings verwenden wir die gesamte Tokenanzahl im aktuellen Auftrag:
// Zweig für vollständiges Matching (alle Tokens werden verkauft und der volle Betrag an ERGs erhalten)
val totalMatching = (returnBox.value == selfTokenAmount * tokenPrice + fullSpread(selfTokenAmount))
Im Fall des teilweisen Matchings kennen wir die Anzahl der "verkauften" Tokens aus dem Restauftrag (val soldTokenAmount = selfTokenAmount - residualOrderTokenAmount) und überprüfen, ob die Spanne im Rückgabewert der Box enthalten ist:
val returnBoxValueIsCorrect = returnBox.value == soldTokenErgValue + fullSpread(soldTokenAmount)
Share post
13. August 2025
12. August 2025
9. Juli 2025
12. Mai 2025
9. Dezember 2024
19. August 2024
