Learning to write Waves smart contracts on RIDE and RIDE4DAPPS. Part 2 (DAO - Decentralized Autonomous Organization)
Hello!
In the first part, we examined in detail how to create and work with dApp (decentralized application) in the Waves RIDE IDE .
Let’s test a little example now .
Stage 3. Testing dApp account
What problems are immediately apparent with Alice dApp Account?
Firstly:
Boob and Cooper can accidentally send funds to a dApp address using a regular transfer transaction, and thus will not be able to access them back.
Secondly:
We do not limit Alice to withdraw funds without the consent of Boob and / or Cooper. Since, pay attention to verify, all transactions from Alice will be executed.
Thirdly:
Anyone can perform any operations from an Alice account simply by substituting its publicKey in the transaction:
const unsignedTransferTx = transfer({ amount: 1, recipient: '3P6fVra21KmTfWHBdib45iYV6aFduh4WwC2', //senderPublicKey is required if you omit seed senderPublicKey: '6nR7CXVV7Zmt9ew11BsNzSvVmuyM5PF6VPbWHW9BHgPq' })
Unfortunately, Waves smart contracts do not yet allow you to block incoming transactions on your account, so Boob and Cooper must control their outgoing transactions themselves.
Let's fix 2nd and 3rd by disabling Alice for all transactions except SetScriptTransaction , disabling others by specifying its PublicKey in @Verifier. That is, we will only allow Alice, as a dApp developer, to update / correct a smart contract for a while.
Yes, Alice can always update the script so as to get more rights and manage the means of "users", but only she can do this and all users will see the moment of unauthorized changes to the contract and will be able to take action. But as long as transactions other than invokeScript are not blocked, clients need to be trusted by Alice.
Deploy the corrected script:
@Verifier(tx)
func verify() = {
match tx {
case d: SetScriptTransaction =>
sigVerify(tx.bodyBytes, tx.proofs[0], base58'x51ySWMyhE8G2AqJqmDpe3qoQM2aBwmieiJzZLK33JW')
case _ => true
}
We are trying to withdraw coins with dApp Alice and its signature. We get the error:
We try to withdraw through withdraw:
broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"withdraw",args:[{type:"integer", value: 1000000}]}, payment: []}))
The script works and with the 2nd point we figured out!
Stage 4. We create DAO with voting
Unfortunately, the RIDE language has not yet provided for the possibility of working with collections (dictionaries, dictionaries, iterators, reducers, etc.). However, for any operations with flat key-value collections, we can design a system for working with strings, respectively, with keys and their decryption.
Strings are very easy to concatenate; strings can be split by index.
We have everything you need to write complex DAO dApp logic !
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’.”
We create a DAO with the following conditions:
In order for a startup to receive financing by calling getFunds () , support is required for at least 2 participants - DAO investors. It will be possible to withdraw exactly as much as the total amount indicated by the owners of the DAO.
Let's make 3 types of keys and add logic for working with balances in 2 new functions vote and getFunds:
xx ... xx _ia = investors, available balance (vote, deposit, withdrawal)
xx ... xx _sv = startups, number of votes ( vote, getFunds)
xx ... xx _sf = startups, number of votes (vote, getFunds)
xx ... xx = public address (35 characters)
Note in Vote we needed to update several fields at once:
WriteSet([DataEntry(key1, value1), DataEntry(key2, value2)]),
WriteSet allows us to make several records at once within one invokeScript transaction.
This is how it looks in the DAO dApp key-value storage, after Bob and Cooper replenished ia- deposits:
The deposit function has slightly changed:
Now comes the most important moment in the activities of the DAO - voting for projects for financing.
Bob Votes for Neli's 500,000 Wavelets Project:
broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"vote",args:[{type:"integer", value: 500000}, {type:"string", value: "3MrXEKJr9nDLNyVZ1d12Mq4jjeUYwxNjMsH"}]}, payment: []}))
Vote function code:
@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")
elseif (amount > currentAmount)
then throw("Not enough balance!")
elseif (flag > 0)
then throw("Only one vote per project is possible!")
elseWriteSet([
DataEntry(xxxInvestorBalance, currentAmount - amount),
DataEntry(xxxStartupVotes, currentVotes + 1),
DataEntry(flagKey, 1),
DataEntry(xxxStartupFund, currentFund + amount)
])
}
In the data warehouse, we see all the necessary entries for the Neli address:
Cooper also voted for the Neli project.
Let's take a look at the getFunds function code . Neli must collect a minimum of 2 votes in order to be able to withdraw funds from the DAO.
Neli is going to withdraw half of the amount entrusted to her:
broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"getFunds",args:[{type:"integer", value: 500000}]}, payment: []}))
She succeeds, that is, the DAO works!
We reviewed the process of creating a DAO in the language of RIDE4DAPPS .
In the following parts, we will deal in more detail with code refactoring and case testing.
Full version of the code in Waves RIDE IDE:
# Inthis example multiple accounts can deposit their funds to DAO and safely take them back, no one can interfere withthis.
# 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 trythis 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_VERSION3 #-}
{-# CONTENT_TYPEDAPP #-}
{-# SCRIPT_TYPEACCOUNT #-}
@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")
elseif (newAmount < 0)
then throw("Not enough balance")
elseScriptResult(
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")
elseif (newAmount < 0)
then throw("Not enough balance")
elseif (totalVotes < quorum)
then throw("Not enough votes. At least 2 votes required!")
elseScriptResult(
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")
elseif (amount > currentAmount)
then throw("Not enough balance!")
elseif (flag > 0)
then throw("Only one vote per project is possible!")
elseWriteSet([
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
}
}
First part Waves RIDE IDE
github code Grant program announcement