diff --git a/contracts/dao/Cargo.toml b/contracts/dao/Cargo.toml index f5f4bf3..ff8de8c 100644 --- a/contracts/dao/Cargo.toml +++ b/contracts/dao/Cargo.toml @@ -10,7 +10,9 @@ ink = { version = "4.2.1", default-features = false } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } scale-info = { version = "2.5", default-features = false, features = ["derive"], optional = true } -openbrush = { git = "https://github.com/727-Ventures/openbrush-contracts", version = "3.1.1", default-features = false } +#openbrush = { git = "https://github.com/727-Ventures/openbrush-contracts", version = "3.1.1", default-features = false } # 2023.08.06 DEL +openbrush = { tag = "4.0.0-beta", git = "https://github.com/Brushfam/openbrush-contracts", default-features = false, features = ["psp22"] } # 2023.08.06 ADD +gtoken = { path = "../governance-token", default-features = false, features = ["ink-as-dependency"] } # 2023.08.09 ADD [lib] path = "lib.rs" @@ -29,4 +31,4 @@ ink-as-dependency = [] overflow-checks = false [profile.release] -overflow-checks = false \ No newline at end of file +overflow-checks = false diff --git a/contracts/dao/lib.rs b/contracts/dao/lib.rs index 08a2ae0..40e6ab7 100644 --- a/contracts/dao/lib.rs +++ b/contracts/dao/lib.rs @@ -2,6 +2,12 @@ #[ink::contract] pub mod dao { + + const ONE_MINUTE: u64= 60; // ADD + + pub type ProposalId = u32; // ADD + + // Import crate // ADD use ink::storage::Mapping; use openbrush::contracts::traits::psp22::*; use scale::{ @@ -13,12 +19,28 @@ pub mod dao { #[cfg_attr(feature = "std", derive(Debug, PartialEq, Eq, scale_info::TypeInfo))] pub enum VoteType { // to implement + For, // ADD + Against // ADD } #[derive(Copy, Clone, Debug, PartialEq, Eq, Encode, Decode)] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] pub enum GovernorError { // to implement + + // ADD --> + AmountShouldNotBeZero, + DurationError, + QuorumNotReached, + ProposalNotFound, + ProposalAlreadyExecuted, + VotePeriodEnded, + VotePeriodNotEnded, + NotEnoughBalance, + AlreadyVoted, + ProposalNotAccepted, + ProposalVoteNotFound, + // <-- ADD } #[derive(Encode, Decode)] @@ -32,8 +54,17 @@ pub mod dao { ink::storage::traits::StorageLayout ) )] + pub struct Proposal { // to implement + + // ADD --> + to: AccountId, + vote_start: u64, + vote_end: u64, + executed: bool, + amount: u128, + // <-- ADD } #[derive(Encode, Decode, Default)] @@ -47,19 +78,43 @@ pub mod dao { ink::storage::traits::StorageLayout ) )] + pub struct ProposalVote { // to implement + count: u8, // number of votes + for_weight: u128, // weight total for for + against_weight: u128, // weight total for against } #[ink(storage)] pub struct Governor { // to implement + + // ADD --> + governance_token: AccountId, + quorum: u8, + proposal_id: ProposalId, + + // Mapping + proposals: Mapping, + proposal_votes: Mapping, + // <-- ADD } impl Governor { #[ink(constructor, payable)] pub fn new(governance_token: AccountId, quorum: u8) -> Self { - unimplemented!() + //unimplemented!() // DEL + + // ADD --> + Self { + governance_token: governance_token, + quorum: quorum, + proposal_id: 0, + proposals: Mapping::default(), + proposal_votes: Mapping::default(), + } + // <-- ADD } #[ink(message)] @@ -69,7 +124,45 @@ pub mod dao { amount: Balance, duration: u64, ) -> Result<(), GovernorError> { - unimplemented!() + //unimplemented!() // DEL + + // ADD --> + if amount == 0 { + return Err(GovernorError::AmountShouldNotBeZero) + } + + // <-- + + // I have not been able to implement a check that the amount does not exceed + // the total supply of governance tokens. + + // --> + + if duration == 0 { + return Err(GovernorError::DurationError) + } + + let current = self.env().block_timestamp(); + let proposal: Proposal = Proposal { + to: to, + vote_start: current, + vote_end: current + duration * ONE_MINUTE, + executed: false, + amount: amount, + }; + self.proposals.insert(self.proposal_id, &proposal); + + let proposal_vote: ProposalVote = ProposalVote { + count: 0, + for_weight: 0, + against_weight: 0, + }; + self.proposal_votes.insert(self.proposal_id, &proposal_vote); + + self.proposal_id += 1; + + return Ok(()); + // <-- ADD } #[ink(message)] @@ -78,12 +171,114 @@ pub mod dao { proposal_id: ProposalId, vote: VoteType, ) -> Result<(), GovernorError> { - unimplemented!() + // unimplemented!() // DEL + + // ADD --> + let total_supply: u128; + let amount: u128; + let weight: u128; + + if let Err(result) = self.get_proposal(proposal_id) { + return Err(GovernorError::ProposalNotFound) + } + + let proposal = self.get_proposal(proposal_id).unwrap(); + if proposal.executed == true { + return Err(GovernorError::ProposalAlreadyExecuted) + } + + let current = self.env().block_timestamp(); + if proposal.vote_end <= current { + return Err(GovernorError::VotePeriodEnded) + } + + // --> + + // I haven't been able to implement the voted check. + // (GovernorError::AlreadyVoted) + + // <-- + + let caller: AccountId; // Voter's AccountId + caller = self.env().caller(); + + // --> + + // I haven't been able to implement governonce tokens. + // Therefore, it was not possible to obtain the amount of + // governance tokens owned by the caller. + + // <-- + + // DEBUG --> + total_supply = 1000; + amount = 600; + weight = amount * 100 / total_supply; + // <-- DEBUG + + if let Err(result) = self.get_proposal_vote(proposal_id) { + return Err(GovernorError::ProposalVoteNotFound) + } + let mut proposal_vote = self.get_proposal_vote(proposal_id).unwrap(); + proposal_vote.count += 1; + + if vote == VoteType::For { + proposal_vote.for_weight += weight; + } + else { + proposal_vote.against_weight += weight; + } + + self.proposal_votes.insert(proposal_id, &proposal_vote); + + return Ok(()); + // <-- ADD } #[ink(message)] pub fn execute(&mut self, proposal_id: ProposalId) -> Result<(), GovernorError> { - unimplemented!() + // unimplemented!() // DEL + + // ADD --> + if let Err(result) = self.get_proposal(proposal_id) { + return Err(GovernorError::ProposalNotFound) + } + + let mut proposal = self.get_proposal(proposal_id).unwrap(); + if proposal.executed == true { + return Err(GovernorError::ProposalAlreadyExecuted) + } + + if let Err(result) = self.get_proposal_vote(proposal_id) { + return Err(GovernorError::ProposalVoteNotFound) + } + let proposal_vote = self.get_proposal_vote(proposal_id).unwrap(); + if self.quorum > proposal_vote.count { + return Err(GovernorError::QuorumNotReached) + } + + if proposal_vote.for_weight <= proposal_vote.against_weight { + return Err(GovernorError::ProposalNotAccepted) + } + + // --> + + // I added this error handling because I couldn't implement + // automatic execution of execute(). + let current = self.env().block_timestamp(); + if proposal.vote_end <= current { + return Err(GovernorError::VotePeriodNotEnded) + } + + // <-- + + self.env().transfer(proposal.to, proposal.amount); + + proposal.executed = true; + self.proposals.insert(proposal_id, &proposal); + + return Ok(()); + // <-- ADD } // used for test @@ -91,10 +286,50 @@ pub mod dao { pub fn now(&self) -> u64 { self.env().block_timestamp() } + + // ADD --> + #[ink(message)] + pub fn get_proposal(&mut self, proposal_id: ProposalId) -> Result { + + let proposal: Option = self.proposals.get(proposal_id); + match proposal { + Some(proposal) => { + return Ok(proposal); + } + None => { + return Err(GovernorError::ProposalNotFound) + } + } + } + // <-- ADD + + // ADD --> + #[ink(message)] + pub fn get_proposal_vote(&mut self, proposal_id: ProposalId) -> Result { + + let proposal_vote: Option = self.proposal_votes.get(proposal_id); + match proposal_vote { + Some(proposal_vote) => { + return Ok(proposal_vote); + } + None => { + return Err(GovernorError::ProposalVoteNotFound) + } + } + } + // <-- ADD + + // ADD --> + #[ink(message)] + pub fn next_proposal_id(&self) -> u32 { + return self.proposal_id; + } + // <-- ADD } #[cfg(test)] mod tests { + use super::*; fn create_contract(initial_balance: Balance) -> Governor { @@ -127,6 +362,7 @@ pub mod dao { fn propose_works() { let accounts = default_accounts(); let mut governor = create_contract(1000); + assert_eq!( governor.propose(accounts.django, 0, 1), Err(GovernorError::AmountShouldNotBeZero) @@ -137,6 +373,14 @@ pub mod dao { ); let result = governor.propose(accounts.django, 100, 1); assert_eq!(result, Ok(())); + + // ADD --> + assert_eq!( + governor.get_proposal(1), + Err(GovernorError::ProposalNotFound) + ); + // <--ADD + let proposal = governor.get_proposal(0).unwrap(); let now = governor.now(); assert_eq!( @@ -152,6 +396,105 @@ pub mod dao { assert_eq!(governor.next_proposal_id(), 1); } + // ADD --> + #[ink::test] + fn vote_works() { + let accounts = default_accounts(); + let mut governor = create_contract(1000); + let result = governor.propose(accounts.django, 100, 1); + + assert_eq!( + governor.vote(1, VoteType::For), + Err(GovernorError::ProposalNotFound) + ); + + let mut proposal = governor.get_proposal(0).unwrap(); + proposal.executed = true; + governor.proposals.insert(0, &proposal); + assert_eq!( + governor.vote(0, VoteType::For), + Err(GovernorError::ProposalAlreadyExecuted) + ); + + proposal.executed = false; + proposal.vote_end = 0; + governor.proposals.insert(0, &proposal); + assert_eq!( + governor.vote(0, VoteType::For), + Err(GovernorError::VotePeriodEnded) + ); + + proposal.vote_end = 1; + governor.proposals.insert(0, &proposal); + governor.vote(0, VoteType::For); + let proposal_vote = governor.get_proposal_vote(0).unwrap(); + assert_eq!( + proposal_vote, + ProposalVote { + count: 1, + for_weight: 60, + against_weight: 0, + } + ); + + assert_eq!( + governor.get_proposal_vote(1), + Err(GovernorError::ProposalVoteNotFound) + ); + } + // <-- ADD + + // ADD --> + #[ink::test] + fn execute_works() { + let accounts = default_accounts(); + let mut governor = create_contract(1000); + let result = governor.propose(accounts.django, 100, 1); + + assert_eq!( + governor.execute(1), + Err(GovernorError::ProposalNotFound) + ); + + let mut proposal = governor.get_proposal(0).unwrap(); + proposal.executed = true; + governor.proposals.insert(0, &proposal); + assert_eq!( + governor.execute(0), + Err(GovernorError::ProposalAlreadyExecuted) + ); + + proposal.executed = false; + proposal.vote_end = 1; + governor.proposals.insert(0, &proposal); + assert_eq!( + governor.execute(0), + Err(GovernorError::QuorumNotReached) + ); + + proposal.vote_end = 1; + governor.proposals.insert(0, &proposal); + governor.vote(0, VoteType::For); + let mut proposal_vote = governor.get_proposal_vote(0).unwrap(); + proposal_vote.count = 50; + + proposal_vote.for_weight = 0; + proposal_vote.against_weight = 50; + governor.proposal_votes.insert(0, &proposal_vote); + assert_eq!( + governor.execute(0), + Err(GovernorError::ProposalNotAccepted) + ); + + proposal_vote.for_weight = 50; + proposal_vote.against_weight = 0; + governor.proposal_votes.insert(0, &proposal_vote); + governor.proposal_votes.insert(0, &proposal_vote); + let result = governor.execute(0); + assert_eq!(result, Ok(())); + } + // <-- ADD + #[ink::test] fn quorum_not_reached() { let mut governor = create_contract(1000); @@ -161,4 +504,57 @@ pub mod dao { assert_eq!(execute, Err(GovernorError::QuorumNotReached)); } } + + // ADD --> + #[cfg(all(test, feature = "e2e-tests"))] + mod tests { + use openbrush::contracts::psp22::extensions::metadata::psp22metadata_external::PSP22Metadata; + + #[rustfmt::skip] + use super::*; + #[rustfmt::skip] + use ink_e2e::build_message; + + type E2EResult = Result>; + + #[ink_e2e::test] + async fn metadata_works(client: ink_e2e::Client) -> E2EResult<()> { + let _name = String::from("TOKEN"); + let _symbol = String::from("TKN"); + + let constructor = ContractRef::new(1000, Some(_name), Some(_symbol), 18); + + let address = client + .instantiate("my_psp22_metadata", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let token_name = { + let _msg = build_message::(address.clone()).call(|contract| contract.token_name()); + client.call_dry_run(&ink_e2e::alice(), &_msg, 0, None).await + } + .return_value(); + + let token_symbol = { + let _msg = build_message::(address.clone()).call(|contract| contract.token_symbol()); + client.call_dry_run(&ink_e2e::alice(), &_msg, 0, None).await + } + .return_value(); + + let token_decimals = { + let _msg = build_message::(address.clone()).call(|contract| contract.token_decimals()); + + client.call_dry_run(&ink_e2e::alice(), &_msg, 0, None).await + } + .return_value(); + + assert!(matches!(token_name, Some(_name))); + assert!(matches!(token_symbol, Some(_symbol))); + assert!(matches!(token_decimals, 18)); + + Ok(()) + } + } + // <-- ADD } diff --git a/contracts/governance-token/Cargo.toml b/contracts/governance-token/Cargo.toml index c2f348f..f711956 100644 --- a/contracts/governance-token/Cargo.toml +++ b/contracts/governance-token/Cargo.toml @@ -1 +1,34 @@ -# Implement PSP2 + PSP22Metadata \ No newline at end of file +[package] +name = "gtoken" +version = "1.0.0" +edition = "2021" +authors = ["The best developer ever"] + +[dependencies] + +#ink = { version = "4.1.0", default-features = false } # DEL +ink = { version = "4.2.1", default-features = false } # ADD + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.3", default-features = false, features = ["derive"], optional = true } + +# Include brush as a dependency and enable default implementation for PSP22 via brush feature +#openbrush = { tag = "3.1.0", git = "https://github.com/727-Ventures/openbrush-contracts", default-features = false, features = ["psp22", "ownable"] } # DEL +openbrush = { tag = "4.0.0-beta", git = "https://github.com/Brushfam/openbrush-contracts", default-features = false, features = ["psp22", "ownable"] } # ADD + +[lib] +path = "lib.rs" +crate-type = [ + "rlib", +] + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", + + "openbrush/std", +] +ink-as-dependency = [] diff --git a/contracts/governance-token/lib.rs b/contracts/governance-token/lib.rs index 76544f5..f13abbb 100644 --- a/contracts/governance-token/lib.rs +++ b/contracts/governance-token/lib.rs @@ -1 +1,81 @@ -// Implement PSP2 + PSP22Metadata \ No newline at end of file +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[openbrush::implementation(PSP22, PSP22Metadata)] +#[openbrush::contract] +pub mod my_psp22 { + use openbrush::traits::Storage; + + #[ink(storage)] + #[derive(Default, Storage)] + pub struct Contract { + #[storage_field] + psp22: psp22::Data, + #[storage_field] + metadata: metadata::Data, + } + + impl Contract { + #[ink(constructor)] + pub fn new(total_supply: Balance, name: Option, symbol: Option, decimal: u8) -> Self { + let mut instance = Self::default(); + let caller = instance.env().caller(); + + instance.metadata.name.set(&name); + instance.metadata.symbol.set(&symbol); + instance.metadata.decimals.set(&decimal); + + psp22::Internal::_mint_to(&mut instance, caller, total_supply).expect("Should mint total_supply"); + + instance + } + } + + #[cfg(all(test, feature = "e2e-tests"))] + pub mod tests { + use openbrush::contracts::psp22::extensions::metadata::psp22metadata_external::PSP22Metadata; + + #[rustfmt::skip] + use super::*; + #[rustfmt::skip] + use ink_e2e::build_message; + + type E2EResult = Result>; + + #[ink_e2e::test] + async fn metadata_works(client: ink_e2e::Client) -> E2EResult<()> { + let _name = String::from("TOKEN"); + let _symbol = String::from("TKN"); + + let constructor = ContractRef::new(1000, Some(_name), Some(_symbol), 18); + let address = client + .instantiate("my_psp22_metadata", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let token_name = { + let _msg = build_message::(address.clone()).call(|contract| contract.token_name()); + client.call_dry_run(&ink_e2e::alice(), &_msg, 0, None).await + } + .return_value(); + + let token_symbol = { + let _msg = build_message::(address.clone()).call(|contract| contract.token_symbol()); + client.call_dry_run(&ink_e2e::alice(), &_msg, 0, None).await + } + .return_value(); + + let token_decimals = { + let _msg = build_message::(address.clone()).call(|contract| contract.token_decimals()); + client.call_dry_run(&ink_e2e::alice(), &_msg, 0, None).await + } + .return_value(); + + assert!(matches!(token_name, Some(_name))); + assert!(matches!(token_symbol, Some(_symbol))); + assert!(matches!(token_decimals, 18)); + + Ok(()) + } + } +}