Um Exemplo de ICO em Cima do Ergo
10 de abril de 2019

Este artigo descreve uma ICO (Oferta Inicial de Moedas) totalmente funcional implementada em ErgoScript. O exemplo abrange várias características importantes e novas da Plataforma Ergo e mostra como ela pode suportar contratos complexos com uma quantidade mínima de código.
Parte 1. Preliminares
Uma decisão de design importante em um protocolo de criptomoeda é especificar o que uma transação de gasto realmente gasta. Existem duas possibilidades aqui. A primeira é um modelo baseado em UTXO, como no Bitcoin, onde uma transação gasta contêineres de ativos de uso único (chamados de 'moedas' ou UTXOs no Bitcoin) e cria novos. A outra é um modelo baseado em contas, como no Nxt, Ethereum ou Waves, onde uma transação transfere uma certa quantidade de ativos de uma conta existente de longa duração para outra, possivelmente nova, conta de longa duração, com possíveis efeitos colaterais no caminho, como execução de contrato em Waves ou Ethereum. Nesse sentido, o Ergo é semelhante ao Bitcoin, porque usa a abordagem baseada em UTXO, onde contêineres de uso único chamados boxes estão sendo gastos. Curiosamente, uma transação Ergo também pode ter entradas de dados que não estão sendo gastas, mas sim usadas para fornecer algumas informações do conjunto atual de boxes não gastos.
Não é trivial criar uma ICO em cima de um modelo baseado em UTXO, porque, ao contrário dos modelos baseados em contas, não há armazenamento persistente explícito aqui. No entanto, o Ergo traz uma transação de gasto para o contexto de execução de um script.
Com essa pequena mudança, torna-se possível expressar dependências entre saídas e entradas de transação. Por sua vez, ao definir dependências, podemos executar até mesmo programas Turing-completos arbitrariamente complexos em cima da blockchain (veja o artigo "Moedas Auto-reproduzíveis como Máquina de Turing Universal"). Neste artigo, definiremos um cenário concreto de um contrato de múltiplas etapas usando uma ICO, onde temos três etapas (financiamento, emissão de tokens, retirada).
Agora imagine uma ICO para milhares de participantes. Ao contrário do Ethereum, o Ergo não fornece a possibilidade de armazenar grandes conjuntos de dados e transportá-los ao longo da execução do contrato. Em vez disso, permite armazenar apenas cerca de 40 bytes de cabeçalho de uma estrutura de dados, representada como um dicionário chave -> valor, autenticado de forma semelhante a uma árvore de Merkle. Para acessar alguns elementos no dicionário, ou para modificá-lo, uma transação de gasto que está acionando a execução do script de proteção deve fornecer provas de busca ou modificação. Isso dá a possibilidade de um contrato autenticar conjuntos de dados potencialmente enormes sem exigir muita memória para armazenar o estado do contrato. No entanto, armazenar espaço no estado (de contratos ativos) significaria transações maiores, mas esse problema é mais fácil do ponto de vista da escalabilidade, e a escalabilidade é uma prioridade máxima para o Ergo.
Parte 2. O Contrato ICO
Podem existir muitos cenários possíveis associados a uma Oferta Inicial de Moedas (ICO). Neste artigo, consideramos uma ICO que deseja coletar pelo menos uma certa quantia de fundos (em Ergs) para iniciar o projeto. Uma vez que o limite de financiamento é ultrapassado e o período de financiamento termina, o projeto é iniciado e os tokens ICO são emitidos pelo projeto com base no total de financiamento coletado. Na fase de retirada, que se estende para sempre, os investidores retiram tokens ICO com base na quantia que investiram durante o período de financiamento. As etapas do contrato são brevemente descritas abaixo, com detalhes fornecidos a seguir:
- Primeiro, ocorre a época de financiamento. Ela começa com uma box do projeto autenticando um dicionário vazio. O dicionário é destinado a manter pares (investidor, saldo), onde o investidor é um script que protege a box contendo tokens retirados. Para o saldo, assumimos que 1 token é igual a 1 Ergo durante a ICO. Durante a época de financiamento, é possível apenas colocar Ergs na box do projeto.
Uma transação de financiamento gasta a box do projeto e cria uma nova box do projeto com informações atualizadas. Para isso, uma transação de gasto para a box do projeto também tem outras entradas que contêm scripts de retirada do investidor. Scripts de investidor e valores de entrada devem ser adicionados à árvore da nova box. Podem haver muitas transações de financiamento encadeadas. - Em segundo lugar, o período de financiamento termina, após o qual a árvore que contém os dados dos investidores se torna somente leitura. Uma árvore autenticada pode ter diferentes operações de modificação permitidas individualmente: inserções, exclusões, atualizações, ou todas as operações podem ser proibidas (então a árvore pode estar no modo somente leitura). Além disso, essa transação cria tokens do projeto ICO que serão retirados na próxima etapa. O projeto pode retirar Ergs nesta etapa.
- Em terceiro lugar, os investidores retiram seus tokens ICO. Para isso, uma transação de gasto cria saídas com condições de proteção e valores de tokens retirados da árvore. Os pares retirados também são removidos da árvore. Podem haver muitas transações de gasto encadeadas.
Essas três etapas devem ser vinculadas em ordem lógica. Uma sequência de boxes é usada para alcançar esses objetivos.
Parte 3. Detalhes do Contrato ICO
Agora é hora de fornecer detalhes e código ErgoScript das etapas do contrato ICO.
A Etapa de Financiamento
Na etapa de financiamento, que vem primeiro, assumimos que inicialmente um projeto cria uma box comprometendo-se a um dicionário vazio (armazenado no registro R5) com um script de proteção descrito abaixo. Esta etapa dura pelo menos até a altura 2.000. Mais concretamente, a primeira transação com altura de 2.000 ou mais deve alterar o script da box de saída conforme descrito na próxima seção (transações em alturas mais baixas devem produzir uma box com o mesmo script).
A box do projeto verifica se é sempre a primeira entrada e saída de uma transação. As outras entradas são consideradas entradas dos investidores. A entrada de um investidor contém o hash de um script no registro R4. Esse hash representa o script de retirada que será usado mais tarde na fase de retirada. Os hashes, assim como os valores monetários de todas as entradas de investimento, devem ser adicionados ao dicionário. A
transação de gasto fornece uma prova de que os dados do investidor foram de fato adicionados ao dicionário, e a prova é verificada no contrato.
Não é verificado no subcontrato de financiamento que o dicionário permite apenas inserções, e não atualizações de valores existentes ou remoções (não é difícil adicionar uma verificação explícita, no entanto).
A transação de gasto deve pagar uma taxa, caso contrário, é improvável que seja incluída em um bloco. Assim, o contrato de financiamento verifica se a transação de gasto tem duas saídas (uma para si mesma, outra para pagar a taxa), a taxa não deve ser superior a um certo limite (apenas um nanoErg em nosso exemplo), e a proposição de proteção deve ser tal que apenas um minerador possa gastar a saída (usamos apenas uma variável "feeProp" do ambiente de compilação em nosso exemplo sem fornecer detalhes). Essa "feeProp" corresponde a um padrão, embora não exigido pelo protocolo.
O código abaixo impõe as condições descritas acima. Observe que a
variável de ambiente "nextStageScriptHash" contém o hash do script da etapa de emissão 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
A Etapa de Emissão
Esta etapa tem apenas uma transação de gasto para chegar à próxima etapa (a etapa de retirada). As transações de gasto fazem as seguintes modificações. Primeiro, ela altera a lista de operações permitidas no dicionário de "apenas inserções" para "apenas remoções", já que a próxima etapa (retirada) lida apenas com a remoção de entradas do dicionário.
Em segundo lugar, o contrato verifica se a quantidade adequada de tokens ICO foi emitida. No Ergo, é permitido emitir um novo tipo de token por transação, e o identificador do token deve ser igual ao identificador (único) da primeira box de entrada. O subcontrato de emissão verifica se um novo token foi emitido, e a quantidade dele é igual à quantidade de nanoErgs coletados pela ICO até agora.
Em terceiro lugar, o contrato verifica se uma transação de gasto está realmente recriando a box com o script de proteção correspondente à próxima etapa, a etapa de retirada.
Finalmente, o projeto deve retirar os Ergs coletados, e, claro, cada transação de gasto deve pagar uma taxa. Assim, o subcontrato verifica se a transação de gasto tem de fato 3 saídas (uma para cada box de tokens do projeto, a box de retirada de Ergs e a box de taxa), e que a primeira saída e saída estão carregando os tokens emitidos. Como não especificamos os detalhes da retirada de dinheiro do projeto, exigimos uma assinatura do projeto na transação 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
A Etapa de Retirada
Nesta etapa, os investidores são autorizados a retirar tokens do projeto protegidos por um script de guarda pré-definido (cujo hash é armazenado no dicionário). Digamos que a retirada é feita em lotes de tamanho N. Uma transação de retirada, portanto, tem N + 2 saídas, onde a primeira saída carrega o subcontrato de retirada e tokens de saldo, a última saída paga a taxa e as N saídas restantes têm scripts de proteção e valores de tokens de acordo com o dicionário. O contrato requer duas provas para os elementos do dicionário: uma provando que os valores a serem retirados estão de fato no dicionário, e a segunda provando que o dicionário resultante não possui os valores retirados. O subcontrato está abaixo.
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
Possíveis Melhorias
Observe que existem muitas nuances que nosso contrato de exemplo está ignorando. Por exemplo, qualquer um que escute a blockchain está autorizado a executar o contrato e construir transações de gasto adequadas durante as etapas de financiamento e retirada. No mundo real, uma assinatura adicional do projeto ou de um árbitro confiável pode ser usada.
Além disso, não há caso de autodestruição considerado no contrato de retirada, então ele viverá até ser destruído por mineradores via mecanismo de aluguel de armazenamento, potencialmente por décadas ou até séculos. Para a etapa de financiamento, seria razoável ter uma entrada adicional do projeto com o valor igual ao valor da saída de taxa. E assim por diante.
Share post
13 de agosto de 2025
12 de agosto de 2025
9 de julho de 2025
12 de maio de 2025

7 de abril de 2022

8 de março de 2022


















