Un Ejemplo de ICO Sobre Ergo
10 de abril de 2019

Este artículo describe una ICO (Oferta Inicial de Monedas) completamente funcional implementada en ErgoScript. El ejemplo cubre varias características importantes y novedosas de la Plataforma Ergo y muestra cómo puede soportar contratos complejos con una pequeña cantidad de código.
Parte 1. Preliminares
Una decisión de diseño importante en un protocolo de criptomonedas es especificar qué gasta realmente una transacción de gasto. Aquí hay dos posibilidades. La primera es un modelo basado en UTXO, como en Bitcoin, donde una transacción gasta contenedores de activos de un solo uso (llamados 'monedas' o UTXOs en Bitcoin) y crea nuevos. La otra es un modelo basado en cuentas, como en Nxt, Ethereum o Waves, donde una transacción transfiere una cierta cantidad de activos de una cuenta existente de larga duración a otra, posiblemente nueva, cuenta de larga duración, con posibles efectos secundarios en el camino, como la ejecución de contratos en Waves o Ethereum. En este sentido, Ergo es similar a Bitcoin, porque utiliza el enfoque basado en UTXO, donde se gastan contenedores de un solo uso llamados cajas. Curiosamente, una transacción de Ergo también puede tener entradas de datos que no se gastan, sino que se utilizan para proporcionar información del conjunto actual de cajas no gastadas.
No es trivial crear una ICO sobre un modelo basado en UTXO, porque, a diferencia de los modelos basados en cuentas, no hay almacenamiento persistente explícito aquí. Sin embargo, Ergo lleva una transacción de gasto al contexto de ejecución de un script.
Con este pequeño cambio, se vuelve posible expresar dependencias entre las salidas e entradas de la transacción. A su vez, al establecer dependencias, podemos ejecutar incluso programas Turing-completos arbitrariamente complejos sobre la blockchain (ver el "Self-reproducing Coins as Universal Turing Machine" paper). En este artículo definiremos un escenario concreto de un contrato de múltiples etapas utilizando una ICO, donde tenemos tres etapas (financiación, emisión de tokens, retiro).
Ahora imagina una ICO para miles de participantes. A diferencia de Ethereum, Ergo no proporciona la posibilidad de almacenar grandes conjuntos de datos y llevarlos a cabo durante la ejecución del contrato. Más bien, permite almacenar solo aproximadamente 40 bytes de encabezado de una
estructura de datos, representada como un diccionario clave -> valor, autenticado de manera similar a un árbol de Merkle. Para acceder a algunos elementos
en el diccionario, o para modificarlo, una transacción de gasto que activa la ejecución del script de protección debe
proporcionar pruebas de búsqueda o modificación. Esto da la posibilidad a un contrato de autenticar conjuntos de datos potencialmente enormes sin requerir mucha memoria para almacenar el estado del contrato. Sin embargo, almacenar espacio en el estado (de contratos activos) significaría transacciones más grandes, pero este problema es más fácil desde el punto de vista de escalabilidad, y la escalabilidad es una prioridad máxima para Ergo.
Parte 2. El Contrato ICO
Podría haber muchos escenarios posibles asociados con una Oferta Inicial de Monedas (ICO). En este artículo consideramos una ICO que quiere recaudar al menos una cierta cantidad de fondos (en Ergs) para iniciar el proyecto. Una vez que se cruza el umbral de financiación y termina el período de financiación, el proyecto se inicia y se emiten tokens ICO por parte del proyecto en función de la financiación total recaudada. En la fase de retiro, que se extiende indefinidamente, los inversores retiran tokens ICO en función de la cantidad que habían invertido durante el período de financiación. Los pasos del contrato se describen brevemente a continuación con detalles proporcionados más adelante:
- Primero, tiene lugar la época de financiación. Comienza con una caja del proyecto autenticando un diccionario vacío. El diccionario está destinado a contener pares (inversor, saldo), donde el inversor es un script que protege la caja que contiene los tokens retirados. Para el saldo, asumimos que 1 token es igual a 1 Ergo durante la ICO. Durante la época de financiación, solo es posible poner Ergs en la caja del proyecto.
Una transacción de financiación gasta la caja del proyecto y crea una nueva caja del proyecto con información actualizada. Para ello, una transacción de gasto para la caja del proyecto también tiene otras entradas que contienen scripts de retiro de inversores. Los scripts de inversores y los valores de entrada deben añadirse al árbol de la nueva caja. Podría haber muchas transacciones de financiación encadenadas. - En segundo lugar, finaliza el período de financiación, después del cual el árbol que contiene los datos de los inversores se vuelve de solo lectura. Un árbol autenticado podría tener diferentes operaciones de modificación permitidas individualmente: inserciones, eliminaciones, actualizaciones, o todas las operaciones podrían estar prohibidas (por lo que el árbol podría estar en modo de solo lectura). Además, esta transacción crea tokens del proyecto ICO que se retirarán en la siguiente etapa. El proyecto puede retirar Ergs en esta etapa.
- En tercer lugar, los inversores retiran sus tokens ICO. Para ello, una transacción de gasto crea salidas con condiciones de protección y valores de tokens tomados del árbol. Los pares retirados también se eliminan del árbol. Podría haber muchas transacciones de gasto encadenadas.
Estas tres etapas deben estar vinculadas en el orden lógico. Se utiliza una secuencia de cajas para lograr estos objetivos.
Parte 3. Detalles del Contrato ICO
Ahora es el momento de proporcionar detalles y código ErgoScript de las etapas del contrato ICO.
La Etapa de Financiación
En la etapa de financiación, que es la primera, asumimos que inicialmente un proyecto crea una caja comprometiéndose a un diccionario vacío (almacenado en el registro R5) con un script de protección descrito a continuación. Esta etapa dura al menos hasta la altura 2,000. Más concretamente, la primera transacción con una altura de 2,000 o más debe cambiar el script de la caja de salida como se describe en la siguiente sección (las transacciones a alturas más bajas deben producir una caja con el mismo script).
La caja del proyecto verifica que siempre es la primera entrada y salida de una transacción. Las otras entradas se consideran entradas de inversores. La entrada de un inversor contiene el hash de un script en el registro R4. Este hash representa el script de retiro que se utilizará más adelante en la fase de retiro. Los hashes, así como los valores monetarios de todas las entradas de inversión, deben añadirse al diccionario. La
transacción de gasto proporciona una prueba de que los datos del inversor se han añadido al diccionario, y la prueba se verifica en el contrato.
No se verifica en el subcontrato de financiación que el diccionario permita solo inserciones, y no actualizaciones de valores existentes o eliminaciones (sin embargo, no es difícil añadir una verificación explícita).
La transacción de gasto debe pagar una tarifa, de lo contrario, es poco probable que se incluya en un bloque. Por lo tanto, el contrato de financiación verifica que la transacción de gasto tiene dos salidas (una para sí misma, otra para pagar la tarifa), la tarifa no debe ser más de un cierto límite (solo un nanoErg en nuestro ejemplo), y la proposición de protección debe ser tal que solo un minero pueda gastar la salida (utilizamos solo una variable "feeProp" del entorno de compilación en nuestro ejemplo sin proporcionar detalles). Esta "feeProp" corresponde a un estándar, aunque no requerido por el protocolo.
El código a continuación hace cumplir las condiciones descritas anteriormente. Tenga en cuenta que la
"nextStageScriptHash" variable de entorno contiene el hash del script de la etapa de emisión serializado.
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 Etapa de Emisión
Esta etapa tiene solo una transacción de gasto para llegar a la siguiente etapa (la etapa de retiro). La transacción de gasto realiza las siguientes modificaciones. Primero, cambia la lista de operaciones permitidas en el diccionario de "solo inserciones" a "solo eliminaciones", ya que la siguiente etapa (retiro) solo se ocupa de eliminar entradas del diccionario.
En segundo lugar, el contrato verifica que se emita la cantidad adecuada de tokens ICO. En Ergo, se permite emitir un nuevo tipo de token por transacción, y el identificador del token debe ser igual al (único) identificador de la primera caja de entrada. El subcontrato de emisión verifica que se ha emitido un nuevo token, y la cantidad de este es igual a la cantidad de nanoErgs recaudados por la ICO hasta ahora.
En tercer lugar, el contrato verifica que una transacción de gasto esté recreando efectivamente la caja con el script de protección correspondiente a la siguiente etapa, la etapa de retiro.
Finalmente, el proyecto debe retirar los Ergs recaudados, y, por supuesto, cada transacción de gasto debe pagar una tarifa. Por lo tanto, el subcontrato verifica que la transacción de gasto tiene efectivamente 3 salidas (una para la caja de tokens del proyecto, la caja de retiro de Ergs y la caja de tarifas), y que la primera salida y salida lleva los tokens emitidos. Como no especificamos los detalles del retiro de dinero del proyecto, requerimos una firma del proyecto en la transacción de gasto.
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 Etapa de Retiro
En esta etapa, se permite a los inversores retirar tokens del proyecto protegidos por un script de guardia predefinido (cuyo hash se almacena en el diccionario). Supongamos que el retiro se realiza en lotes de tamaño N. Una transacción de retiro, por lo tanto, tiene N + 2 salidas, donde la primera salida lleva el subcontrato de retiro y los tokens de saldo, la última salida paga la tarifa y las N salidas restantes tienen scripts de protección y valores de tokens de acuerdo con el diccionario. El contrato requiere dos pruebas para los elementos del diccionario: una que prueba que los valores a retirar están efectivamente en el diccionario, y la segunda que prueba que el diccionario resultante no tiene los valores retirados. El subcontrato es el siguiente.
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
Posibles Mejoras
Tenga en cuenta que hay muchas matices que nuestro contrato de ejemplo está ignorando. Por ejemplo, cualquier persona que escuche la blockchain puede ejecutar el contrato y construir transacciones de gasto adecuadas durante las etapas de financiación y retiro. En el mundo real, se puede utilizar una firma adicional del proyecto o un árbitro de confianza.
Además, no se considera un caso de autodestrucción en el contrato de retiro, por lo que vivirá hasta ser destruido por mineros a través del mecanismo de alquiler de almacenamiento, potencialmente durante décadas o incluso siglos. Para la etapa de financiación, sería razonable tener una entrada adicional del proyecto con un valor igual al valor de la salida de tarifa. Y así sucesivamente.
Share post
13 de agosto de 2025
12 de agosto de 2025
9 de julio de 2025
12 de mayo de 2025

9 de febrero de 2022

8 de febrero de 2022

5 de febrero de 2022

1 de febrero de 2022

27 de enero de 2022

20 de enero de 2022

18 de enero de 2022

6 de enero de 2022

4 de enero de 2022

30 de diciembre de 2021

28 de diciembre de 2021

23 de diciembre de 2021








