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


    Всем привет!


    В первой части мы подробно рассмотрели как создавать и работать с 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
    Анонс грантовой программы

    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое