Как стать автором
Обновить

Учимся писать Waves смарт-контракты на RIDE и RIDE4DAPPS. Часть 2 (DAO — Decentralized Autonomous Organization)

Время на прочтение7 мин
Количество просмотров1.9K


Всем привет!


В первой части мы подробно рассмотрели как создавать и работать с dApp (децентрализованным приложением) в Waves RIDE IDE.


Давайте сейчас немного потестируем разобраный пример.


Этап 3. Тестирование dApp аккаунта



Какие проблемы сразу бросаются на гласа с Alice dApp Account?
Во-первых:
Boob и Cooper могут случайно отправить на адрес dApp средства с помощью обычной transfer транзакции и, таким образом, не смогут получить к ним доступ обратно.

Во-вторых:
Мы никак не ограничиваем Alice в выводе средств без согласования с Boob или(и) Cooper. Так как, обратите внимание на verify, все транзакции от Alice будут исполняться.

В-третьих:
Кто угодно, может производить любые операции с аккаунта Alice просто подставляя ее publicKey в транзакцию:
const unsignedTransferTx = transfer({
amount: 1,
recipient: '3P6fVra21KmTfWHBdib45iYV6aFduh4WwC2',
//senderPublicKey is required if you omit seed
senderPublicKey: '6nR7CXVV7Zmt9ew11BsNzSvVmuyM5PF6VPbWHW9BHgPq' 
})

К сожалению, смарт-контракты Waves пока не дают возможности блокировать входящие транзакции на аккаунт, поэтому Boob и Cooper должны сами контролировать свои исходящие транзакции.


Давайте исправим 2-е и 3-е, запретив Alice все транзакции, кроме SetScriptTransaction, запретив остальным, указав ее PublicKey в @Verifier. То есть, разрешим только Алисе, как разработчику dApp на некоторое время только обновлять/исправлять смарт-контракт.


Да, Алиса всегда может обновить скрипт так, чтобы получить больше прав и распоряжаться средствами "пользователей", но это сможет делать только она и все пользователи увидят момент несанкционированного изменения контракта и будут иметь возможность принять меры. Но до тех пор, пока другие транзакции кроме invokeScript не заблокированы, клиентам нужно доверять Alice.

Деплоим исправленный скрипт:


@Verifier(tx)
func verify() = {
    match tx {
        case d: SetScriptTransaction =>
            sigVerify(tx.bodyBytes, tx.proofs[0], base58'x51ySWMyhE8G2AqJqmDpe3qoQM2aBwmieiJzZLK33JW')
        case _ => true
}

Пробуем вывести монеты с dApp Alice и ее подписью. Получаем ошибку:



Пробуем вывести через withdraw:


broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"withdraw",args:[{type:"integer", value: 1000000}]}, payment: []}))

Скрипт работает и со 2-м пунктом мы разобрались!


Этап 4. Создаем DAO с голосованием


К сожалению, в языке RIDE пока не предусмотрены возможности работы с коллекциями (словари-словарей, итераторы, редьюсеры и проч). Однако, для любых операций с плоскими коллекциями key-value мы можем спроектировать систему работы со строками, соотвественно с ключами и их расшифровкой.
Строки очень просто конкатенировать, строки можно разделять по индексам.


Мы имеем все необходимое для написания сложной логики DAO dApp!


Data Transactions

Data Transactions:
“The maximum size for a key is 100 characters, and a key can contain arbitrary Unicode code points including spaces and other non-printable symbols. String values have a limit of 32,768 bytes and the maximum number of possible entries in data transaction is 100. Overall, the maximum size of a data transaction is around 140kb — for reference, almost exactly the length of Shakespeare’s play ‘Romeo and Juliet’.”


Создаем DAO со следующими условиями:
Для того, чтобы стартапу получить финансирование, вызвав getFunds() необходима поддержка минимум 2-х участников — инвесторов DAO. Вывести можно будет ровно столько, сколько в сумме указали на голосовании владельцы DAO.


Давайте сделаем 3 типа ключей и добавим логику по работе с балансами в 2-х новых функциях vote и getFunds:
xx...xx_ia = инвесторы, доступный баланс (vote, deposit, withdrawal)
xx…xx_sv = стартапы, количество голосов (vote, getFunds)
xx…xx_sf = стартапы, количество голосов (vote, getFunds)
xx…xx = публичный адрес (35 символов)

Заметьте в Vote нам понадобилось обновлять сразу несколько полей:


WriteSet([DataEntry(key1, value1), DataEntry(key2, value2)]),

WriteSet позволяет нам делать сразу несколько записей в рамках одной invokeScript транзакции.


Вот так это выглядит в key-value хранилище DAO dApp, после того как Bob и Cooper пополнили ia-депозиты:



Функция депозита у нас слегка изменилась:



Сейчас наступает самый важный момент в деятельности DAO — голосование за проекты для финансирования.


Bob голосует за проект Neli на 500000 wavelets:


broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"vote",args:[{type:"integer", value: 500000}, {type:"string", value: "3MrXEKJr9nDLNyVZ1d12Mq4jjeUYwxNjMsH"}]}, payment: []}))

Код функции Vote:


@Callable(i)
func vote(amount: Int, address: String) = {
        let currentKey = toBase58String(i.caller.bytes)
        let xxxInvestorBalance = currentKey + "_" + "ib"
        let xxxStartupFund = address + "_" + "sf"
        let xxxStartupVotes = address + "_" + "sv"
        let flagKey = address + "_" + currentKey
        let flag = match getInteger(this, flagKey) {
            case a:Int => a
            case _ => 0
        }
        let currentAmount = match getInteger(this, xxxInvestorBalance) {
            case a:Int => a
            case _ => 0
        }
        let currentVotes = match getInteger(this, xxxStartupVotes) {
            case a:Int => a
            case _ => 0
        }
        let currentFund = match getInteger(this, xxxStartupFund) {
            case a:Int => a
            case _ => 0
        }
    if (amount <= 0)
            then throw("Can't withdraw negative amount")
    else if (amount > currentAmount)
            then throw("Not enough balance!")
    else if (flag > 0)
            then throw("Only one vote per project is possible!")
    else 
            WriteSet([
                        DataEntry(xxxInvestorBalance, currentAmount - amount),
                        DataEntry(xxxStartupVotes, currentVotes + 1),
                        DataEntry(flagKey, 1),
                        DataEntry(xxxStartupFund, currentFund + amount)
            ])
    }

В хранилище данных мы видим все необходимые записи для адреса Neli:



Купер также проголосовал за проект Neli.



Давайте взглянем на код функции getFunds. Neli должна собрать минимум 2 голоса, чтобы иметь возможность вывести средства из DAO.



Neli собирается вывести половину доверенной ей суммы:


broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"getFunds",args:[{type:"integer", value: 500000}]}, payment: []}))


Ей это удается, то есть DAO работает!


Мы рассмотрели процесс создания DAO на языке RIDE4DAPPS.
В следующих частях мы подробнее займемся рефакторингом кода и тестированием кейсов.


Полная версия кода в Waves RIDE IDE:


# In this example multiple accounts can deposit their funds to DAO and safely take them back, no one can interfere with this.
# DAO participants can also vote for particular addresses and let them withdraw invested funds then quorum has reached.
# An inner state is maintained as mapping `address=>waves`.
# https://medium.com/waves-lab/waves-announces-funding-for-ride-for-dapps-developers-f724095fdbe1

# You can try this contract by following commands in the IDE (ide.wavesplatform.com)
# Run commands as listed below
# From account #0:
#      deploy()
# From account #1: deposit funds
#      broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"deposit",args:[]}, payment: [{amount: 100000000, asset:null }]}))
# From account #2: deposit funds
#      broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"deposit",args:[]}, payment: [{amount: 100000000, asset:null }]}))
# From account #1: vote for startup
#      broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"vote",args:[{type:"integer", value: 500000}, {type:"string", value: "3MrXEKJr9nDLNyVZ1d12Mq4jjeUYwxNjMsH"}]}, payment: []}))
# From account #2: vote for startup
#      broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"vote",args:[{type:"integer", value: 500000}, {type:"string", value: "3MrXEKJr9nDLNyVZ1d12Mq4jjeUYwxNjMsH"}]}, payment: []}))
# From account #3: get invested funds
#      broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"getFunds",args:[{type:"integer", value: 500000}]}, payment: []}))

{-# STDLIB_VERSION 3 #-}
{-# CONTENT_TYPE DAPP #-}
{-# SCRIPT_TYPE ACCOUNT #-}

@Callable(i)
func deposit() = {
   let pmt = extract(i.payment)
   if (isDefined(pmt.assetId)) then throw("can hodl waves only at the moment")
   else {
        let currentKey = toBase58String(i.caller.bytes)
        let xxxInvestorBalance = currentKey + "_" + "ib"
        let currentAmount = match getInteger(this, xxxInvestorBalance) {
            case a:Int => a
            case _ => 0
        }
        let newAmount = currentAmount + pmt.amount
        WriteSet([DataEntry(xxxInvestorBalance, newAmount)])
   }
}
@Callable(i)
func withdraw(amount: Int) = {
        let currentKey = toBase58String(i.caller.bytes)
        let xxxInvestorBalance = currentKey + "_" + "ib"
        let currentAmount = match getInteger(this, xxxInvestorBalance) {
            case a:Int => a
            case _ => 0
        }
        let newAmount = currentAmount - amount
     if (amount < 0)
            then throw("Can't withdraw negative amount")
    else if (newAmount < 0)
            then throw("Not enough balance")
            else ScriptResult(
                    WriteSet([DataEntry(xxxInvestorBalance, newAmount)]),
                    TransferSet([ScriptTransfer(i.caller, amount, unit)])
                )
    }
@Callable(i)
func getFunds(amount: Int) = {
        let quorum = 2
        let currentKey = toBase58String(i.caller.bytes)
        let xxxStartupFund = currentKey + "_" + "sf"
        let xxxStartupVotes = currentKey + "_" + "sv"
        let currentAmount = match getInteger(this, xxxStartupFund) {
            case a:Int => a
            case _ => 0
        }
        let totalVotes = match getInteger(this, xxxStartupVotes) {
            case a:Int => a
            case _ => 0
        }
        let newAmount = currentAmount - amount
    if (amount < 0)
            then throw("Can't withdraw negative amount")
    else if (newAmount < 0)
            then throw("Not enough balance")
    else if (totalVotes < quorum)
            then throw("Not enough votes. At least 2 votes required!")
    else ScriptResult(
                    WriteSet([
                        DataEntry(xxxStartupFund, newAmount)
                        ]),
                    TransferSet([ScriptTransfer(i.caller, amount, unit)])
                )
    }
@Callable(i)
func vote(amount: Int, address: String) = {
        let currentKey = toBase58String(i.caller.bytes)
        let xxxInvestorBalance = currentKey + "_" + "ib"
        let xxxStartupFund = address + "_" + "sf"
        let xxxStartupVotes = address + "_" + "sv"
        let flagKey = address + "_" + currentKey
        let flag = match getInteger(this, flagKey) {
            case a:Int => a
            case _ => 0
        }
        let currentAmount = match getInteger(this, xxxInvestorBalance) {
            case a:Int => a
            case _ => 0
        }
        let currentVotes = match getInteger(this, xxxStartupVotes) {
            case a:Int => a
            case _ => 0
        }
        let currentFund = match getInteger(this, xxxStartupFund) {
            case a:Int => a
            case _ => 0
        }
    if (amount <= 0)
            then throw("Can't withdraw negative amount")
    else if (amount > currentAmount)
            then throw("Not enough balance!")
    else if (flag > 0)
            then throw("Only one vote per project is possible!")
    else 
            WriteSet([
                        DataEntry(xxxInvestorBalance, currentAmount - amount),
                        DataEntry(xxxStartupVotes, currentVotes + 1),
                        DataEntry(flagKey, 1),
                        DataEntry(xxxStartupFund, currentFund + amount)
            ])
    }
@Verifier(tx)
func verify() = {
    match tx {
        case d: SetScriptTransaction =>
            sigVerify(tx.bodyBytes, tx.proofs[0], base58'x51ySWMyhE8G2AqJqmDpe3qoQM2aBwmieiJzZLK33JW')
        case _ => false
    }
}



Первая часть
Код на гитхабе
Waves RIDE IDE
Анонс грантовой программы

Теги:
Хабы:
+5
Комментарии0

Публикации

Изменить настройки темы

Истории

Работа

Scala разработчик
21 вакансия

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн