darkfi_dao_contract/
model.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2025 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use core::str::FromStr;
20
21use darkfi_money_contract::model::{Nullifier, TokenId};
22use darkfi_sdk::{
23    crypto::{
24        note::{AeadEncryptedNote, ElGamalEncryptedNote},
25        pasta_prelude::*,
26        poseidon_hash, BaseBlind, ContractId, MerkleNode, PublicKey,
27    },
28    error::ContractError,
29    pasta::pallas,
30};
31use darkfi_serial::{Encodable, SerialDecodable, SerialEncodable};
32
33#[cfg(feature = "client")]
34use darkfi_serial::async_trait;
35
36#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
37// ANCHOR: dao
38/// DAOs are represented on chain as a commitment to this object
39pub struct Dao {
40    /// The minimum amount of governance tokens needed to open a proposal
41    pub proposer_limit: u64,
42    /// Minimal threshold of participating total tokens needed for a proposal to pass
43    pub quorum: u64,
44    /// Minimal threshold of participating total tokens needed for a proposal to
45    /// be considered as strongly supported, enabling early execution.
46    /// Must be greater or equal to normal quorum.
47    pub early_exec_quorum: u64,
48    /// The ratio of winning/total votes needed for a proposal to pass
49    pub approval_ratio_quot: u64,
50    pub approval_ratio_base: u64,
51    /// DAO's governance token ID
52    pub gov_token_id: TokenId,
53    /// DAO notes decryption public key
54    pub notes_public_key: PublicKey,
55    /// DAO proposals creator public key
56    pub proposer_public_key: PublicKey,
57    /// DAO proposals viewer public key
58    pub proposals_public_key: PublicKey,
59    /// DAO votes viewer public key
60    pub votes_public_key: PublicKey,
61    /// DAO proposals executor public key
62    pub exec_public_key: PublicKey,
63    /// DAO strongly supported proposals executor public key
64    pub early_exec_public_key: PublicKey,
65    /// DAO bulla blind
66    pub bulla_blind: BaseBlind,
67}
68// ANCHOR_END: dao
69
70impl Dao {
71    pub fn to_bulla(&self) -> DaoBulla {
72        let proposer_limit = pallas::Base::from(self.proposer_limit);
73        let quorum = pallas::Base::from(self.quorum);
74        let early_exec_quorum = pallas::Base::from(self.early_exec_quorum);
75        let approval_ratio_quot = pallas::Base::from(self.approval_ratio_quot);
76        let approval_ratio_base = pallas::Base::from(self.approval_ratio_base);
77        let (notes_pub_x, notes_pub_y) = self.notes_public_key.xy();
78        let (proposer_pub_x, proposer_pub_y) = self.proposer_public_key.xy();
79        let (proposals_pub_x, proposals_pub_y) = self.proposals_public_key.xy();
80        let (votes_pub_x, votes_pub_y) = self.votes_public_key.xy();
81        let (exec_pub_x, exec_pub_y) = self.exec_public_key.xy();
82        let (early_exec_pub_x, early_exec_pub_y) = self.early_exec_public_key.xy();
83        let bulla = poseidon_hash([
84            proposer_limit,
85            quorum,
86            early_exec_quorum,
87            approval_ratio_quot,
88            approval_ratio_base,
89            self.gov_token_id.inner(),
90            notes_pub_x,
91            notes_pub_y,
92            proposer_pub_x,
93            proposer_pub_y,
94            proposals_pub_x,
95            proposals_pub_y,
96            votes_pub_x,
97            votes_pub_y,
98            exec_pub_x,
99            exec_pub_y,
100            early_exec_pub_x,
101            early_exec_pub_y,
102            self.bulla_blind.inner(),
103        ]);
104        DaoBulla(bulla)
105    }
106}
107
108/// A `DaoBulla` represented in the state
109#[derive(Debug, Copy, Clone, Eq, PartialEq, SerialEncodable, SerialDecodable)]
110pub struct DaoBulla(pallas::Base);
111
112impl DaoBulla {
113    /// Reference the raw inner base field element
114    pub fn inner(&self) -> pallas::Base {
115        self.0
116    }
117
118    /// Create a `DaoBulla` object from given bytes, erroring if the
119    /// input bytes are noncanonical.
120    pub fn from_bytes(x: [u8; 32]) -> Result<Self, ContractError> {
121        match pallas::Base::from_repr(x).into() {
122            Some(v) => Ok(Self(v)),
123            None => {
124                Err(ContractError::IoError("Failed to instantiate DaoBulla from bytes".to_string()))
125            }
126        }
127    }
128
129    /// Convert the `DaoBulla` type into 32 raw bytes
130    pub fn to_bytes(&self) -> [u8; 32] {
131        self.0.to_repr()
132    }
133}
134
135impl std::hash::Hash for DaoBulla {
136    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
137        state.write(&self.to_bytes());
138    }
139}
140
141darkfi_sdk::fp_from_bs58!(DaoBulla);
142darkfi_sdk::fp_to_bs58!(DaoBulla);
143darkfi_sdk::ty_from_fp!(DaoBulla);
144
145#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
146// ANCHOR: dao-auth-call
147pub struct DaoAuthCall {
148    pub contract_id: ContractId,
149    pub function_code: u8,
150    pub auth_data: Vec<u8>,
151}
152// ANCHOR_END: dao-auth-call
153
154pub trait VecAuthCallCommit {
155    fn commit(&self) -> pallas::Base;
156}
157
158impl VecAuthCallCommit for Vec<DaoAuthCall> {
159    fn commit(&self) -> pallas::Base {
160        // Hash a bunch of data, then convert it so pallas::Base
161        // see https://docs.rs/ff/0.13.0/ff/trait.FromUniformBytes.html
162        // We essentially create a really large value and reduce it modulo the field
163        // to diminish the statistical significance of any overlap.
164        //
165        // The range of pallas::Base is [0, p-1] where p < u256 (=32 bytes).
166        // For those values produced by blake3 hash which are [p, u256::MAX],
167        // they get mapped to [0, u256::MAX - p].
168        // Those 32 bits of pallas::Base are hashed to more frequently.
169        // note: blake2 is more secure but slower than blake3
170        let mut hasher =
171            blake2b_simd::Params::new().hash_length(64).personal(b"justDAOthings").to_state();
172        self.encode(&mut hasher).unwrap();
173        let hash = hasher.finalize();
174        let bytes = hash.as_array();
175        pallas::Base::from_uniform_bytes(bytes)
176    }
177}
178
179#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
180// ANCHOR: dao-proposal
181pub struct DaoProposal {
182    pub auth_calls: Vec<DaoAuthCall>,
183    pub creation_blockwindow: u64,
184    pub duration_blockwindows: u64,
185    /// Arbitrary data provided by the user. We don't use this.
186    pub user_data: pallas::Base,
187    pub dao_bulla: DaoBulla,
188    pub blind: BaseBlind,
189}
190// ANCHOR_END: dao-proposal
191
192impl DaoProposal {
193    pub fn to_bulla(&self) -> DaoProposalBulla {
194        let bulla = poseidon_hash([
195            self.auth_calls.commit(),
196            pallas::Base::from(self.creation_blockwindow),
197            pallas::Base::from(self.duration_blockwindows),
198            self.user_data,
199            self.dao_bulla.inner(),
200            self.blind.inner(),
201        ]);
202        DaoProposalBulla(bulla)
203    }
204}
205
206/// A `DaoProposalBulla` represented in the state
207#[derive(Debug, Copy, Clone, Eq, PartialEq, SerialEncodable, SerialDecodable)]
208pub struct DaoProposalBulla(pallas::Base);
209
210impl DaoProposalBulla {
211    /// Reference the raw inner base field element
212    pub fn inner(&self) -> pallas::Base {
213        self.0
214    }
215
216    /// Create a `DaoBulla` object from given bytes, erroring if the
217    /// input bytes are noncanonical.
218    pub fn from_bytes(x: [u8; 32]) -> Result<Self, ContractError> {
219        match pallas::Base::from_repr(x).into() {
220            Some(v) => Ok(Self(v)),
221            None => Err(ContractError::IoError(
222                "Failed to instantiate DaoProposalBulla from bytes".to_string(),
223            )),
224        }
225    }
226
227    /// Convert the `DaoBulla` type into 32 raw bytes
228    pub fn to_bytes(&self) -> [u8; 32] {
229        self.0.to_repr()
230    }
231}
232
233impl std::hash::Hash for DaoProposalBulla {
234    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
235        state.write(&self.to_bytes());
236    }
237}
238
239darkfi_sdk::fp_from_bs58!(DaoProposalBulla);
240darkfi_sdk::fp_to_bs58!(DaoProposalBulla);
241darkfi_sdk::ty_from_fp!(DaoProposalBulla);
242
243#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
244// ANCHOR: dao-mint-params
245/// Parameters for `Dao::Mint`
246pub struct DaoMintParams {
247    /// The DAO bulla
248    pub dao_bulla: DaoBulla,
249    /// The DAO signature(notes) public key
250    pub dao_pubkey: PublicKey,
251}
252// ANCHOR_END: dao-mint-params
253
254/// State update for `Dao::Mint`
255#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
256pub struct DaoMintUpdate {
257    /// Revealed DAO bulla
258    pub dao_bulla: DaoBulla,
259}
260
261#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
262// ANCHOR: dao-propose-params
263/// Parameters for `Dao::Propose`
264pub struct DaoProposeParams {
265    /// Merkle root of the DAO in the DAO state
266    pub dao_merkle_root: MerkleNode,
267    /// Token ID commitment for the proposal
268    pub token_commit: pallas::Base,
269    /// Bulla of the DAO proposal
270    pub proposal_bulla: DaoProposalBulla,
271    /// Encrypted note
272    pub note: AeadEncryptedNote,
273    /// Inputs for the proposal
274    pub inputs: Vec<DaoProposeParamsInput>,
275}
276// ANCHOR_END: dao-propose-params
277
278#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
279// ANCHOR: dao-propose-params-input
280/// Input for a DAO proposal
281pub struct DaoProposeParamsInput {
282    /// Value commitment for the input
283    pub value_commit: pallas::Point,
284    /// Merkle root for the input's coin inclusion proof
285    pub merkle_coin_root: MerkleNode,
286    /// SMT root for the input's nullifier exclusion proof
287    pub smt_null_root: pallas::Base,
288    /// Public key used for signing
289    pub signature_public: PublicKey,
290}
291// ANCHOR_END: dao-propose-params-input
292
293/// State update for `Dao::Propose`
294#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
295pub struct DaoProposeUpdate {
296    /// Minted proposal bulla
297    pub proposal_bulla: DaoProposalBulla,
298    /// Snapshotted Merkle root in the Money state
299    pub snapshot_coins: MerkleNode,
300    /// Snapshotted SMT root in the Money state
301    pub snapshot_nulls: pallas::Base,
302}
303
304/// Metadata for a DAO proposal on the blockchain
305#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
306pub struct DaoProposalMetadata {
307    /// Vote aggregate
308    pub vote_aggregate: DaoBlindAggregateVote,
309    /// Snapshotted Merkle root in the Money state
310    pub snapshot_coins: MerkleNode,
311    /// Snapshotted SMT root in the Money state
312    pub snapshot_nulls: pallas::Base,
313}
314
315#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
316// ANCHOR: dao-vote-params
317/// Parameters for `Dao::Vote`
318pub struct DaoVoteParams {
319    /// Token commitment for the vote inputs
320    pub token_commit: pallas::Base,
321    /// Proposal bulla being voted on
322    pub proposal_bulla: DaoProposalBulla,
323    /// Commitment for yes votes
324    pub yes_vote_commit: pallas::Point,
325    /// Encrypted note
326    pub note: ElGamalEncryptedNote<4>,
327    /// Inputs for the vote
328    pub inputs: Vec<DaoVoteParamsInput>,
329}
330// ANCHOR_END: dao-vote-params
331
332#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
333// ANCHOR: dao-vote-params-input
334/// Input for a DAO proposal vote
335pub struct DaoVoteParamsInput {
336    /// Vote commitment
337    pub vote_commit: pallas::Point,
338    /// Vote nullifier
339    pub vote_nullifier: Nullifier,
340    /// Public key used for signing
341    pub signature_public: PublicKey,
342}
343// ANCHOR_END: dao-vote-params-input
344
345/// State update for `Dao::Vote`
346#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
347pub struct DaoVoteUpdate {
348    /// The proposal bulla being voted on
349    pub proposal_bulla: DaoProposalBulla,
350    /// The updated proposal metadata
351    pub proposal_metadata: DaoProposalMetadata,
352    /// Vote nullifiers,
353    pub vote_nullifiers: Vec<Nullifier>,
354}
355
356#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
357// ANCHOR: dao-blind-aggregate-vote
358/// Represents a single or multiple blinded votes.
359/// These can be summed together.
360pub struct DaoBlindAggregateVote {
361    /// Weighted vote commit
362    pub yes_vote_commit: pallas::Point,
363    /// All value staked in the vote
364    pub all_vote_commit: pallas::Point,
365}
366// ANCHOR_END: dao-blind-aggregate-vote
367
368impl DaoBlindAggregateVote {
369    /// Aggregate a vote with existing one
370    pub fn aggregate(&mut self, other: Self) {
371        self.yes_vote_commit += other.yes_vote_commit;
372        self.all_vote_commit += other.all_vote_commit;
373    }
374}
375
376impl Default for DaoBlindAggregateVote {
377    fn default() -> Self {
378        Self {
379            yes_vote_commit: pallas::Point::identity(),
380            all_vote_commit: pallas::Point::identity(),
381        }
382    }
383}
384
385#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
386// ANCHOR: dao-exec-params
387/// Parameters for `Dao::Exec`
388pub struct DaoExecParams {
389    /// The proposal bulla
390    pub proposal_bulla: DaoProposalBulla,
391    /// The proposal auth calls
392    pub proposal_auth_calls: Vec<DaoAuthCall>,
393    /// Aggregated blinds for the vote commitments
394    pub blind_total_vote: DaoBlindAggregateVote,
395    /// Flag indicating if its early execution
396    pub early_exec: bool,
397    /// Public key for the signature.
398    /// The signature ensures this DAO::exec call cannot be modified with other calls.
399    pub signature_public: PublicKey,
400}
401// ANCHOR_END: dao-exec-params
402
403/// State update for `Dao::Exec`
404#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
405pub struct DaoExecUpdate {
406    /// The proposal bulla
407    pub proposal_bulla: DaoProposalBulla,
408}
409
410#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
411// ANCHOR: dao-auth_xfer-params
412/// Parameters for `Dao::AuthMoneyTransfer`
413pub struct DaoAuthMoneyTransferParams {
414    pub enc_attrs: Vec<ElGamalEncryptedNote<5>>,
415    pub dao_change_attrs: ElGamalEncryptedNote<3>,
416}
417// ANCHOR_END: dao-auth_xfer-params