Contratti di Scambio Decentralizzati su Ergo
31 luglio 2020

Ergo ha contratti intelligenti espressivi e un modello transazionale che consente l'implementazione di un protocollo DEX senza fiducia, in cui ordini di acquisto e vendita firmati possono essere inseriti nella blockchain in modo indipendente da acquirenti e venditori. Un servizio di abbinamento off-chain può osservare la blockchain di Ergo, trovare ordini corrispondenti e inviare la transazione di scambio senza conoscere alcun segreto. L'abbinamento può essere incentivato da una ricompensa DEX pagata come parte di una transazione di scambio. Chiunque scopra per primo l'abbinamento dei due ordini può creare la transazione di scambio e ricevere una ricompensa in ERG. L'abbinamento parziale è supportato, il che significa che l'ordine target (acquisto/vendita) può essere eseguito parzialmente, nel qual caso un nuovo ordine "residuo" (box) deve essere creato nella stessa transazione di scambio. Qualsiasi ordine può essere annullato in qualsiasi momento dal "proprietario".
Contratto di ordine di vendita source.
Contratto di ordine di acquisto source.
Abbinamento parziale
Entrambi i contratti hanno parametri di prezzo del token e commissione DEX codificati in una compilazione. Questo ci consente di controllare gli asset dell'ordine "residuo", ERG per un ordine di acquisto e token per un ordine di vendita.
Nel contratto di ordine di acquisto, cerchiamo un box residuo, controllando che abbia parametri e asset corretti.
// in caso di abbinamento parziale, un nuovo box di ordine di acquisto dovrebbe essere creato con fondi che non sono stati abbinati in questa tx
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
}
Poi, controlliamo che le seguenti proprietà siano valide:
- Il valore (ERG) del box di ordine "residuo" è il valore del box corrente (ordine) meno il valore in ERG dei token che stiamo ricevendo in questa transazione di scambio e meno la commissione DEX per questa transazione di scambio.
- Solo un box di ordine "residuo" è creato in questa transazione di scambio.
// ERG pagati per i token acquistati
val returnTokenValue = returnTokenAmount * tokenPrice
// ramo per abbinamento totale (tutti gli ERG sono spesi e la corretta quantità di token è acquistata)
val totalMatching = (SELF.value - expectedDexFee) == returnTokenValue &&
returnBox.value >= fullSpread
// ramo per abbinamento parziale, ad esempio, oltre ai token acquistati, richiediamo un nuovo ordine di acquisto con ERG per
// la parte non abbinata di questo ordine
val partialMatching = {
val correctResidualOrderBoxValue = (SELF.value - returnTokenValue - expectedDexFee)
foundResidualOrderBoxes.size == 1 &&
foundResidualOrderBoxes(0).value == correctResidualOrderBoxValue &&
returnBox.value >= fullSpread
}
Nel contratto di ordine di vendita, cerchiamo un box residuo, controllando che abbia parametri e asset corretti.
// in caso di abbinamento parziale, un nuovo box di ordine di vendita dovrebbe essere creato con token che non sono stati abbinati in questa tx
// controlla che ci sia solo un box di questo tipo creato più avanti nel codice
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
}
Poi, controlliamo che le seguenti proprietà siano valide:
- La differenza tra la quantità di token nel box corrente (ordine) e il box di ordine "residuo" determina la quantità di ERG che il venditore riceve per i token "venduti" in questa transazione di scambio (
soldTokenAmount * tokenPrice). - Il valore (ERG) del box di ordine "residuo" è il valore del box corrente (ordine) meno la commissione DEX per questa transazione di scambio.
- Solo un box di ordine "residuo" è creato in questa transazione di scambio.
// ramo per abbinamento parziale, ad esempio, oltre agli ERG ricevuti, richiediamo un nuovo ordine di vendita con token per
// la parte non abbinata di questo ordine
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
}
}
Abbinamento totale
Sia gli ordini di vendita che quelli di acquisto possono essere eseguiti nella transazione di scambio interamente. In questo caso, non c'è alcun requisito per il box di ordine "residuo".
Per questo percorso, controlliamo che le seguenti proprietà siano valide.
Per l'ordine di vendita:
- La quantità di ERG che il venditore riceve in questa transazione di scambio deve essere uguale alla quantità di token nell'ordine corrente moltiplicata per il prezzo del token.
val totalMatching = (returnBox.value == selfTokenAmount * tokenPrice + fullSpread(selfTokenAmount))
source
Per l'ordine di acquisto:
- Il valore del token (quantità di token * prezzo del token, in ERG) che l'acquirente riceve in questa transazione di scambio deve essere uguale al valore del box corrente (ordine) meno la commissione DEX.
val totalMatching = (SELF.value - expectedDexFee) == (returnTokenAmount * tokenPrice) && returnBox.value >= fullSpread
source
Spread Bid-Ask
Controllo di ordinamento degli ordini contro
Lo spread è la differenza tra il prezzo dell'ordine di acquisto (bid) e il prezzo dell'ordine di vendita (ask). Vogliamo assicurarci che, se c'è uno spread, l'ordine "più vecchio" lo ottenga.
Per questo contratto, è richiesto che gli ordini contro (ordini di spesa) siano ordinati per importo di spread. In modo che quelli con uno spread maggiore vengano "consumati" per primi.
Nel contratto di ordine di acquisto:
// controlla se questo ordine dovrebbe ottenere lo spread per un dato ordine contro (altezza)
val spreadIsMine = { (counterOrderBoxHeight: Int) =>
// maggiore o uguale poiché solo un maggiore rigoroso dà vittoria nel contratto di ordine di vendita
// Denys: dobbiamo decidere chi ottiene lo spread se l'altezza è uguale, senza alcun motivo ho scelto l'ordine di acquisto
counterOrderBoxHeight >= SELF.creationInfo._1
}
// controlla che gli ordini contro (vendita) siano ordinati per spread in INPUTS
// in modo che lo spread maggiore (superiore) venga "consumato" per primo
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
}
}
Controlliamo anche che il prezzo del token dichiarato nel registro R5 degli ordini di vendita contro sia nell'intervallo corretto per prevenire l'esploitazione di overflow aritmetico e altri attacchi simili.
Nel contratto di ordine di vendita:
// controlla se questo ordine dovrebbe ottenere lo spread per un dato ordine contro (altezza)
val spreadIsMine = { (counterOrderBoxHeight: Int) =>
// maggiore rigorosamente poiché l'uguaglianza dà vittoria nel contratto di ordine di acquisto
// Denys: dobbiamo decidere chi ottiene lo spread se l'altezza è uguale, senza alcun motivo ho scelto l'ordine di acquisto
counterOrderBoxHeight > SELF.creationInfo._1
}
// controlla che gli ordini contro (acquisto) siano ordinati per spread in INPUTS
// in modo che lo spread maggiore (superiore) venga "consumato" per primo
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)
// anche se la commissione DEX dell'ordine di acquisto non è utilizzata qui, controlliamo se è positiva come parte del controllo di sanità
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
}
}
Controlliamo anche che il prezzo del token dichiarato nel registro R5 e la commissione DEX per token in R6 degli ordini di acquisto contro siano nell'intervallo corretto.
Calcolo dello spread
Per controllare che l'ordine corrente ottenga il proprio spread, dobbiamo calcolarlo prima. Con gli ordini contro ordinati per importo di spread, iniziamo a "consumare" in quell'ordine, diminuendo il numero di token rimasti in questo abbinamento.
Nel contratto di ordine di acquisto:
// spread aggregato che otteniamo da tutti gli ordini contro (vendita)
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)) {
// lo spread è nostro
val spreadPerToken = tokenPrice - sellOrderTokenPrice
val sellOrderSpread = spreadPerToken * tokenAmountFromThisOrder
(returnTokensLeft - tokenAmountFromThisOrder, accumulatedFullSpread + sellOrderSpread)
}
else {
// lo spread non è nostro
(returnTokensLeft - tokenAmountFromThisOrder, accumulatedFullSpread)
}
})._2
}
Nel contratto di ordine di vendita, dobbiamo fare affidamento sia sul prezzo del token che sull'importo della commissione DEX per calcolare quanti token ci sono in quell'ordine di acquisto. Oltre a ciò, poiché non possiamo dedurre la quantità di token "venduti" in questa transazione di scambio dal valore del box di ritorno, rendiamo il calcolo dello spread parametrizzato con la quantità concreta di token che conosceremo più avanti nel codice:
// spread aggregato che otteniamo da tutti gli ordini contro (acquisto)
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)) {
// lo spread è nostro
val spreadPerToken = buyOrderTokenPrice - tokenPrice
val buyOrderSpread = spreadPerToken * tokenAmountInThisOrder
(returnTokensLeft - tokenAmountInThisOrder, accumulatedFullSpread + buyOrderSpread)
}
else {
// lo spread non è nostro
(returnTokensLeft - tokenAmountInThisOrder, accumulatedFullSpread)
}
})._2
}
Controlla lo spread ricevuto
Con l'importo dello spread determinato, dobbiamo controllare se l'ordine corrente ha effettivamente ricevuto lo spread.
Nel contratto di ordine di acquisto, controlliamo che sia incluso nel valore del box di ritorno:
// ramo per abbinamento totale (tutti gli ERG sono spesi e la corretta quantità di token è acquistata)
val totalMatching = (SELF.value - expectedDexFee) == returnTokenValue &&
returnBox.value >= fullSpread
// ramo per abbinamento parziale, ad esempio, oltre ai token acquistati, richiediamo un nuovo ordine di acquisto con ERG per
// la parte non abbinata di questo ordine
val partialMatching = {
val correctResidualOrderBoxValue = (SELF.value - returnTokenValue - expectedDexFee)
foundResidualOrderBoxes.size == 1 &&
foundResidualOrderBoxes(0).value == correctResidualOrderBoxValue &&
returnBox.value >= fullSpread
}
Nel contratto di ordine di vendita, non appena sappiamo la quantità di token "venduti" in questa transazione di scambio, controlliamo che il valore del box di ritorno abbia incluso lo spread.
Nel caso di abbinamento totale, utilizziamo la quantità totale di token nell'ordine corrente:
// ramo per abbinamento totale (tutti i token sono venduti e l'importo totale di ERG ricevuto)
val totalMatching = (returnBox.value == selfTokenAmount * tokenPrice + fullSpread(selfTokenAmount))
Nel caso di abbinamento parziale, sappiamo l'importo del token "venduto" dall'ordine residuo (val soldTokenAmount = selfTokenAmount - residualOrderTokenAmount) e controlliamo che lo spread sia incluso nel valore del box di ritorno:
val returnBoxValueIsCorrect = returnBox.value == soldTokenErgValue + fullSpread(soldTokenAmount)
Share post
13 agosto 2025
9 luglio 2025
12 maggio 2025
7 agosto 2022




















