contract/dao: Initial implementation.

This commit is contained in:
parazyd
2022-11-29 16:01:31 +01:00
parent 6bc24e54ef
commit 0b6f6f847d
16 changed files with 1282 additions and 35 deletions

43
Cargo.lock generated
View File

@@ -1147,39 +1147,6 @@ dependencies = [
"url",
]
[[package]]
name = "dao-example"
version = "0.3.0"
dependencies = [
"async-channel",
"async-executor",
"async-std",
"async-trait",
"bs58",
"chacha20poly1305",
"darkfi",
"darkfi-sdk",
"darkfi-serial",
"easy-parallel",
"env_logger",
"futures",
"fxhash",
"group",
"halo2_gadgets",
"halo2_proofs",
"incrementalmerkletree",
"lazy_static",
"log",
"num_cpus",
"pasta_curves",
"rand",
"serde_json",
"simplelog",
"smol",
"thiserror",
"url",
]
[[package]]
name = "daod"
version = "0.3.0"
@@ -1279,6 +1246,16 @@ dependencies = [
"wasmer-middlewares",
]
[[package]]
name = "darkfi-dao-contract"
version = "0.3.0"
dependencies = [
"darkfi-money-contract",
"darkfi-sdk",
"darkfi-serial",
"getrandom 0.2.8",
]
[[package]]
name = "darkfi-derive"
version = "0.3.0"

View File

@@ -45,9 +45,9 @@ members = [
"src/serial/derive-internal",
"src/contract/money",
"src/contract/dao",
"example/dchat",
"example/dao",
]
[dependencies]

2
src/contract/dao/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
dao_contract.wasm
proof/*.zk.bin

View File

@@ -0,0 +1,24 @@
[package]
name = "darkfi-dao-contract"
version = "0.3.0"
authors = ["Dyne.org foundation <foundation@dyne.org>"]
license = "AGPL-3.0-only"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
darkfi-sdk = { path = "../../sdk" }
darkfi-serial = { path = "../../serial", features = ["derive", "crypto"] }
darkfi-money-contract = { path = "../money", features = ["no-entrypoint"] }
# We need to disable random using "custom" which makes the crate a noop
# so the wasm32-unknown-unknown target is enabled.
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2.8", features = ["custom"] }
[features]
default = []
no-entrypoint = []
client = []

37
src/contract/dao/Makefile Normal file
View File

@@ -0,0 +1,37 @@
# Cargo binary
CARGO ?= cargo
# Zkas binary
ZKAS ?= ../../../zkas
# zkas circuit source files
ZKAS_SRC = $(shell find proof -type f -name '*.zk')
# wasm source files
WASM_SRC = \
$(shell find src -type f) \
$(shell find ../../sdk -type f) \
$(shell find ../../serial -type f)
# zkas circuit bin files
ZKAS_BIN = $(ZKAS_SRC:=.bin)
# Contract WASM binaries
WASM_BIN = dao_contract.wasm
all: $(WASM_BIN)
$(ZKAS_BIN): $(ZKAS_SRC)
$(ZKAS) $(basename $@) -o $@
dao_contract.wasm: $(ZKAS_BIN) $(WASM_SRC)
$(CARGO) build --release --package darkfi-dao-contract --target wasm32-unknown-unknown
cp -f ../../../target/wasm32-unknown-unknown/release/darkfi_dao_contract.wasm $@
test: all
$(CARGO) test --release --features=no-entrypoint,client --package darkfi-dao-contract
clean:
rm -f $(ZKAS_BIN) $(WASM_BIN)
.PHONY: all test clean

View File

@@ -0,0 +1,150 @@
constant "DaoExec" {
EcFixedPointShort VALUE_COMMIT_VALUE,
EcFixedPoint VALUE_COMMIT_RANDOM,
}
contract "DaoExec" {
# Proposal parameters
Base proposal_dest_x,
Base proposal_dest_y,
Base proposal_amount,
Base proposal_serial,
Base proposal_token_id,
Base proposal_blind,
# DAO parameters
Base dao_proposer_limit,
Base dao_quorum,
Base dao_approval_ratio_quot,
Base dao_approval_ratio_base,
Base gov_token_id,
Base dao_public_x,
Base dao_public_y,
Base dao_bulla_blind,
# Votes
Base yes_votes_value,
Base all_votes_value,
Scalar yes_votes_blind,
Scalar all_votes_blind,
# Outputs + Inputs
Base user_serial,
Base user_coin_blind,
Base dao_serial,
Base dao_coin_blind,
Base input_value,
Scalar input_value_blind,
# Miscellaneous
Base dao_spend_hook,
Base user_spend_hook,
Base user_data,
}
circuit "DaoExec" {
dao_bulla = poseidon_hash(
dao_proposer_limit,
dao_quorum,
dao_approval_ratio_quot,
dao_approval_ratio_base,
gov_token_id,
dao_public_x,
dao_public_y,
dao_bulla_blind,
);
# Proposal bulla being valid means DAO bulla is also valid because
# dao-propose-main.zk already checks that when we first create the
# proposal - so it is redundant here.
proposal_bulla = poseidon_hash(
proposal_dest_x,
proposal_dest_y,
proposal_amount,
proposal_serial,
proposal_token_id,
dao_bulla,
proposal_blind,
# Blind twice to workaround odd-n poseidon bug
proposal_blind,
);
constrain_instance(proposal_bulla);
coin_0 = poseidon_hash(
proposal_dest_x,
proposal_dest_y,
proposal_amount,
proposal_token_id,
proposal_serial,
user_spend_hook,
user_data,
proposal_blind,
);
constrain_instance(coin_0);
change = base_sub(input_value, proposal_amount);
coin_1 = poseidon_hash(
dao_public_x,
dao_public_y,
change,
proposal_token_id,
dao_serial,
dao_spend_hook,
dao_bulla,
dao_coin_blind,
);
constrain_instance(coin_1);
# Create Pedersen commitments for win_votes and total_votes, and
# constrain the commitments' coordinates.
yes_votes_value_c = ec_mul_short(yes_votes_value, VALUE_COMMIT_VALUE);
yes_votes_blind_c = ec_mul(yes_votes_blind, VALUE_COMMIT_RANDOM);
yes_votes_commit = ec_add(yes_votes_value_c, yes_votes_blind_c);
constrain_instance(ec_get_x(yes_votes_commit));
constrain_instance(ec_get_y(yes_votes_commit));
all_votes_value_c = ec_mul_short(all_votes_value, VALUE_COMMIT_VALUE);
all_votes_blind_c = ec_mul(all_votes_blind, VALUE_COMMIT_RANDOM);
all_votes_commit = ec_add(all_votes_value_c, all_votes_blind_c);
constrain_instance(ec_get_x(all_votes_commit));
constrain_instance(ec_get_y(all_votes_commit));
# Create Pedersen commitment for input_value and make public
input_value_v = ec_mul_short(input_value, VALUE_COMMIT_VALUE);
input_value_r = ec_mul(input_value_blind, VALUE_COMMIT_RANDOM);
input_value_commit = ec_add(input_value_v, input_value_r);
constrain_instance(ec_get_x(input_value_commit));
constrain_instance(ec_get_y(input_value_commit));
constrain_instance(dao_spend_hook);
constrain_instance(user_spend_hook);
constrain_instance(user_data);
# Check that dao_quorum is less than or equal to all_votes_value
one = witness_base(1);
all_votes_value_1 = base_add(all_votes_value, one);
less_than_strict(dao_quorum, all_votes_value_1);
# approval_ratio_quot / approval_ratio_base <= yes_votes / all_votes
#
# The above is also equivalent to this:
#
# all_votes * approval_ratio_quot <= yes_votes * approval_ratio_base
lhs = base_mul(all_votes_value, dao_approval_ratio_quot);
rhs = base_mul(yes_votes_value, dao_approval_ratio_base);
rhs_1 = base_add(rhs, one);
less_than_strict(lhs, rhs_1);
# Create coin 0
# Create coin 1
# Check values of coin 0 + coin 1 == input_value
# Check value of coin 0 == proposal_amount
# Check public key matches too
# Create the input value commit
# Create the value commits
# NOTE: There is a vulnerability here where someone can create the exec
# transaction with a bad note so it cannot be decrypted by the receiver
# TODO: Research verifiable encryption inside ZK
}

View File

@@ -0,0 +1,31 @@
constant "DaoMint" {
EcFixedPoint VALUE_COMMIT_RANDOM,
}
contract "DaoMint" {
Base dao_proposer_limit,
Base dao_quorum,
Base dao_approval_ratio_quot,
Base dao_approval_ratio_base,
Base gov_token_id,
Base dao_public_x,
Base dao_public_y,
Base dao_bulla_blind,
}
circuit "DaoMint" {
# This circuit states that the bulla is a hash of 8 values
bulla = poseidon_hash(
dao_proposer_limit,
dao_quorum,
dao_approval_ratio_quot,
dao_approval_ratio_base,
gov_token_id,
dao_public_x,
dao_public_y,
dao_bulla_blind,
);
constrain_instance(bulla);
}

View File

@@ -0,0 +1,61 @@
constant "DaoProposeInput" {
EcFixedPointBase NULLIFIER_K,
EcFixedPoint VALUE_COMMIT_RANDOM,
EcFixedPointShort VALUE_COMMIT_VALUE,
}
contract "DaoProposeInput" {
Base secret,
Base serial,
Base spend_hook,
Base user_data,
Base value,
Base token,
Base coin_blind,
Scalar value_blind,
Base token_blind,
Uint32 leaf_pos,
MerklePath path,
Base signature_secret,
}
circuit "DaoProposeInput" {
nullifier = poseidon_hash(secret, serial);
constrain_instance(nullifier);
# Pedersen commitment for coin's value
vcv = ec_mul_short(value, VALUE_COMMIT_VALUE);
vcr = ec_mul(value_blind, VALUE_COMMIT_RANDOM);
value_commit = ec_add(vcv, vcr);
constrain_instance(ec_get_x(value_commit));
constrain_instance(ec_get_y(value_commit));
# Commitment for coin's token ID
token_commit = poseidon_hash(token, token_blind);
constrain_instance(token_commit);
# Coin hash
pub = ec_mul_base(secret, NULLIFIER_K);
pub_x = ec_get_x(pub);
pub_y = ec_get_y(pub);
C = poseidon_hash(
pub_x,
pub_y,
value,
token,
serial,
spend_hook,
user_data,
coin_blind
);
# Merkle root
root = merkle_root(leaf_pos, path, C);
constrain_instance(root);
# Finally we derive a public key for the signature and constrain
# its coordinates:
signature_public = ec_mul_base(signature_secret, NULLIFIER_K);
constrain_instance(ec_get_x(signature_public));
constrain_instance(ec_get_y(signature_public));
}

View File

@@ -0,0 +1,83 @@
constant "DaoProposeMain" {
EcFixedPointShort VALUE_COMMIT_VALUE,
EcFixedPoint VALUE_COMMIT_RANDOM,
}
contract "DaoProposeMain" {
# Proposers total number of governance tokens
Base total_funds,
Scalar total_funds_blind,
# Check the inputs and this proof are for the same token
Base gov_token_blind,
# Proposal parameters
Base proposal_dest_x,
Base proposal_dest_y,
Base proposal_amount,
Base proposal_serial,
Base proposal_token_id,
Base proposal_blind,
# DAO params
Base dao_proposer_limit,
Base dao_quorum,
Base dao_approval_ratio_quot,
Base dao_approval_ratio_base,
Base gov_token_id,
Base dao_public_x,
Base dao_public_y,
Base dao_bulla_blind,
Uint32 dao_leaf_pos,
MerklePath dao_path,
}
circuit "DaoProposeMain" {
token_commit = poseidon_hash(gov_token_id, gov_token_blind);
constrain_instance(token_commit);
dao_bulla = poseidon_hash(
dao_proposer_limit,
dao_quorum,
dao_approval_ratio_quot,
dao_approval_ratio_base,
gov_token_id,
dao_public_x,
dao_public_y,
dao_bulla_blind,
);
dao_root = merkle_root(dao_leaf_pos, dao_path, dao_bulla);
constrain_instance(dao_root);
# Proves this DAO is valid
proposal_bulla = poseidon_hash(
proposal_dest_x,
proposal_dest_y,
proposal_amount,
proposal_serial,
proposal_token_id,
dao_bulla,
proposal_blind,
proposal_blind,
);
constrain_instance(proposal_bulla);
# Rangeproof check for proposal amount
zero = witness_base(0);
less_than_strict(zero, proposal_amount);
# This is the main check
# We check that dao_proposer_limit <= total_funds
one = witness_base(1);
total_funds_1 = base_add(total_funds, one);
less_than_strict(dao_proposer_limit, total_funds_1);
# Pedersen commitment for coin's value
vcv = ec_mul_short(total_funds, VALUE_COMMIT_VALUE);
vcr = ec_mul(total_funds_blind, VALUE_COMMIT_RANDOM);
total_funds_commit = ec_add(vcv, vcr);
constrain_instance(ec_get_x(total_funds_commit));
constrain_instance(ec_get_y(total_funds_commit));
}

View File

@@ -0,0 +1,56 @@
constant "DaoVoteInput" {
EcFixedPointBase NULLIFIER_K,
EcFixedPoint VALUE_COMMIT_RANDOM,
EcFixedPointShort VALUE_COMMIT_VALUE,
}
contract "DaoVoteInput" {
Base secret,
Base serial,
Base spend_hook,
Base user_data,
Base value,
Base gov_token_id,
Base coin_blind,
Scalar value_blind,
Base gov_token_blind,
Uint32 leaf_pos,
MerklePath path,
Base signature_secret,
}
circuit "DaoVoteInput" {
nullifier = poseidon_hash(secret, serial);
constrain_instance(nullifier);
vcv = ec_mul_short(value, VALUE_COMMIT_VALUE);
vcr = ec_mul(value_blind, VALUE_COMMIT_RANDOM);
value_commit = ec_add(vcv, vcr);
constrain_instance(ec_get_x(value_commit));
constrain_instance(ec_get_y(value_commit));
token_commit = poseidon_hash(gov_token_id, gov_token_blind);
constrain_instance(token_commit);
pub = ec_mul_base(secret, NULLIFIER_K);
pub_x = ec_get_x(pub);
pub_y = ec_get_y(pub);
C = poseidon_hash(
pub_x,
pub_y,
value,
gov_token_id,
serial,
spend_hook,
user_data,
coin_blind,
);
# Merkle root
root = merkle_root(leaf_pos, path, C);
constrain_instance(root);
signature_public = ec_mul_base(signature_secret, NULLIFIER_K);
constrain_instance(ec_get_x(signature_public));
constrain_instance(ec_get_y(signature_public));
}

View File

@@ -0,0 +1,84 @@
constant "DaoVoteMain" {
EcFixedPoint VALUE_COMMIT_RANDOM,
EcFixedPointShort VALUE_COMMIT_VALUE,
}
contract "DaoVoteMain" {
# Proposal parameters
Base proposal_dest_x,
Base proposal_dest_y,
Base proposal_amount,
Base proposal_serial,
Base proposal_token_id,
Base proposal_blind,
# DAO parameters
Base dao_proposer_limit,
Base dao_quorum,
Base dao_approval_ratio_quot,
Base dao_approval_ratio_base,
Base gov_token_id,
Base dao_public_x,
Base dao_public_y,
Base dao_bulla_blind,
# Is the vote yes or no
Base vote_option,
Scalar yes_vote_blind,
# Total amount of capital allocated to vote
Base all_votes_value,
Scalar all_votes_blind,
# Check the inputs and this proof are for the same token
Base gov_token_blind,
}
circuit "DaoVoteMain" {
token_commit = poseidon_hash(gov_token_id, gov_token_blind);
constrain_instance(token_commit);
dao_bulla = poseidon_hash(
dao_proposer_limit,
dao_quorum,
dao_approval_ratio_quot,
dao_approval_ratio_base,
gov_token_id,
dao_public_x,
dao_public_y,
dao_bulla_blind,
);
proposal_bulla = poseidon_hash(
proposal_dest_x,
proposal_dest_y,
proposal_amount,
proposal_serial,
proposal_token_id,
dao_bulla,
proposal_blind,
proposal_blind,
);
constrain_instance(proposal_bulla);
# TODO: We need to check the proposal isn't invalidated
# that is expired or already executed.
# Normally we call this yes vote
# Pedersen commitment for vote option
yes_votes_value = base_mul(vote_option, all_votes_value);
yes_votes_value_c = ec_mul_short(yes_votes_value, VALUE_COMMIT_VALUE);
yes_votes_blind_c = ec_mul(yes_vote_blind, VALUE_COMMIT_RANDOM);
yes_votes_commit = ec_add(yes_votes_value_c, yes_votes_blind_c);
constrain_instance(ec_get_x(yes_votes_commit));
constrain_instance(ec_get_y(yes_votes_commit));
# Pedersen commitment for vote value
all_votes_c = ec_mul_short(all_votes_value, VALUE_COMMIT_VALUE);
all_votes_blind_c = ec_mul(all_votes_blind, VALUE_COMMIT_RANDOM);
all_votes_commit = ec_add(all_votes_c, all_votes_blind_c);
constrain_instance(ec_get_x(all_votes_commit));
constrain_instance(ec_get_y(all_votes_commit));
# Vote option should be 0 or 1
bool_check(vote_option);
}

View File

@@ -0,0 +1,553 @@
/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2022 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use darkfi_sdk::{
crypto::{ContractId, MerkleNode, MerkleTree, PublicKey},
db::{db_contains_key, db_get, db_init, db_lookup, db_set, ZKAS_DB_NAME},
error::{ContractError, ContractResult},
merkle::merkle_add,
msg,
pasta::{
arithmetic::CurveAffine,
group::{Curve, Group},
pallas,
},
tx::ContractCall,
util::set_return_data,
};
use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
use darkfi_money_contract::{
state::MoneyTransferParams, MoneyFunction, COIN_ROOTS_TREE, NULLIFIERS_TREE,
};
use crate::{
state::{
DaoExecParams, DaoExecUpdate, DaoMintParams, DaoMintUpdate, DaoProposeParams,
DaoProposeUpdate, DaoVoteParams, DaoVoteUpdate, ProposalVotes,
},
DaoFunction,
};
darkfi_sdk::define_contract!(
init: init_contract,
exec: process_instruction,
apply: process_update,
metadata: get_metadata
);
// These are the different sled trees that will be created
pub const DAO_BULLA_TREE: &str = "dao_info";
pub const DAO_ROOTS_TREE: &str = "dao_roots";
pub const DAO_PROPOSAL_TREE: &str = "dao_proposals";
pub const DAO_PROPOSAL_ROOTS_TREE: &str = "dao_proposal_roots";
pub const DAO_PROPOSAL_VOTES_TREE: &str = "dao_proposal_votes";
// These are keys inside the some db trees
pub const DAO_MERKLE_TREE: &str = "dao_merkle_tree";
pub const DAO_PROPOSAL_MERKLE_TREE: &str = "dao_proposals_merkle_tree";
// These are zkas circuit namespaces
pub const ZKAS_DAO_EXEC_NS: &str = "DaoExec";
pub const ZKAS_DAO_MINT_NS: &str = "DaoMint";
pub const ZKAS_DAO_VOTE_BURN_NS: &str = "DaoVoteInput";
pub const ZKAS_DAO_VOTE_MAIN_NS: &str = "DaoVoteMain";
pub const ZKAS_DAO_PROPOSE_BURN_NS: &str = "DaoProposeInput";
pub const ZKAS_DAO_PROPOSE_MAIN_NS: &str = "DaoProposeMain";
fn init_contract(cid: ContractId, ix: &[u8]) -> ContractResult {
// The zkas circuits can simply be embedded in the wasm and set up by
// the initialization. Note that the tree should then be called "zkas".
// The lookups can then be done by `contract_id+_zkas+namespace`.
let zkas_db = match db_lookup(cid, ZKAS_DB_NAME) {
Ok(v) => v,
Err(_) => db_init(cid, ZKAS_DB_NAME)?,
};
let dao_exec_bin = include_bytes!("../proof/dao-exec.zk.bin");
let dao_mint_bin = include_bytes!("../proof/dao-mint.zk.bin");
let dao_vote_burn_bin = include_bytes!("../proof/dao-vote-burn.zk.bin");
let dao_vote_main_bin = include_bytes!("../proof/dao-vote-main.zk.bin");
let dao_propose_burn_bin = include_bytes!("../proof/dao-propose-burn.zk.bin");
let dao_propose_main_bin = include_bytes!("../proof/dao-propose-main.zk.bin");
db_set(zkas_db, &serialize(&ZKAS_DAO_EXEC_NS.to_string()), &dao_exec_bin[..])?;
db_set(zkas_db, &serialize(&ZKAS_DAO_MINT_NS.to_string()), &dao_mint_bin[..])?;
db_set(zkas_db, &serialize(&ZKAS_DAO_VOTE_BURN_NS.to_string()), &dao_vote_burn_bin[..])?;
db_set(zkas_db, &serialize(&ZKAS_DAO_VOTE_MAIN_NS.to_string()), &dao_vote_main_bin[..])?;
db_set(zkas_db, &serialize(&ZKAS_DAO_PROPOSE_BURN_NS.to_string()), &dao_propose_burn_bin[..])?;
db_set(zkas_db, &serialize(&ZKAS_DAO_PROPOSE_MAIN_NS.to_string()), &dao_propose_main_bin[..])?;
// Set up a database tree to hold the Merkle tree for DAO bullas
let dao_bulla_db = match db_lookup(cid, DAO_BULLA_TREE) {
Ok(v) => v,
Err(_) => db_init(cid, DAO_BULLA_TREE)?,
};
match db_get(dao_bulla_db, &serialize(&DAO_MERKLE_TREE))? {
Some(bytes) => {
// We found some bytes, try to deserialize into a tree.
// For now, if this doesn't work, we bail.
let _: MerkleTree = deserialize(&bytes)?;
}
None => {
// We didn't find a tree, so just make a new one.
let tree = MerkleTree::new(100);
db_set(dao_bulla_db, &serialize(&DAO_MERKLE_TREE), &serialize(&tree))?;
}
};
// Set up a database tree to hold Merkle roots for the DAO bullas Merkle tree
let _ = match db_lookup(cid, DAO_ROOTS_TREE) {
Ok(v) => v,
Err(_) => db_init(cid, DAO_ROOTS_TREE)?,
};
// Set up a database tree to hold the Merkle tree for proposal bullas
let dao_proposal_db = match db_lookup(cid, DAO_PROPOSAL_TREE) {
Ok(v) => v,
Err(_) => db_init(cid, DAO_PROPOSAL_TREE)?,
};
match db_get(dao_proposal_db, &serialize(&DAO_PROPOSAL_MERKLE_TREE))? {
Some(bytes) => {
// We found some bytes, try to deserialize into a tree.
// For now, if this doesn't work, we bail.
let _: MerkleTree = deserialize(&bytes)?;
}
None => {
// We didn't find a tree, so just make a new one.
let tree = MerkleTree::new(100);
db_set(dao_proposal_db, &serialize(&DAO_PROPOSAL_MERKLE_TREE), &serialize(&tree))?;
}
};
// Set up a database tree to hold Merkle roots for the proposal bullas Merkle tree
let _ = match db_lookup(cid, DAO_PROPOSAL_ROOTS_TREE) {
Ok(v) => v,
Err(_) => db_init(cid, DAO_PROPOSAL_ROOTS_TREE)?,
};
// Set up a database tree to hold proposal votes (k: proposalbulla, v: ProposalVotes)
let _ = match db_lookup(cid, DAO_PROPOSAL_VOTES_TREE) {
Ok(v) => v,
Err(_) => db_init(cid, DAO_PROPOSAL_VOTES_TREE)?,
};
Ok(())
}
fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult {
let (call_idx, call): (u32, Vec<ContractCall>) = deserialize(ix)?;
assert!(call_idx < call.len() as u32);
let self_ = &call[call_idx as usize];
match DaoFunction::try_from(self_.data[0])? {
DaoFunction::Mint => {
let params: DaoMintParams = deserialize(&self_.data[1..])?;
// No checks in Mint, just return the update.
let update = DaoMintUpdate { dao_bulla: params.dao_bulla };
let mut update_data = vec![];
update_data.write_u8(DaoFunction::Mint as u8)?;
update.encode(&mut update_data)?;
set_return_data(&update_data)?;
msg!("[DAO Mint] State update set!");
Ok(())
}
DaoFunction::Propose => {
let params: DaoProposeParams = deserialize(&self_.data[1..])?;
// Check the Merkle roots for the input coins are valid
let money_cid = ContractId::from(pallas::Base::from(u64::MAX - 420));
let coin_roots_db = db_lookup(money_cid, COIN_ROOTS_TREE)?;
for input in &params.inputs {
if !db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? {
msg!("Invalid input Merkle root: {}", input.merkle_root);
return Err(ContractError::Custom(2))
}
}
// Is the DAO bulla generated in the ZK proof valid
let dao_roots_db = db_lookup(cid, DAO_ROOTS_TREE)?;
if !db_contains_key(dao_roots_db, &serialize(&params.dao_merkle_root))? {
msg!("Invalid DAO Merkle root: {}", params.dao_merkle_root);
return Err(ContractError::Custom(3))
}
// TODO: Look at gov tokens avoid using already spent ones
// Need to spend original coin and generate 2 nullifiers?
let update = DaoProposeUpdate { proposal_bulla: params.proposal_bulla };
let mut update_data = vec![];
update_data.write_u8(DaoFunction::Propose as u8)?;
update.encode(&mut update_data)?;
set_return_data(&update_data)?;
msg!("[DAO Propose] State update set!");
Ok(())
}
DaoFunction::Vote => {
let params: DaoVoteParams = deserialize(&self_.data[1..])?;
let money_cid = ContractId::from(pallas::Base::from(u64::MAX - 420));
// Check proposal bulla exists
let proposal_votes_db = db_lookup(cid, DAO_PROPOSAL_VOTES_TREE)?;
let Some(proposal_votes) = db_get(proposal_votes_db, &serialize(&params.proposal_bulla))? else {
msg!("Invalid proposal {:?}", params.proposal_bulla);
return Err(ContractError::Custom(4))
};
let proposal_votes: ProposalVotes = deserialize(&proposal_votes)?;
// Check the Merkle roots and nullifiers for the input coins are valid
let mut vote_nullifiers = vec![];
let mut all_vote_commit = pallas::Point::identity();
let money_roots_db = db_lookup(money_cid, COIN_ROOTS_TREE)?;
let money_nullifier_db = db_lookup(money_cid, NULLIFIERS_TREE)?;
for input in &params.inputs {
if !db_contains_key(money_roots_db, &serialize(&input.merkle_root))? {
msg!("Invalid input Merkle root: {:?}", input.merkle_root);
return Err(ContractError::Custom(5))
}
if db_contains_key(money_nullifier_db, &serialize(&input.nullifier))? {
msg!("Coin is already spent");
return Err(ContractError::Custom(6))
}
if vote_nullifiers.contains(&input.nullifier) ||
proposal_votes.vote_nullifiers.contains(&input.nullifier)
{
msg!("Attempted double vote");
return Err(ContractError::Custom(7))
}
all_vote_commit += input.vote_commit;
vote_nullifiers.push(input.nullifier);
}
let update = DaoVoteUpdate {
proposal_bulla: params.proposal_bulla,
vote_nullifiers,
yes_vote_commit: params.yes_vote_commit,
all_vote_commit,
};
let mut update_data = vec![];
update_data.write_u8(DaoFunction::Vote as u8)?;
update.encode(&mut update_data)?;
set_return_data(&update_data)?;
msg!("[DAO Vote] State update set!");
Ok(())
}
DaoFunction::Exec => {
let params: DaoExecParams = deserialize(&self_.data[1..])?;
// =============================
// Enforce tx has correct format
// =============================
// 1. There should be only two calls
assert!(call.len() == 2);
// 2. func_call_index == 1
assert!(call_idx == 1);
// 3. First item should be a MoneyTransfer call
// FIXME: No hardcode of contract like this
assert!(call[0].contract_id == ContractId::from(pallas::Base::from(u64::MAX - 420)));
assert!(call[0].data[0] == MoneyFunction::Transfer as u8);
// 4. MoneyTransfer has exactly 2 outputs
let mt_params: MoneyTransferParams = deserialize(&call[0].data[1..])?;
assert!(mt_params.outputs.len() == 2);
// ======
// Checks
// ======
// 1. Check both coins in MoneyTransfer are equal to our coin_0, coin_1
assert!(mt_params.outputs[0].coin == params.coin_0);
assert!(mt_params.outputs[1].coin == params.coin_1);
// 2. Sum of MoneyTransfer input value commits == our input value commit
let mut input_valcoms = pallas::Point::identity();
for input in mt_params.inputs {
input_valcoms += input.value_commit;
}
assert!(input_valcoms == params.input_value_commit);
// 3. Get the ProposalVote from DAO state
let proposal_db = db_lookup(cid, DAO_PROPOSAL_TREE)?;
let Some(proposal_votes) = db_get(proposal_db, &serialize(&params.proposal))? else {
msg!("Proposal {:?} not found in db", params.proposal);
return Err(ContractError::Custom(1));
};
let proposal_votes: ProposalVotes = deserialize(&proposal_votes)?;
// 4. Check yes_votes_commit and all_votes_commit are the same as in ProposalVotes
assert!(proposal_votes.yes_votes_commit == params.yes_votes_commit);
assert!(proposal_votes.all_votes_commit == params.all_votes_commit);
let update = DaoExecUpdate { proposal: params.proposal };
let mut update_data = vec![];
update_data.write_u8(DaoFunction::Exec as u8)?;
update.encode(&mut update_data)?;
set_return_data(&update_data)?;
msg!("[DAO Exec] State update set!");
Ok(())
}
}
}
fn process_update(cid: ContractId, ix: &[u8]) -> ContractResult {
match DaoFunction::try_from(ix[0])? {
DaoFunction::Mint => {
let update: DaoMintUpdate = deserialize(&ix[1..])?;
let bulla_db = db_lookup(cid, DAO_BULLA_TREE)?;
let roots_db = db_lookup(cid, DAO_ROOTS_TREE)?;
let node = MerkleNode::from(update.dao_bulla.inner());
merkle_add(bulla_db, roots_db, &serialize(&DAO_MERKLE_TREE), &node)?;
Ok(())
}
DaoFunction::Propose => {
let update: DaoProposeUpdate = deserialize(&ix[1..])?;
let proposal_tree_db = db_lookup(cid, DAO_PROPOSAL_TREE)?;
let proposal_root_db = db_lookup(cid, DAO_PROPOSAL_ROOTS_TREE)?;
let proposal_vote_db = db_lookup(cid, DAO_PROPOSAL_VOTES_TREE)?;
let node = MerkleNode::from(update.proposal_bulla);
merkle_add(
proposal_tree_db,
proposal_root_db,
&serialize(&DAO_PROPOSAL_MERKLE_TREE),
&node,
)?;
let pv = ProposalVotes::default();
db_set(proposal_vote_db, &serialize(&update.proposal_bulla), &serialize(&pv))?;
Ok(())
}
DaoFunction::Vote => {
let mut update: DaoVoteUpdate = deserialize(&ix[1..])?;
let proposal_vote_db = db_lookup(cid, DAO_PROPOSAL_VOTES_TREE)?;
let Some(proposal_votes) = db_get(proposal_vote_db, &serialize(&update.proposal_bulla))? else {
msg!("Proposal {:?} not found in db", update.proposal_bulla);
return Err(ContractError::Custom(1));
};
let mut proposal_votes: ProposalVotes = deserialize(&proposal_votes)?;
proposal_votes.yes_votes_commit += update.yes_vote_commit;
proposal_votes.all_votes_commit += update.all_vote_commit;
proposal_votes.vote_nullifiers.append(&mut update.vote_nullifiers);
Ok(())
}
DaoFunction::Exec => {
let update: DaoExecUpdate = deserialize(&ix[1..])?;
// TODO: Implement db_del
// Remove proposal from db
Ok(())
}
}
}
fn get_metadata(cid: ContractId, ix: &[u8]) -> ContractResult {
let (call_idx, call): (u32, Vec<ContractCall>) = deserialize(ix)?;
assert!(call_idx < call.len() as u32);
let self_ = &call[call_idx as usize];
match DaoFunction::try_from(self_.data[0])? {
DaoFunction::Mint => {
let params: DaoMintParams = deserialize(&self_.data[1..])?;
let mut zk_public_values: Vec<(String, Vec<pallas::Base>)> = vec![];
let signature_pubkeys: Vec<PublicKey> = vec![];
zk_public_values.push((ZKAS_DAO_MINT_NS.to_string(), vec![params.dao_bulla.inner()]));
let mut metadata = vec![];
zk_public_values.encode(&mut metadata)?;
signature_pubkeys.encode(&mut metadata)?;
// Using this, we pass the above data to the host.
set_return_data(&metadata)?;
Ok(())
}
DaoFunction::Propose => {
let params: DaoProposeParams = deserialize(&self_.data[1..])?;
assert!(!params.inputs.is_empty());
let mut zk_public_values: Vec<(String, Vec<pallas::Base>)> = vec![];
let mut signature_pubkeys: Vec<PublicKey> = vec![];
let mut total_funds_commit = pallas::Point::identity();
for input in &params.inputs {
signature_pubkeys.push(input.signature_public);
total_funds_commit += input.value_commit;
let value_coords = input.value_commit.to_affine().coordinates().unwrap();
let (sig_x, sig_y) = input.signature_public.xy();
zk_public_values.push((
ZKAS_DAO_PROPOSE_BURN_NS.to_string(),
vec![
*value_coords.x(),
*value_coords.y(),
params.token_commit,
input.merkle_root.inner(),
sig_x,
sig_y,
],
));
}
let total_funds_coords = total_funds_commit.to_affine().coordinates().unwrap();
zk_public_values.push((
ZKAS_DAO_PROPOSE_MAIN_NS.to_string(),
vec![
params.token_commit,
params.dao_merkle_root.inner(),
params.proposal_bulla,
*total_funds_coords.x(),
*total_funds_coords.y(),
],
));
let mut metadata = vec![];
zk_public_values.encode(&mut metadata)?;
signature_pubkeys.encode(&mut metadata)?;
// Using this, we pass the above data to the host.
set_return_data(&metadata)?;
Ok(())
}
DaoFunction::Vote => {
let params: DaoVoteParams = deserialize(&self_.data[1..])?;
assert!(!params.inputs.is_empty());
let mut zk_public_values: Vec<(String, Vec<pallas::Base>)> = vec![];
let mut signature_pubkeys: Vec<PublicKey> = vec![];
let mut all_votes_commit = pallas::Point::identity();
for input in &params.inputs {
signature_pubkeys.push(input.signature_public);
all_votes_commit += input.vote_commit;
let value_coords = input.vote_commit.to_affine().coordinates().unwrap();
let (sig_x, sig_y) = input.signature_public.xy();
zk_public_values.push((
ZKAS_DAO_VOTE_BURN_NS.to_string(),
vec![
input.nullifier.inner(),
*value_coords.x(),
*value_coords.y(),
params.token_commit,
input.merkle_root.inner(),
sig_x,
sig_y,
],
));
}
let yes_vote_commit_coords = params.yes_vote_commit.to_affine().coordinates().unwrap();
let all_vote_commit_coords = all_votes_commit.to_affine().coordinates().unwrap();
zk_public_values.push((
ZKAS_DAO_VOTE_MAIN_NS.to_string(),
vec![
params.token_commit,
params.proposal_bulla,
*yes_vote_commit_coords.x(),
*yes_vote_commit_coords.y(),
*all_vote_commit_coords.x(),
*all_vote_commit_coords.y(),
],
));
let mut metadata = vec![];
zk_public_values.encode(&mut metadata)?;
signature_pubkeys.encode(&mut metadata)?;
// Using this, we pass the above data to the host.
set_return_data(&metadata)?;
Ok(())
}
DaoFunction::Exec => {
let params: DaoExecParams = deserialize(&self_.data[1..])?;
let mut zk_public_values: Vec<(String, Vec<pallas::Base>)> = vec![];
let signature_pubkeys: Vec<PublicKey> = vec![];
let yes_votes_coords = params.yes_votes_commit.to_affine().coordinates().unwrap();
let all_votes_coords = params.all_votes_commit.to_affine().coordinates().unwrap();
let input_value_coords = params.input_value_commit.to_affine().coordinates().unwrap();
zk_public_values.push((
ZKAS_DAO_EXEC_NS.to_string(),
vec![
params.proposal,
params.coin_0,
params.coin_1,
*yes_votes_coords.x(),
*yes_votes_coords.y(),
*all_votes_coords.x(),
*all_votes_coords.y(),
*input_value_coords.x(),
*input_value_coords.y(),
pallas::Base::from(u64::MAX - 420), // <-- TODO: Should be money contract id?
pallas::Base::zero(),
pallas::Base::zero(),
],
));
let mut metadata = vec![];
zk_public_values.encode(&mut metadata)?;
signature_pubkeys.encode(&mut metadata)?;
// Using this, we pass the above data to the host.
set_return_data(&metadata)?;
Ok(())
}
}
}

View File

@@ -0,0 +1,46 @@
/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2022 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use darkfi_sdk::error::ContractError;
#[cfg(not(feature = "no-entrypoint"))]
pub mod entrypoint;
pub mod state;
#[repr(u8)]
pub enum DaoFunction {
Mint = 0x00,
Propose = 0x01,
Vote = 0x02,
Exec = 0x03,
}
impl TryFrom<u8> for DaoFunction {
type Error = ContractError;
fn try_from(x: u8) -> core::result::Result<DaoFunction, Self::Error> {
match x {
0x00 => Ok(DaoFunction::Mint),
0x01 => Ok(DaoFunction::Propose),
0x02 => Ok(DaoFunction::Vote),
0x03 => Ok(DaoFunction::Exec),
_ => Err(ContractError::InvalidFunction),
}
}
}

View File

@@ -0,0 +1,138 @@
/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2022 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use darkfi_sdk::{
crypto::{MerkleNode, Nullifier, PublicKey},
pasta::{group::Group, pallas},
};
use darkfi_serial::{SerialDecodable, SerialEncodable};
#[derive(SerialEncodable, SerialDecodable)]
pub struct DaoBulla(pallas::Base);
impl DaoBulla {
pub fn inner(&self) -> pallas::Base {
self.0
}
}
impl From<pallas::Base> for DaoBulla {
fn from(x: pallas::Base) -> Self {
Self(x)
}
}
#[derive(SerialEncodable, SerialDecodable)]
pub struct ProposalVotes {
// TODO: Might be more logical to have `yes_votes_commit` and `no_votes_commit`
/// Weighted vote commit
pub yes_votes_commit: pallas::Point,
/// All value staked in the vote
pub all_votes_commit: pallas::Point,
/// Vote nullifiers
pub vote_nullifiers: Vec<Nullifier>,
}
impl ProposalVotes {
pub fn nullifier_exists(&self, nullifier: &Nullifier) -> bool {
self.vote_nullifiers.iter().any(|n| n == nullifier)
}
}
impl Default for ProposalVotes {
fn default() -> Self {
Self {
yes_votes_commit: pallas::Point::identity(),
all_votes_commit: pallas::Point::identity(),
vote_nullifiers: vec![],
}
}
}
#[derive(SerialEncodable, SerialDecodable)]
pub struct DaoMintParams {
pub dao_bulla: DaoBulla,
}
#[derive(SerialEncodable, SerialDecodable)]
pub struct DaoMintUpdate {
pub dao_bulla: DaoBulla,
}
#[derive(Clone, SerialEncodable, SerialDecodable)]
pub struct ProposeInput {
pub value_commit: pallas::Point,
pub merkle_root: MerkleNode,
pub signature_public: PublicKey,
}
#[derive(SerialEncodable, SerialDecodable)]
pub struct DaoProposeParams {
pub dao_merkle_root: MerkleNode,
pub token_commit: pallas::Base,
pub proposal_bulla: pallas::Base,
pub ciphertext: Vec<u8>,
pub ephem_public: PublicKey,
pub inputs: Vec<ProposeInput>,
}
#[derive(SerialEncodable, SerialDecodable)]
pub struct DaoProposeUpdate {
pub proposal_bulla: pallas::Base,
}
#[derive(SerialEncodable, SerialDecodable)]
pub struct VoteInput {
pub nullifier: Nullifier,
pub vote_commit: pallas::Point,
pub merkle_root: MerkleNode,
pub signature_public: PublicKey,
}
#[derive(SerialEncodable, SerialDecodable)]
pub struct DaoVoteParams {
pub token_commit: pallas::Base,
pub proposal_bulla: pallas::Base,
pub yes_vote_commit: pallas::Point,
pub ciphertext: Vec<u8>,
pub ephem_public: PublicKey,
pub inputs: Vec<VoteInput>,
}
#[derive(SerialEncodable, SerialDecodable)]
pub struct DaoVoteUpdate {
pub proposal_bulla: pallas::Base,
pub vote_nullifiers: Vec<Nullifier>,
pub yes_vote_commit: pallas::Point,
pub all_vote_commit: pallas::Point,
}
#[derive(SerialEncodable, SerialDecodable)]
pub struct DaoExecParams {
pub proposal: pallas::Base,
pub coin_0: pallas::Base,
pub coin_1: pallas::Base,
pub yes_votes_commit: pallas::Point,
pub all_votes_commit: pallas::Point,
pub input_value_commit: pallas::Point,
}
#[derive(SerialEncodable, SerialDecodable)]
pub struct DaoExecUpdate {
pub proposal: pallas::Base,
}

View File

@@ -34,5 +34,4 @@ test: all
clean:
rm -f $(ZKAS_BIN) $(WASM_BIN)
# We always rebuild the wasm no matter what
.PHONY: all test clean

View File

@@ -68,6 +68,9 @@ pub enum ContractError {
#[error("Db contains_key failed")]
DbContainsKeyFailed,
#[error("Invalid function call")]
InvalidFunction,
}
/// Builtin return values occupy the upper 32 bits
@@ -91,6 +94,7 @@ pub const DB_SET_FAILED: i64 = to_builtin!(11);
pub const DB_LOOKUP_FAILED: i64 = to_builtin!(12);
pub const DB_GET_FAILED: i64 = to_builtin!(13);
pub const DB_CONTAINS_KEY_FAILED: i64 = to_builtin!(14);
pub const INVALID_FUNCTION: i64 = to_builtin!(15);
impl From<ContractError> for i64 {
fn from(err: ContractError) -> Self {
@@ -108,6 +112,7 @@ impl From<ContractError> for i64 {
ContractError::DbLookupFailed => DB_LOOKUP_FAILED,
ContractError::DbGetFailed => DB_GET_FAILED,
ContractError::DbContainsKeyFailed => DB_CONTAINS_KEY_FAILED,
ContractError::InvalidFunction => INVALID_FUNCTION,
ContractError::Custom(error) => {
if error == 0 {
CUSTOM_ZERO
@@ -136,6 +141,7 @@ impl From<i64> for ContractError {
DB_LOOKUP_FAILED => Self::DbLookupFailed,
DB_GET_FAILED => Self::DbGetFailed,
DB_CONTAINS_KEY_FAILED => Self::DbContainsKeyFailed,
INVALID_FUNCTION => Self::InvalidFunction,
_ => Self::Custom(error as u32),
}
}