Un Esempio di ICO Su Ergo
10 aprile 2019

Questo articolo descrive un ICO (Offerta Iniziale di Monete) completo implementato in ErgoScript. L'esempio copre diverse caratteristiche importanti e innovative della Piattaforma Ergo e mostra come può supportare contratti complessi con una quantità minima di codice.
Parte 1. Preliminari
Una decisione di design importante in un protocollo di criptovaluta è specificare cosa spende effettivamente una transazione di spesa. Qui ci sono due possibilità. La prima è un modello basato su UTXO, come in Bitcoin, dove una transazione spende contenitori di asset usa e getta (chiamati 'monete' o UTXO in Bitcoin) e ne crea di nuovi. L'altra è un modello basato su account, come in Nxt, Ethereum o Waves, dove una transazione trasferisce una certa quantità di asset da un account esistente a lungo termine a un altro, possibilmente nuovo, account a lungo termine, con possibili effetti collaterali lungo il cammino, come l'esecuzione di contratti in Waves o Ethereum. A questo proposito, Ergo è simile a Bitcoin, perché utilizza l'approccio basato su UTXO, dove contenitori usa e getta chiamati box vengono spesi. È interessante notare che una transazione Ergo può anche avere input di dati che non vengono spesi, ma piuttosto utilizzati per fornire alcune informazioni dall'insieme attuale di box non spesi.
Non è banale creare un ICO su un modello basato su UTXO, perché, a differenza dei modelli basati su account, qui non c'è uno storage persistente esplicito. Tuttavia, Ergo porta una transazione di spesa nel contesto di esecuzione di uno script.
Con questo piccolo cambiamento diventa possibile esprimere dipendenze tra gli output e gli input delle transazioni. A sua volta, impostando le dipendenze possiamo eseguire anche programmi Turing-completi arbitrariamente complessi sopra la blockchain (vedi il "Self-reproducing Coins as Universal Turing Machine" paper). In questo articolo definiremo uno scenario concreto di un contratto multi-fase utilizzando un ICO, dove abbiamo tre fasi (finanziamento, emissione di token, prelievo).
Ora immagina un ICO per migliaia di partecipanti. A differenza di Ethereum, Ergo non fornisce la possibilità di memorizzare grandi set di dati e portarli avanti durante l'esecuzione del contratto. Piuttosto, consente di memorizzare solo circa 40 byte di intestazione di una struttura di dati, rappresentata come dizionario chiave -> valore, autenticata in modo simile a un albero di Merkle. Per accedere ad alcuni elementi nel dizionario, o per modificarlo, una transazione di spesa che attiva l'esecuzione dello script di protezione dovrebbe fornire prove di ricerca o modifica. Questo offre la possibilità a un contratto di autenticare potenzialmente enormi set di dati senza richiedere molta memoria per memorizzare lo stato del contratto. Tuttavia, memorizzare spazio nello stato (dei contratti attivi) significherebbe transazioni più grandi, ma questo problema è più facile dal punto di vista della scalabilità, e la scalabilità è una priorità assoluta per Ergo.
Parte 2. Il Contratto ICO
Ci potrebbero essere molti possibili scenari associati a un'Offerta Iniziale di Monete (ICO). In questo articolo consideriamo un ICO che vuole raccogliere almeno una certa quantità di fondi (in Ergs) per avviare il progetto. Una volta superata la soglia di finanziamento e terminato il periodo di finanziamento, il progetto viene avviato e i token ICO vengono emessi dal progetto in base al totale dei fondi raccolti. Nella fase di prelievo, che si estende all'infinito, gli investitori prelevano i token ICO in base all'importo che avevano investito durante il periodo di finanziamento. I passaggi del contratto sono brevemente descritti di seguito con dettagli forniti successivamente:
- Prima, si svolge l'epoca di finanziamento. Inizia con una box del progetto che autentica un dizionario vuoto. Il dizionario è destinato a contenere coppie (investitore, saldo), dove l'investitore è uno script che protegge la box contenente i token prelevati. Per il saldo, assumiamo che 1 token sia uguale a 1 Ergo durante l'ICO. Durante l'epoca di finanziamento, è possibile solo inserire Ergs nella box del progetto.
Una transazione di finanziamento spende la box del progetto e crea una nuova box del progetto con informazioni aggiornate. Per questo, una transazione di spesa per la box del progetto ha anche altri input che contengono script di prelievo degli investitori. Gli script degli investitori e i valori di input devono essere aggiunti all'albero della nuova box. Ci potrebbero essere molte transazioni di finanziamento concatenate. - In secondo luogo, il periodo di finanziamento termina, dopo di che l'albero che contiene i dati degli investitori diventa di sola lettura. Un albero autenticato potrebbe avere diverse operazioni di modifica consentite individualmente: inserimenti, cancellazioni, aggiornamenti, o tutte le operazioni potrebbero essere vietate (quindi l'albero potrebbe essere in modalità di sola lettura). Inoltre, questa transazione crea token del progetto ICO che saranno prelevati nella fase successiva. Il progetto può prelevare Ergs in questa fase.
- In terzo luogo, gli investitori prelevano i loro token ICO. Per questo, una transazione di spesa crea output con condizioni di protezione e valori di token presi dall'albero. Le coppie prelevate vengono anche rimosse dall'albero. Ci potrebbero essere molte transazioni di spesa concatenate.
Queste tre fasi dovrebbero essere collegate insieme nell'ordine logico. Una sequenza di box viene utilizzata per raggiungere questi obiettivi.
Parte 3. I Dettagli del Contratto ICO
Ora è il momento di fornire dettagli e codice ErgoScript delle fasi del contratto ICO.
La Fase di Finanziamento
Nella fase di finanziamento, che viene per prima, assumiamo che inizialmente un progetto crea una box impegnandosi a un dizionario vuoto (memorizzato nel registro R5) con uno script di protezione descritto di seguito. Questa fase dura almeno fino all'altezza 2.000. Più concretamente, la prima transazione con un'altezza di 2.000 o superiore dovrebbe cambiare lo script della box di output come descritto nella sezione successiva (le transazioni a altezze inferiori devono produrre una box con lo stesso script).
La box del progetto controlla che sia sempre il primo input e output di una transazione. Gli altri input sono considerati input degli investitori. Un input dell'investitore contiene l'hash di uno script nel registro R4. Questo hash rappresenta lo script di prelievo che sarà utilizzato successivamente nella fase di prelievo. Gli hash così come i valori monetari di tutti gli input di investimento devono essere aggiunti al dizionario. La
transazione di spesa fornisce una prova che i dati degli investitori sono stati effettivamente aggiunti al dizionario, e la prova viene controllata nel contratto.
Non viene controllato nel sub-contratto di finanziamento che il dizionario consenta solo inserimenti, e non aggiornamenti di valori esistenti o rimozioni (non è difficile aggiungere un controllo esplicito però).
La transazione di spesa dovrebbe pagare una commissione, altrimenti, è improbabile che venga inclusa in un blocco. Pertanto, il contratto di finanziamento controlla che la transazione di spesa abbia due output (uno per se stessa, l'altro per pagare la commissione), la commissione non deve superare un certo limite (solo un nanoErg nel nostro esempio), e la proposizione di protezione dovrebbe essere tale che solo un miner possa spendere l'output (utilizziamo solo una variabile "feeProp" dall'ambiente di compilazione nel nostro esempio senza fornire ulteriori dettagli). Questo "feeProp" corrisponde a uno standard, anche se non richiesto dal protocollo.
Il codice qui sotto impone le condizioni descritte sopra. Si prega di notare che la
variabile ambientale "nextStageScriptHash" contiene l'hash dello script serializzato della fase di emissione.
val selfIndexIsZero = INPUTS(0).id == SELF.id
val proof = getVar[Coll[Byte]](1).get
val inputsCount = INPUTS.size
val toAdd: Coll[(Coll[Byte], Coll[Byte])] = INPUTS.slice(1, inputsCount).map({ (b: Box) =>
val pk = b.R4[Coll[Byte]].get
val value = longToByteArray(b.value)
(pk, value)
})
val modifiedTree = SELF.R5[AvlTree].get.insert(toAdd, proof).get
val expectedTree = OUTPUTS(0).R5[AvlTree].get
val properTreeModification = modifiedTree == expectedTree
val outputsCount = OUTPUTS.size == 2
val selfOutputCorrect = if(HEIGHT < 2000) {
OUTPUTS(0).propositionBytes == SELF.propositionBytes
} else {
blake2b256(OUTPUTS(0).propositionBytes) == nextStageScriptHash
}
val feeOutputCorrect = (OUTPUTS(1).value <= 1) && (OUTPUTS(1).propositionBytes == feeBytes)
val outputsCorrect = outputsCount && feeOutputCorrect && selfOutputCorrect
selfIndexIsZero && outputsCorrect && properTreeModification
La Fase di Emissione
Questa fase ha solo una transazione di spesa per passare alla fase successiva (la fase di prelievo). Le transazioni di spesa apportano le seguenti modifiche. In primo luogo, cambia l'elenco delle operazioni consentite sul dizionario da "solo inserimenti" a "solo rimozioni", poiché la fase successiva (prelievo) si occupa solo di rimuovere voci dal dizionario.
In secondo luogo, il contratto controlla che la giusta quantità di token ICO venga emessa. In Ergo, è consentito emettere un nuovo tipo di token per transazione, e l'identificatore del token dovrebbe essere uguale all'identificatore (unico) della prima box di input. Il sub-contratto di emissione controlla che un nuovo token sia stato emesso, e la sua quantità è uguale alla quantità di nanoErgs raccolti dall'ICO fino a quel momento.
In terzo luogo, il contratto controlla che una transazione di spesa stia effettivamente ricreando la box con lo script di protezione corrispondente alla fase successiva, la fase di prelievo.
Infine, il progetto dovrebbe prelevare gli Ergs raccolti, e ovviamente, ogni transazione di spesa dovrebbe pagare una commissione. Pertanto, il sub-contratto controlla che la transazione di spesa abbia effettivamente 3 output (uno ciascuno per la box dei token del progetto, la box di prelievo degli Ergs e la box della commissione), e che il primo output e output porti i token emessi. Poiché non specifichiamo i dettagli del prelievo di denaro del progetto, richiediamo una firma del progetto sulla transazione di spesa.
val openTree = SELF.R5[AvlTree].get
val closedTree = OUTPUTS(0).R5[AvlTree].get
val digestPreserved = openTree.digest == closedTree.digest
val keyLengthPreserved = openTree.keyLength == closedTree.keyLength
val valueLengthPreserved = openTree.valueLengthOpt == closedTree.valueLengthOpt
val treeIsClosed = closedTree.enabledOperations == 4
val tokenId: Coll[Byte] = INPUTS(0).id
val tokensIssued = OUTPUTS(0).tokens(0)._2
val outputsCountCorrect = OUTPUTS.size == 3
val secondOutputNoTokens = OUTPUTS(0).tokens.size == 1 && OUTPUTS(1).tokens.size == 0 && OUTPUTS(2).tokens.size == 0
val correctTokensIssued = SELF.value == tokensIssued
val correctTokenId = OUTPUTS(0).R4[Coll[Byte]].get == tokenId && OUTPUTS(0).tokens(0)._1 == tokenId
val valuePreserved = outputsCountCorrect && secondOutputNoTokens && correctTokensIssued && correctTokenId
val stateChanged = blake2b256(OUTPUTS(0).propositionBytes) == nextStageScriptHash
val treeIsCorrect = digestPreserved && valueLengthPreserved && keyLengthPreserved && treeIsClosed
projectPubKey && treeIsCorrect && valuePreserved && stateChanged
La Fase di Prelievo
In questa fase, agli investitori è consentito prelevare i token del progetto protetti da uno script di protezione predefinito (il cui hash è memorizzato nel dizionario). Supponiamo che il prelievo venga effettuato in lotti di dimensione N. Una transazione di prelievo, quindi, ha N + 2 output, dove il primo output porta il sub-contratto di prelievo e i token di saldo, l'ultimo output paga la commissione e i restanti N output hanno script di protezione e valori di token secondo il dizionario. Il contratto richiede due prove per gli elementi del dizionario: una che dimostra che i valori da prelevare sono effettivamente nel dizionario, e la seconda che dimostra che il dizionario risultante non ha i valori prelevati. Il sub-contratto è qui sotto.
val removeProof = getVar[Coll[Byte]](2).get
val lookupProof = getVar[Coll[Byte]](3).get
val withdrawIndexes = getVar[Coll[Int]](4).get
val out0 = OUTPUTS(0)
val tokenId: Coll[Byte] = SELF.R4[Coll[Byte]].get
val withdrawals = withdrawIndexes.map({(idx: Int) =>
val b = OUTPUTS(idx)
if(b.tokens(0)._1 == tokenId) {
(blake2b256(b.propositionBytes), b.tokens(0)._2)
} else {
(blake2b256(b.propositionBytes), 0L)
}
})
val withdrawValues = withdrawals.map({(t: (Coll[Byte], Long)) => t._2})
val withdrawTotal = withdrawValues.fold(0L, { (l1: Long, l2: Long) => l1 + l2 })
val toRemove = withdrawals.map({(t: (Coll[Byte], Long)) => t._1})
val initialTree = SELF.R5[AvlTree].get
val removedValues = initialTree.getMany(toRemove, lookupProof).map({(o: Option[Coll[Byte]]) => byteArrayToLong(o.get)})
val valuesCorrect = removedValues == withdrawValues
val modifiedTree = initialTree.remove(toRemove, removeProof).get
val expectedTree = out0.R5[AvlTree].get
val selfTokensCorrect = SELF.tokens(0)._1 == tokenId
val selfOutTokensAmount = SELF.tokens(0)._2
val soutTokensCorrect = out0.tokens(0)._1 == tokenId
val soutTokensAmount = out0.tokens(0)._2
val tokensPreserved = selfTokensCorrect && soutTokensCorrect && (soutTokensAmount + withdrawTotal == selfOutTokensAmount)
val properTreeModification = modifiedTree == expectedTree
val selfOutputCorrect = out0.propositionBytes == SELF.propositionBytes
properTreeModification && valuesCorrect && selfOutputCorrect && tokensPreserved
Possibili Miglioramenti
Si prega di notare che ci sono molte sfumature che il nostro contratto di esempio ignora. Ad esempio, chiunque ascolti la blockchain è autorizzato a eseguire il contratto e costruire transazioni di spesa appropriate durante le fasi di finanziamento e prelievo. Nel mondo reale, potrebbe essere utilizzata un'ulteriore firma da parte del progetto o di un arbitro fidato.
Inoltre, non viene considerato alcun caso di autodistruzione nel contratto di prelievo, quindi vivrà fino a essere distrutto dai miner tramite il meccanismo di affitto di storage, potenzialmente per decenni o addirittura secoli. Per la fase di finanziamento, sarebbe ragionevole avere un input aggiuntivo dal progetto con un valore uguale a quello dell'output della commissione. E così via.
Share post
13 agosto 2025
9 luglio 2025
12 maggio 2025
7 agosto 2022




















