Пример ICO на базе Ergo
10 апреля 2019 г.

В этой статье описывается полнофункциональное ICO (первичное предложение монет), реализованное на ErgoScript. Пример охватывает несколько важных и новых функций платформы Ergo и показывает, как она может поддерживать сложные контракты с минимальным количеством кода.
Часть 1. Предварительные сведения
Важным дизайнерским решением в протоколе криптовалюты является определение того, что именно тратит транзакция. Здесь есть две возможности. Первая — это модель на основе UTXO, как в Bitcoin, где транзакция тратит одноразовые контейнеры активов (называемые 'монетами' или UTXO в Bitcoin) и создает новые. Вторая — это модель на основе аккаунтов, как в Nxt, Ethereum или Waves, где транзакция переводит определенную сумму активов с существующего долгоживущего аккаунта на другой, возможно новый, долгоживущий аккаунт, с возможными побочными эффектами, такими как выполнение контракта в Waves или Ethereum. В этом отношении Ergo похож на Bitcoin, потому что использует подход на основе UTXO, где одноразовые контейнеры, называемые коробками, тратятся. Интересно, что транзакция Ergo также может иметь входные данные, которые не тратятся, а используются для предоставления некоторой информации из текущего набора неиспользованных коробок.
Создать ICO на основе модели UTXO не тривиально, потому что, в отличие от моделей на основе аккаунтов, здесь нет явного постоянного хранилища. Однако Ergo вводит транзакцию расходования в контекст выполнения скрипта.
С этим небольшим изменением становится возможным выразить зависимости между выходами и входами транзакции. В свою очередь, устанавливая зависимости, мы можем выполнять даже произвольно сложные программы, полные Тьюринга, на блокчейне (см. статью "Самовоспроизводящиеся монеты как универсальная машина Тьюринга"). В этой статье мы определим конкретный сценарий многоступенчатого контракта, используя ICO, где у нас есть три этапа (финансирование, выпуск токенов, вывод).
Теперь представьте ICO для тысяч участников. В отличие от Ethereum, Ergo не предоставляет возможности хранить большие наборы данных и переносить их на протяжении выполнения контракта. Скорее, он позволяет хранить только около 40 байт заголовка структуры данных, представленной в виде словаря ключ -> значение, аутентифицированного аналогично дереву Меркла. Чтобы получить доступ к некоторым элементам в словаре или изменить его, транзакция расходования, которая инициирует выполнение защищающего скрипта, должна предоставить доказательства поиска или модификации. Это дает возможность контракту аутентифицировать потенциально огромные наборы данных, не требуя много памяти для хранения состояния контракта. Однако хранение пространства в состоянии (активных контрактах) означало бы более крупные транзакции, но эта проблема легче с точки зрения масштабируемости, а масштабируемость является высшим приоритетом для Ergo.
Часть 2. Контракт ICO
Существует множество возможных сценариев, связанных с первичным предложением монет (ICO). В этой статье мы рассматриваем ICO, которое хочет собрать как минимум определенную сумму средств (в Ergs), чтобы начать проект. Как только порог финансирования превышен и период финансирования заканчивается, проект запускается, и токены ICO выпускаются проектом на основе общего собранного финансирования. На этапе вывода, который продолжается бесконечно, инвесторы выводят токены ICO на основе суммы, которую они инвестировали в течение периода финансирования. Шаги контракта кратко описаны ниже с подробностями, предоставленными далее:
- Во-первых, происходит эпоха финансирования. Она начинается с коробки проекта, аутентифицирующей пустой словарь. Словарь предназначен для хранения пар (инвестор, баланс), где инвестор — это скрипт, защищающий коробку, содержащую выведенные токены. Для баланса мы предполагаем, что 1 токен равен 1 Ergo во время ICO. В течение эпохи финансирования возможно только вложение Ergs в коробку проекта.
Транзакция финансирования тратит коробку проекта и создает новую коробку проекта с обновленной информацией. Для этого транзакция расходования для коробки проекта также имеет другие входы, которые содержат скрипты вывода инвесторов. Скрипты инвесторов и значения входов должны быть добавлены в дерево новой коробки. Может быть много связанных транзакций финансирования. - Во-вторых, период финансирования заканчивается, после чего дерево, содержащее данные инвесторов, становится доступным только для чтения. Аутентифицированное дерево может иметь различные операции модификации, разрешенные индивидуально: вставки, удаления, обновления, или все операции могут быть запрещены (так что дерево может находиться в режиме только для чтения). Также эта транзакция создает токены проекта ICO, которые будут выведены на следующем этапе. Проект может вывести Ergs на этом этапе.
- В-третьих, инвесторы выводят свои токены ICO. Для этого транзакция расходования создает выходы с условиями защиты и значениями токенов, взятыми из дерева. Выведенные пары также очищаются из дерева. Может быть много связанных транзакций расходования.
Эти три этапа должны быть связаны друг с другом в логическом порядке. Последовательность коробок используется для достижения этих целей.
Часть 3. Подробности контракта ICO
Теперь пришло время предоставить детали и код ErgoScript этапов контракта ICO.
Этап финансирования
На этапе финансирования, который приходит первым, мы предполагаем, что изначально проект создает коробку, подтверждающую пустой словарь (хранящийся в регистре R5) с некоторым защищающим скриптом, описанным ниже. Этот этап длится как минимум до высоты 2000. Более конкретно, первая транзакция с высотой 2000 или более должна изменить скрипт выходной коробки, как описано в следующем разделе (транзакции с более низкими высотами должны выводить коробку с тем же скриптом).
Коробка проекта проверяет, что она всегда является первым входом и выходом транзакции. Другие входы считаются входами инвесторов. Вход инвестора содержит хэш скрипта в регистре R4. Этот хэш представляет собой скрипт вывода, который будет использоваться позже на этапе вывода. Хэши, а также денежные значения всех инвестиционных входов должны быть добавлены в словарь.
Транзакция расходования предоставляет доказательство того, что данные инвестора действительно добавлены в словарь, и это доказательство проверяется в контракте.
В подконтракте финансирования не проверяется, что словарь позволяет только вставки, а не обновления существующих значений или удаления (хотя несложно добавить явную проверку).
Транзакция расходования должна уплатить комиссию, в противном случае маловероятно, что она будет включена в блок. Таким образом, контракт финансирования проверяет, что транзакция расходования имеет два выхода (один для себя, другой для уплаты комиссии), комиссия не должна превышать определенный лимит (в нашем примере всего один наноErg), и защищающее предложение должно быть таким, чтобы только майнер мог потратить выход (в нашем примере мы используем просто переменную "feeProp" из среды компиляции, не предоставляя никаких деталей). Этот "feeProp" соответствует стандарту, хотя и не требуется протоколом.
Код ниже обеспечивает выполнение описанных выше условий. Обратите внимание, что переменная окружения
"nextStageScriptHash" содержит хэш сериализованного скрипта этапа выпуска.
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
Этап выпуска
На этом этапе есть только одна транзакция расходования, чтобы перейти к следующему этапу (этапу вывода). Транзакция расходования вносит следующие изменения. Во-первых, она изменяет список разрешенных операций в словаре с "только вставки" на "только удаления", так как следующий этап (вывод) только занимается удалением записей из словаря.
Во-вторых, контракт проверяет, что правильное количество токенов ICO выпущено. В Ergo разрешено выпускать один новый вид токена за транзакцию, и идентификатор токена должен быть равен (уникальному) идентификатору первой входной коробки. Подконтракт выпуска проверяет, что новый токен был выпущен, и его количество равно количеству наноErgs, собранных ICO на данный момент.
В-третьих, контракт проверяет, что транзакция расходования действительно воссоздает коробку с защищающим скриптом, соответствующим следующему этапу, этапу вывода.
Наконец, проект должен вывести собранные Ergs, и, конечно, каждая транзакция расходования должна уплатить комиссию. Таким образом, подконтракт проверяет, что транзакция расходования действительно имеет 3 выхода (по одному для коробки токенов проекта, коробки вывода Ergs и коробки комиссии), и что первый выход и выход несут выпущенные токены. Поскольку мы не указываем детали вывода средств проекта, мы требуем подпись проекта на транзакции расходования.
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
Этап вывода
На этом этапе инвесторы могут выводить токены проекта, защищенные предопределенным защищающим скриптом (хэш которого хранится в словаре). Предположим, что вывод осуществляется партиями размером N. Транзакция вывода, таким образом, имеет N + 2 выхода, где первый выход переносит подконтракт вывода и баланс токенов, последний выход оплачивает комиссию, а оставшиеся N выходов имеют защищающие скрипты и значения токенов в соответствии со словарем. Контракт требует два доказательства для элементов словаря: одно, подтверждающее, что значения, подлежащие выводу, действительно находятся в словаре, и второе, подтверждающее, что результирующий словарь не содержит выведенных значений. Подконтракт приведен ниже.
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
Возможные улучшения
Обратите внимание, что в нашем примере контракта игнорируется множество нюансов. Например, любой, кто слушает блокчейн, может выполнять контракт и конструировать правильные транзакции расходования в течение этапов финансирования и вывода. В реальном мире может использоваться дополнительная подпись от проекта или доверенного арбитра.
Кроме того, в контракте на вывод не рассматривается случай самоуничтожения, поэтому он будет существовать до тех пор, пока не будет уничтожен майнерами через механизм аренды хранилища, потенциально на десятилетия или даже века. Для этапа финансирования было бы разумно иметь дополнительный вход от проекта со значением, равным значению выходной комиссии. И так далее.
Share post
13 августа 2025 г.
9 июля 2025 г.
12 мая 2025 г.






