Contoh ICO Di Atas Ergo
10 April 2019

Artikel ini menjelaskan ICO (Initial Coin Offering) yang memiliki fitur lengkap yang diimplementasikan dalam ErgoScript. Contoh ini mencakup beberapa fitur penting dan baru dari Platform Ergo dan menunjukkan bagaimana ia dapat mendukung kontrak kompleks dengan jumlah kode yang sangat sedikit.
Bagian 1. Prakondisi
Keputusan desain penting dalam protokol cryptocurrency adalah menentukan apa yang sebenarnya dibelanjakan oleh transaksi pengeluaran. Ada dua kemungkinan di sini. Yang pertama adalah model berbasis UTXO, seperti di Bitcoin, di mana transaksi membelanjakan wadah aset sekali pakai (disebut sebagai 'koin' atau UTXO di Bitcoin) dan menciptakan yang baru. Yang lainnya adalah model berbasis akun, seperti di Nxt, Ethereum, atau Waves, di mana transaksi mentransfer sejumlah aset dari akun yang ada dan bertahan lama ke akun lain yang mungkin baru, dengan kemungkinan efek samping di sepanjang jalan, seperti eksekusi kontrak di Waves atau Ethereum. Dalam hal ini, Ergo mirip dengan Bitcoin, karena menggunakan pendekatan berbasis UTXO, di mana wadah sekali pakai yang disebut kotak sedang dibelanjakan. Menariknya, transaksi Ergo juga dapat memiliki data-input yang tidak dibelanjakan, tetapi digunakan untuk memberikan beberapa informasi dari set kotak yang tidak dibelanjakan saat ini.
Tidaklah sepele untuk membuat ICO di atas model berbasis UTXO, karena, berbeda dengan model berbasis akun, tidak ada penyimpanan persisten eksplisit di sini. Namun, Ergo membawa transaksi pengeluaran ke dalam konteks eksekusi skrip.
Dengan perubahan kecil ini, menjadi mungkin untuk mengekspresikan ketergantungan antara keluaran dan masukan transaksi. Dengan mengatur ketergantungan, kita dapat mengeksekusi bahkan program Turing-complete yang sangat kompleks di atas blockchain (lihat "Self-reproducing Coins as Universal Turing Machine" paper). Dalam artikel ini, kita akan mendefinisikan skenario konkret dari kontrak multi-tahap menggunakan ICO, di mana kita memiliki tiga tahap (pendanaan, penerbitan token, penarikan).
Sekarang bayangkan sebuah ICO untuk ribuan peserta. Tidak seperti Ethereum, Ergo tidak menyediakan kemungkinan untuk menyimpan set data besar dan membawanya sepanjang eksekusi kontrak. Sebaliknya, ia hanya memungkinkan untuk menyimpan sekitar 40-byte header dari struktur data, yang direpresentasikan sebagai kamus kunci -> nilai, yang diautentikasi mirip dengan pohon Merkle. Untuk mengakses beberapa elemen dalam kamus, atau untuk memodifikasinya, transaksi pengeluaran yang memicu eksekusi skrip perlindungan harus memberikan bukti pencarian atau modifikasi. Ini memberikan kemungkinan bagi kontrak untuk mengautentikasi dataset yang berpotensi besar tanpa memerlukan banyak memori untuk menyimpan status kontrak. Namun, menyimpan ruang dalam status (kontrak aktif) akan berarti transaksi yang lebih besar, tetapi masalah ini lebih mudah dari sudut pandang skalabilitas, dan skalabilitas adalah prioritas utama bagi Ergo.
Bagian 2. Kontrak ICO
Ada banyak kemungkinan skenario yang terkait dengan Initial Coin Offering (ICO). Dalam artikel ini, kami mempertimbangkan ICO yang ingin mengumpulkan setidaknya sejumlah dana tertentu (dalam Ergs) untuk memulai proyek. Setelah ambang pendanaan terlampaui dan periode pendanaan berakhir, proyek dimulai dan token ICO diterbitkan oleh proyek berdasarkan total pendanaan yang terkumpul. Dalam fase penarikan, yang berlangsung selamanya, para investor menarik token ICO berdasarkan jumlah yang mereka investasikan selama periode pendanaan. Langkah-langkah kontrak dijelaskan secara singkat di bawah ini dengan rincian yang diberikan lebih lanjut:
- Pertama, epoch pendanaan berlangsung. Ini dimulai dengan kotak proyek yang mengautentikasi kamus kosong. Kamus ini dimaksudkan untuk menyimpan pasangan (investor, saldo), di mana investor adalah skrip yang melindungi kotak yang berisi token yang ditarik. Untuk saldo, kami mengasumsikan bahwa 1 token sama dengan 1 Ergo selama ICO. Selama epoch pendanaan, hanya mungkin untuk memasukkan Ergs ke dalam kotak proyek.
Transaksi pendanaan membelanjakan kotak proyek dan menciptakan kotak proyek baru dengan informasi yang diperbarui. Untuk itu, transaksi pengeluaran untuk kotak proyek juga memiliki masukan lain yang menyimpan skrip penarikan investor. Skrip investor dan nilai masukan harus ditambahkan ke pohon kotak baru. Mungkin ada banyak transaksi pendanaan yang terhubung. - Kedua, periode pendanaan selesai, setelah itu pohon yang menyimpan data investor menjadi hanya-baca. Pohon yang diautentikasi dapat memiliki berbagai operasi modifikasi yang diizinkan secara individual: penyisipan, penghapusan, pembaruan, atau semua operasi dapat dilarang (sehingga pohon dapat berada dalam mode hanya-baca). Selain itu, transaksi ini menciptakan token proyek ICO yang akan ditarik di tahap berikutnya. Proyek dapat menarik Ergs pada tahap ini.
- Ketiga, investor menarik token ICO mereka. Untuk itu, transaksi pengeluaran menciptakan keluaran dengan kondisi perlindungan dan nilai token yang diambil dari pohon. Pasangan yang ditarik juga dibersihkan dari pohon. Mungkin ada banyak transaksi pengeluaran yang terhubung.
Ketiga tahap ini harus dihubungkan bersama dalam urutan logis. Sebuah urutan kotak digunakan untuk mencapai tujuan ini.
Bagian 3. Rincian Kontrak ICO
Sekarang saatnya untuk memberikan rincian dan kode ErgoScript dari tahap kontrak ICO.
Tahap Pendanaan
Dalam tahap pendanaan, yang datang pertama, kami mengasumsikan bahwa awalnya proyek membuat kotak yang berkomitmen pada kamus kosong (disimpan di register R5) dengan beberapa skrip perlindungan yang dijelaskan di bawah ini. Tahap ini berlangsung setidaknya hingga tinggi 2.000. Lebih konkret, transaksi pertama dengan tinggi 2.000 atau lebih harus mengubah skrip kotak keluaran seperti yang dijelaskan di bagian berikutnya (transaksi pada tinggi yang lebih rendah harus mengeluarkan kotak dengan skrip yang sama).
Kotak proyek memeriksa bahwa ia selalu menjadi masukan dan keluaran pertama dari sebuah transaksi. Masukan lainnya dianggap sebagai masukan investor. Masukan investor berisi hash dari skrip di register R4. Hash ini mewakili skrip penarikan yang akan digunakan nanti di fase penarikan. Hash serta nilai moneter dari semua masukan investasi harus ditambahkan ke kamus.
Transaksi pengeluaran memberikan bukti bahwa data investor memang ditambahkan ke kamus, dan bukti tersebut diperiksa dalam kontrak.
Tidak diperiksa dalam sub-kontrak pendanaan bahwa kamus hanya mengizinkan penyisipan, dan tidak memperbarui nilai yang ada atau penghapusan (tidak sulit untuk menambahkan pemeriksaan eksplisit meskipun).
Transaksi pengeluaran harus membayar biaya, jika tidak, tidak mungkin bahwa itu akan dimasukkan dalam sebuah blok. Oleh karena itu, kontrak pendanaan memeriksa bahwa transaksi pengeluaran memiliki dua keluaran (satu untuk dirinya sendiri, satu lagi untuk membayar biaya), biaya tidak boleh lebih dari batas tertentu (hanya satu nanoErg dalam contoh kami), dan proposisi perlindungan harus sedemikian rupa sehingga hanya penambang yang dapat membelanjakan keluaran (kami hanya menggunakan variabel "feeProp" dari lingkungan kompilasi dalam contoh kami tanpa memberikan rincian lebih lanjut). "feeProp" ini sesuai dengan standar, meskipun tidak diwajibkan oleh protokol.
Kode di bawah ini menegakkan kondisi yang dijelaskan di atas. Harap dicatat bahwa variabel lingkungan "nextStageScriptHash" berisi hash dari skrip terserialisasi tahap penerbitan.
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
Tahap Penerbitan
Tahap ini hanya memiliki satu transaksi pengeluaran untuk mencapai tahap berikutnya (tahap penarikan). Transaksi pengeluaran membuat modifikasi berikut. Pertama, ia mengubah daftar operasi yang diizinkan pada kamus dari "hanya penyisipan" menjadi "hanya penghapusan", karena tahap berikutnya (penarikan) hanya berurusan dengan menghapus entri dari kamus.
Kedua, kontrak memeriksa bahwa jumlah token ICO yang tepat diterbitkan. Di Ergo, diizinkan untuk menerbitkan satu jenis token baru per transaksi, dan pengidentifikasi token harus sama dengan pengidentifikasi (unik) dari kotak masukan pertama. Sub-kontrak penerbitan memeriksa bahwa token baru telah diterbitkan, dan jumlahnya sama dengan jumlah nanoErg yang dikumpulkan oleh ICO hingga saat ini.
Ketiga, kontrak memeriksa bahwa transaksi pengeluaran memang menciptakan kembali kotak dengan skrip perlindungan yang sesuai dengan tahap berikutnya, tahap penarikan.
Akhirnya, proyek harus menarik Ergs yang terkumpul, dan tentu saja, setiap transaksi pengeluaran harus membayar biaya. Oleh karena itu, sub-kontrak memeriksa bahwa transaksi pengeluaran memang memiliki 3 keluaran (satu untuk kotak token proyek, kotak penarikan Ergs, dan kotak biaya), dan bahwa keluaran pertama dan keluaran membawa token yang diterbitkan. Karena kami tidak menentukan rincian penarikan uang proyek, kami memerlukan tanda tangan proyek pada transaksi pengeluaran.
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
Tahap Penarikan
Pada tahap ini, investor diizinkan untuk menarik token proyek yang dilindungi oleh skrip perlindungan yang telah ditentukan sebelumnya (yang hash-nya disimpan dalam kamus). Katakanlah penarikan dilakukan dalam batch ukuran N. Transaksi penarikan, dengan demikian, memiliki N + 2 keluaran, di mana keluaran pertama membawa sub-kontrak penarikan dan saldo token, keluaran terakhir membayar biaya dan N keluaran yang tersisa memiliki skrip perlindungan dan nilai token sesuai dengan kamus. Kontrak memerlukan dua bukti untuk elemen kamus: satu membuktikan bahwa nilai yang akan ditarik memang ada dalam kamus, dan yang kedua membuktikan bahwa kamus yang dihasilkan tidak memiliki nilai yang ditarik. Sub-kontrak ada di bawah ini.
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
Kemungkinan Peningkatan
Harap dicatat bahwa ada banyak nuansa yang diabaikan oleh kontrak contoh kami. Misalnya, siapa pun yang mendengarkan blockchain diizinkan untuk mengeksekusi kontrak dan membangun transaksi pengeluaran yang tepat selama tahap pendanaan dan penarikan. Dalam dunia nyata, tanda tangan tambahan dari proyek atau arbiter tepercaya dapat digunakan.
Selain itu, tidak ada kasus penghancuran diri yang dipertimbangkan dalam kontrak penarikan, jadi kontrak ini akan hidup sampai dihancurkan oleh penambang melalui mekanisme sewa penyimpanan, berpotensi selama beberapa dekade atau bahkan abad. Untuk tahap pendanaan, akan masuk akal untuk memiliki masukan tambahan dari proyek dengan nilai yang sama dengan nilai keluaran biaya. Dan seterusnya.
Share post
13 Agustus 2025
9 Juli 2025






