Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: psp22 example DAO contract #407

Open
wants to merge 51 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
d8f3c22
Dao contract
ndkazu Dec 13, 2024
5cbb44a
Added some events
ndkazu Dec 15, 2024
bb060f1
removed absent module
ndkazu Dec 15, 2024
30637ae
Added some in-code documentation
ndkazu Dec 15, 2024
e6c9edd
Corrected index error
ndkazu Dec 15, 2024
058e53b
Problem with first test
ndkazu Dec 15, 2024
ff2b300
Making sense of pop-drink for testing
ndkazu Dec 15, 2024
2505f89
Added lib.rs to Cargo, but still...
ndkazu Dec 16, 2024
6f653a9
Added the correct deploy() function in tests
ndkazu Dec 17, 2024
aacbf87
Put a limitation of description string length
ndkazu Dec 17, 2024
f746400
chore: add missing authors
chungquantin Dec 17, 2024
285c178
new member test added
ndkazu Dec 17, 2024
34002d0
create_proposal test
ndkazu Dec 17, 2024
d17096c
Calls prepared for testing
ndkazu Dec 17, 2024
3f2c114
ReadMe
ndkazu Dec 18, 2024
22d46e3
ReadME
ndkazu Dec 18, 2024
b02f232
create_proposal_test
ndkazu Dec 19, 2024
c08b335
Another test
ndkazu Dec 19, 2024
7a1563b
Enactment test
ndkazu Dec 19, 2024
9eca1b2
one more test
ndkazu Dec 19, 2024
b0d6194
Reverted some changes
ndkazu Dec 19, 2024
f8910ce
Reverted some changes
ndkazu Dec 19, 2024
94d884e
Documented the failing test: proposal_enactment_works
ndkazu Dec 19, 2024
00d76c0
Another test...
ndkazu Dec 19, 2024
8de7a7d
tests
ndkazu Dec 19, 2024
5e91679
refactored the code & added another test
ndkazu Dec 19, 2024
77328df
Treasury balance check & another test
ndkazu Dec 20, 2024
a8206d4
Another test
ndkazu Dec 20, 2024
56e622d
cargo fmt
ndkazu Dec 20, 2024
4ad34f4
Final test
ndkazu Dec 20, 2024
0cd9573
Applied some fixes related to the code review
ndkazu Dec 24, 2024
be3759f
Added ProposalStatus enum
ndkazu Dec 24, 2024
a34f0d1
Added descriptions for errors
ndkazu Dec 24, 2024
5e8e8e4
Review correction
ndkazu Dec 24, 2024
ede1283
cargo clippy
ndkazu Dec 24, 2024
425d757
Merge branch 'r0gue-io:main' into psp22Example
ndkazu Dec 26, 2024
0e2db17
update ink version
ndkazu Dec 26, 2024
21b13c5
cargo clippy --fix
ndkazu Dec 26, 2024
34ba35c
Some clean up
ndkazu Dec 26, 2024
bd11e86
All tests pass
ndkazu Dec 26, 2024
905705c
cargo fmt
ndkazu Dec 26, 2024
0841c32
Refactored code, implemented Default trait for Proposal
ndkazu Dec 26, 2024
562b17f
cargo fmt
ndkazu Dec 26, 2024
eaae705
Preparations for use of RuntimeCall
ndkazu Dec 27, 2024
adb72f9
Use transfer_from instead of transfer for runtime_call
ndkazu Dec 27, 2024
290d0ec
Corrected test mistake, using transfer_from instead of transfer
ndkazu Dec 29, 2024
bad1126
RuntimeCall working
ndkazu Dec 29, 2024
32ecca5
Corrections
ndkazu Dec 31, 2024
c357a07
Code re-factoring
ndkazu Dec 31, 2024
c0f9f9d
RuntimeCall conversion problem
ndkazu Jan 8, 2025
2526c04
customised RuntimeCalls works
ndkazu Jan 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions pop-api/examples/dao/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[package]
name = "dao"
version = "0.1.0"
edition = "2021"

[dependencies]
ink = { version = "=5.0.0", default-features = false, features = ["ink-debug"] }
pop-api = { path = "../../../pop-api", default-features = false, features = [
"fungibles",
] }
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2.3", default-features = false, features = ["derive"], optional = true }

[dev-dependencies]
drink = { package = "pop-drink", git = "https://github.com/r0gue-io/pop-drink" }
env_logger = { version = "0.11.3" }
serde_json = "1.0.114"

# TODO: due to compilation issues caused by `sp-runtime`, `frame-support-procedural` and `staging-xcm` this dependency
# (with specific version) has to be added. Will be tackled by #348, please ignore for now.
frame-support-procedural = { version = "=30.0.1", default-features = false }
sp-runtime = { version = "=38.0.0", default-features = false }
staging-xcm = { version = "=14.1.0", default-features = false }

[features]
default = ["std"]
e2e-tests = []
ink-as-dependency = []
std = [
"ink/std",
"pop-api/std",
"scale-info/std",
"scale/std",
]
303 changes: 303 additions & 0 deletions pop-api/examples/dao/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
use ink::{
prelude::{string::String, vec::Vec},
storage::Mapping,
};
use pop_api::{
primitives::TokenId,
v0::fungibles::{
self as api,
events::{Approval, Created, Transfer},
Psp22Error,
},
};

#[cfg(test)]
mod tests;

#[ink::contract]
mod dao {
use super::*;

/// Structure of the proposal used by the Dao governance sysytem
#[derive(scale::Decode, scale::Encode, Debug)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
ndkazu marked this conversation as resolved.
Show resolved Hide resolved
pub struct Proposal {
// Description of the proposal
description: String,

// Beginnning of the voting period for this proposal
vote_start: BlockNumber,

// End of the voting period for this proposal
vote_end: BlockNumber,

// Balance representing the total votes for this proposal
yes_votes: Balance,

// Balance representing the total votes against this proposal
no_votes: Balance,

// Flag that indicates if the proposal was executed
executed: bool,

// AccountId of the recipient of the proposal
ndkazu marked this conversation as resolved.
Show resolved Hide resolved
beneficiary: AccountId,

// Amount of tokens to be awarded to the beneficiary
amount: Balance,

// Identifier of the proposal
proposal_id: u32,
}

/// Representation of a member in the voting system
#[derive(scale::Decode, scale::Encode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))]
ndkazu marked this conversation as resolved.
Show resolved Hide resolved
pub struct Member {
// Stores the member's voting influence by using his balance
voting_power: Balance,

// Keeps track of the last vote casted by the member
last_vote: BlockNumber,
}

/// Structure of a DAO (Decentralized Autonomous Organization)
/// that uses Psp22 to manage the Dao treasury and funds projects
/// selected by the members through governance
#[ink(storage)]
pub struct Dao {
// Funding proposals
proposals: Vec<Proposal>,

// Mapping of AccountId to Member structs, representing DAO membership.
members: Mapping<AccountId, Member>,

// Mapping tracking the last time each account voted.
last_votes: Mapping<AccountId, Timestamp>,

// Duration of the voting period
voting_period: BlockNumber,

// Identifier of the Psp22 token associated with this DAO
token_id: TokenId,
}

impl Dao {
/// Instantiate a new Dao contract and create the associated token
ndkazu marked this conversation as resolved.
Show resolved Hide resolved
///
/// # Parameters:
/// - `token_id` - The identifier of the token to be created
/// - `voting_period` - Amount of blocks during which members can cast their votes
/// - `min_balance` - The minimum balance required for accounts holding this token.
// The `min_balance` ensures accounts hold a minimum amount of tokens, preventing tiny,
// inactive balances from bloating the blockchain state and slowing down the network.
#[ink(constructor, payable)]
pub fn new(
token_id: TokenId,
voting_period: BlockNumber,
min_balance: Balance,
) -> Result<Self, Psp22Error> {
let instance = Self {
proposals: Vec::new(),
members: Mapping::default(),
last_votes: Mapping::default(),
voting_period,
token_id: token_id,
};
let contract_id = instance.env().account_id();
api::create(token_id, contract_id, min_balance).map_err(Psp22Error::from)?;
instance.env().emit_event(Created {
id: token_id,
creator: contract_id,
admin: contract_id,
});

Ok(instance)
}

/// Allows members to create new spending proposals
///
/// # Parameters
/// - `beneficiary` - The account that will receive the payment
/// if the proposal is accepted.
/// - `amount` - Amount requested for this proposal
/// - `description` - Description of the proposal
#[ink(message)]
pub fn create_proposal(
&mut self,
beneficiary: AccountId,
amount: Balance,
description: String,
ndkazu marked this conversation as resolved.
Show resolved Hide resolved
) -> Result<(), Error> {
let _caller = self.env().caller();
let current_block = self.env().block_number();
let proposal_id: u32 = self.proposals.len().try_into().unwrap_or(0u32);
let vote_end = current_block.checked_add(self.voting_period).ok_or(Error::ArithmeticOverflow)?;
let proposal = Proposal {
ndkazu marked this conversation as resolved.
Show resolved Hide resolved
description,
vote_start: current_block,
vote_end,
yes_votes: 0,
no_votes: 0,
executed: false,
beneficiary,
amount,
proposal_id,
};

self.proposals.push(proposal);
Ok(())
}

/// Allows Dao's members to vote for a proposal
///
/// # Parameters
/// - `proposal_id` - Identifier of the proposal
/// - `approve` - Indicates whether the vote is in favor (true) or against (false) the
/// proposal.
#[ink(message)]
pub fn vote(&mut self, proposal_id: u32, approve: bool) -> Result<(), Error> {
let caller = self.env().caller();
let current_block = self.env().block_number();

let proposal =
self.proposals.get_mut(proposal_id as usize).ok_or(Error::ProposalNotFound)?;

if current_block < proposal.vote_start || current_block > proposal.vote_end {
return Err(Error::VotingPeriodEnded);
}

let member = self.members.get(caller).ok_or(Error::NotAMember)?;

if member.last_vote >= proposal.vote_start {
return Err(Error::AlreadyVoted);
}

if approve {
proposal.yes_votes.checked_add(member.voting_power).ok_or(Error::ArithmeticOverflow)?;
} else {
proposal.no_votes.checked_add(member.voting_power).ok_or(Error::ArithmeticOverflow)?;
}

self.members.insert(
caller,
&Member { voting_power: member.voting_power, last_vote: current_block },
);

Ok(())
}

/// Enact a proposal approved by the Dao members
///
/// # Parameters
/// - `proposal_id` - Identifier of the proposal
#[ink(message)]
pub fn execute_proposal(&mut self, proposal_id: u32) -> Result<(), Error> {
let vote_end = self
.proposals
.get(proposal_id as usize)
.ok_or(Error::ProposalNotFound)?
.vote_end;

// Check the voting period
if self.env().block_number() <= vote_end {
ndkazu marked this conversation as resolved.
Show resolved Hide resolved
return Err(Error::VotingPeriodNotEnded);
}

// If we've passed the checks, now we can mutably borrow the proposal
let proposal_id_usize = proposal_id as usize;
let proposal = self.proposals.get(proposal_id_usize).ok_or(Error::ProposalNotFound)?;

if proposal.executed {
return Err(Error::ProposalAlreadyExecuted);
}

if proposal.yes_votes > proposal.no_votes {
let contract = self.env().account_id();
// ToDo: Check that there is enough funds in the treasury
// Execute the proposal
api::transfer(self.token_id, proposal.beneficiary, proposal.amount)
.map_err(Psp22Error::from)?;
self.env().emit_event(Transfer {
from: Some(contract),
to: Some(proposal.beneficiary),
value: proposal.amount,
});
self.env().emit_event(Approval {
owner: contract,
spender: contract,
value: proposal.amount,
});

if let Some(proposal) = self.proposals.get_mut(proposal_id_usize) {
proposal.executed = true;
}
Ok(())
} else {
Err(Error::ProposalRejected)
}
}

/// Allows a user to become a member of the Dao
/// by transferring some tokens to the DAO's treasury.
/// The amount of tokens transferred will be stored as the
/// voting power of this member.
///
/// # Parameters
/// - `amount` - Balance transferred to the Dao and representing
/// the voting power of the member.

#[ink(message)]
pub fn join(&mut self, amount: Balance) -> Result<(), Error> {
let caller = self.env().caller();
let contract = self.env().account_id();
api::transfer_from(self.token_id, caller.clone(), contract.clone(), amount)
.map_err(Psp22Error::from)?;
self.env().emit_event(Transfer {
from: Some(caller),
to: Some(contract),
value: amount,
});

let member =
self.members.get(caller).unwrap_or(Member { voting_power: 0, last_vote: 0 });

let voting_power = member.voting_power.checked_add(amount).ok_or(Error::ArithmeticOverflow)?;
self.members.insert(
caller,
&Member { voting_power, last_vote: member.last_vote },
);

Ok(())
}
}

#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Error {
ndkazu marked this conversation as resolved.
Show resolved Hide resolved
ArithmeticOverflow,
ndkazu marked this conversation as resolved.
Show resolved Hide resolved
ProposalNotFound,
VotingPeriodEnded,
NotAMember,
ndkazu marked this conversation as resolved.
Show resolved Hide resolved
AlreadyVoted,
VotingPeriodNotEnded,
ProposalAlreadyExecuted,
ndkazu marked this conversation as resolved.
Show resolved Hide resolved
ProposalRejected,
Psp22(Psp22Error),
}

impl From<Psp22Error> for Error {
fn from(error: Psp22Error) -> Self {
Error::Psp22(error)
}
}

impl From<Error> for Psp22Error {
ndkazu marked this conversation as resolved.
Show resolved Hide resolved
fn from(error: Error) -> Self {
match error {
Error::Psp22(psp22_error) => psp22_error,
_ => Psp22Error::Custom(String::from("Unknown error")),
}
}
}
}
Loading
Loading