Skip to content

Commit

Permalink
Discussion with onchain data #76 (#101)
Browse files Browse the repository at this point in the history
* Add discussion component

* check if proposal is live

* Implement check CRAM token holder functionality

* Refactor get_comment to return array of tuples

* Refactor: change panic to assert

* Add unit test for discussion/add_comment

* Add test for discussion/get_comment

* Refac

* Refac: get_comments to return an array of comments

* Polish

* Format with scarb fmt

* bug fix

* Fix camelCase usage

* Refac: test_get_comments

* refac: clone ipfs_hash variables and ipfs hash in comment response

* Polish

---------

Co-authored-by: Ondřej Sojka <[email protected]>
  • Loading branch information
manlikeHB and tensojka committed Jun 8, 2024
1 parent 0e2230a commit 98e72e2
Show file tree
Hide file tree
Showing 7 changed files with 290 additions and 5 deletions.
12 changes: 10 additions & 2 deletions src/contract.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ mod Governance {
use konoha::upgrades::upgrades as upgrades_component;
use konoha::airdrop::airdrop as airdrop_component;
use konoha::vesting::vesting as vesting_component;
use konoha::discussion::discussion as discussion_component;

use starknet::ContractAddress;

Expand All @@ -43,6 +44,7 @@ mod Governance {
component!(path: vesting_component, storage: vesting, event: VestingEvent);
component!(path: proposals_component, storage: proposals, event: ProposalsEvent);
component!(path: upgrades_component, storage: upgrades, event: UpgradesEvent);
component!(path: discussion_component, storage: discussions, event: DiscussionEvent);

#[abi(embed_v0)]
impl Airdrop = airdrop_component::AirdropImpl<ContractState>;
Expand All @@ -55,6 +57,9 @@ mod Governance {
#[abi(embed_v0)]
impl Upgrades = upgrades_component::UpgradesImpl<ContractState>;

#[abi(embed_v0)]
impl Discussions = discussion_component::DiscussionImpl<ContractState>;

#[storage]
struct Storage {
proposal_initializer_run: LegacyMap::<u64, bool>,
Expand All @@ -66,7 +71,9 @@ mod Governance {
#[substorage(v0)]
proposals: proposals_component::Storage,
#[substorage(v0)]
upgrades: upgrades_component::Storage
upgrades: upgrades_component::Storage,
#[substorage(v0)]
discussions: discussion_component::Storage
}

// PROPOSALS
Expand All @@ -93,7 +100,8 @@ mod Governance {
AirdropEvent: airdrop_component::Event,
VestingEvent: vesting_component::Event,
ProposalsEvent: proposals_component::Event,
UpgradesEvent: upgrades_component::Event
UpgradesEvent: upgrades_component::Event,
DiscussionEvent: discussion_component::Event,
}

#[constructor]
Expand Down
125 changes: 125 additions & 0 deletions src/discussion.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use starknet::ContractAddress;
use konoha::types::Comment;

#[starknet::interface]
trait IDiscussion<TContractState> {
fn add_comment(ref self: TContractState, prop_id: u32, ipfs_hash: ByteArray);
fn get_comments(self: @TContractState, prop_id: u32) -> Array<Comment>;
}

#[starknet::component]
mod discussion {
use array::ArrayTrait;
use core::box::Box;
use core::serde::Serde;

use starknet::get_caller_address;
use starknet::ContractAddress;

use konoha::proposals::proposals as proposals_component;
use konoha::proposals::proposals::ProposalsImpl;
use konoha::traits::IERC20Dispatcher;
use konoha::traits::IERC20DispatcherTrait;
use konoha::traits::get_governance_token_address_self;
use konoha::types::Comment;

#[storage]
struct Storage {
// mapping of (proposal id, index) to comment
comments: LegacyMap::<(u32, u64), Comment>,
// mapping of proposal id to number of comments
comment_count: LegacyMap::<u32, u64>
}

#[event]
#[derive(Drop, starknet::Event)]
enum Event {}

#[embeddable_as(DiscussionImpl)]
impl Discussions<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl Proposals: proposals_component::HasComponent<TContractState>,
> of super::IDiscussion<ComponentState<TContractState>> {
fn add_comment(
ref self: ComponentState<TContractState>, prop_id: u32, ipfs_hash: ByteArray
) {
//Check if proposal is live
let is_live = self.is_proposal_live(prop_id);
assert(is_live, 'Proposal is not live!');

//Check if caller is a governance token holder
let user_address = get_caller_address();
let govtoken_addr = get_governance_token_address_self();
let caller_balance: u256 = IERC20Dispatcher { contract_address: govtoken_addr }
.balance_of(user_address);
assert(caller_balance.high != 0 || caller_balance.low != 0, 'Govtoken balance is zero');

//get current comment count
let count: u64 = self.comment_count.read(prop_id);

//store new comment/ipfs_hash at next index
let new_comment = Comment { user: user_address, ipfs_hash: ipfs_hash };
self.comments.write((prop_id, count), new_comment);

//Increment comment count for proposal by one
self.comment_count.write(prop_id, count + 1);
}

fn get_comments(self: @ComponentState<TContractState>, prop_id: u32) -> Array<Comment> {
//Get comment counts
let count: u64 = self.comment_count.read(prop_id);

//Initialize an array of comments
let mut arr = ArrayTrait::<Comment>::new();

// loop over comment count and collect comments
let mut i: u64 = 0;
loop {
if i >= count {
break;
}

arr.append(self.comments.read((prop_id, i)));
i += 1;
};

// return array of comments
arr
}
}

#[generate_trait]
impl InternalImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl Proposals: proposals_component::HasComponent<TContractState>,
> of InternalTrait<TContractState> {
fn is_proposal_live(ref self: ComponentState<TContractState>, prop_id: u32) -> bool {
let proposals_comp = get_dep_component!(@self, Proposals);

let live_proposals = proposals_comp.get_live_proposals();

let mut is_live = false;

//loop over the array to check if prop_id is in the array
let mut i = 0;
loop {
if i >= live_proposals.len() {
break;
}

match live_proposals.get(i) {
Option::Some(_prop_id) => {
is_live = true;
break;
},
Option::None => i += 1
}
};
is_live
}
}
}
1 change: 1 addition & 0 deletions src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod treasury_types {
}
mod constants;
mod contract;
mod discussion;
mod merkle_tree;
mod proposals;
mod token;
Expand Down
1 change: 1 addition & 0 deletions src/traits.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ trait IERC20<TContractState> {
fn decimals(self: @TContractState) -> u8;
fn totalSupply(self: @TContractState) -> u256;
fn balanceOf(self: @TContractState, account: ContractAddress) -> u256;
fn balance_of(self: @TContractState, account: ContractAddress) -> u256;
fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256;
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
fn transferFrom(
Expand Down
7 changes: 7 additions & 0 deletions src/types.cairo
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use starknet::SyscallResult;
use starknet::syscalls::{storage_read_syscall, storage_write_syscall, ClassHash};
use starknet::storage_address_from_base_and_offset;
use starknet::ContractAddress;
use core::serde::Serde;

#[derive(Copy, Drop, Serde, starknet::Store)]
Expand All @@ -25,3 +26,9 @@ struct CustomProposalConfig {
selector: felt252,
library_call: bool
}

#[derive(Drop, Serde, starknet::Store)]
struct Comment {
user: ContractAddress,
ipfs_hash: ByteArray,
}
6 changes: 3 additions & 3 deletions tests/lib.cairo
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
mod airdrop_tests;
mod basic;
mod test_treasury;
mod proposals_tests;
mod airdrop_tests;
mod upgrades_tests;
mod setup;
mod test_treasury;
mod upgrades_tests;
143 changes: 143 additions & 0 deletions tests/proposals_tests.cairo
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use array::ArrayTrait;
use core::traits::TryInto;
use core::traits::Into;
use debug::PrintTrait;
use starknet::ContractAddress;
use openzeppelin::token::erc20::interface::{
Expand All @@ -22,6 +23,9 @@ use konoha::proposals::IProposalsDispatcherTrait;
use konoha::upgrades::IUpgradesDispatcher;
use konoha::upgrades::IUpgradesDispatcherTrait;
use konoha::constants;
use konoha::discussion::IDiscussionDispatcher;
use konoha::discussion::IDiscussionDispatcherTrait;

use starknet::get_block_timestamp;


Expand Down Expand Up @@ -418,3 +422,142 @@ fn test_successful_proposal_submission() {
assert!(prop_details_1.payload == 42, "wrong payload first proposal");
assert!(prop_details_2.payload == 43, "wrong payload second proposal");
}


#[test]
#[should_panic(expected: ('Proposal is not live!',))]
fn test_add_comment_on_non_live_proposal() {
let token_contract = deploy_and_distribute_gov_tokens(admin_addr.try_into().unwrap());
let gov_contract = deploy_governance(token_contract.contract_address);
let gov_contract_addr = gov_contract.contract_address;
let ipfs_hash: ByteArray = "QmTFMPrNQiJ6o5dfyMn4PPjbQhDrJ6Mu93qe2yMvgnJYM6";

let dispatcher = IProposalsDispatcher { contract_address: gov_contract_addr };

prank(
CheatTarget::One(gov_contract_addr),
admin_addr.try_into().unwrap(),
CheatSpan::TargetCalls(3)
);
let prop_id = dispatcher.submit_proposal(42, 1);

//simulate passage of time
let current_timestamp = get_block_timestamp();
let end_timestamp = current_timestamp + constants::PROPOSAL_VOTING_SECONDS;
start_warp(CheatTarget::One(gov_contract_addr), end_timestamp + 1);

IDiscussionDispatcher { contract_address: gov_contract_addr }
.add_comment(prop_id.try_into().unwrap(), ipfs_hash);
}

#[test]
#[should_panic(expected: ('Govtoken balance is zero',))]
fn test_add_comment_when_token_balance_is_zero() {
let token_contract = deploy_and_distribute_gov_tokens(admin_addr.try_into().unwrap());
let gov_contract = deploy_governance(token_contract.contract_address);
let gov_contract_addr = gov_contract.contract_address;
let ipfs_hash: ByteArray = "QmTFMPrNQiJ6o5dfyMn4PPjbQhDrJ6Mu93qe2yMvgnJYM6";

let dispatcher = IProposalsDispatcher { contract_address: gov_contract_addr };

prank(
CheatTarget::One(gov_contract_addr),
admin_addr.try_into().unwrap(),
CheatSpan::TargetCalls(1)
);
let prop_id = dispatcher.submit_proposal(42, 1);

prank(
CheatTarget::One(gov_contract_addr),
first_address.try_into().unwrap(),
CheatSpan::TargetCalls(1)
);

IDiscussionDispatcher { contract_address: gov_contract_addr }
.add_comment(prop_id.try_into().unwrap(), ipfs_hash);
}

#[test]
fn test_add_comment() {
let token_contract = deploy_and_distribute_gov_tokens(admin_addr.try_into().unwrap());
let gov_contract = deploy_governance(token_contract.contract_address);
let gov_contract_addr = gov_contract.contract_address;
let ipfs_hash: ByteArray = "QmTFMPrNQiJ6o5dfyMn4PPjbQhDrJ6Mu93qe2yMvgnJYM6";

let dispatcher = IProposalsDispatcher { contract_address: gov_contract_addr };

prank(
CheatTarget::One(gov_contract_addr),
admin_addr.try_into().unwrap(),
CheatSpan::TargetCalls(1)
);
let prop_id = dispatcher.submit_proposal(42, 1);

prank(
CheatTarget::One(token_contract.contract_address),
admin_addr.try_into().unwrap(),
CheatSpan::TargetCalls(1)
);
token_contract.transfer(first_address.try_into().unwrap(), 100000.try_into().unwrap());

prank(
CheatTarget::One(gov_contract_addr),
first_address.try_into().unwrap(),
CheatSpan::TargetCalls(1)
);

IDiscussionDispatcher { contract_address: gov_contract_addr }
.add_comment(prop_id.try_into().unwrap(), ipfs_hash);
}

#[test]
fn test_get_comments() {
let token_contract = deploy_and_distribute_gov_tokens(admin_addr.try_into().unwrap());
let gov_contract = deploy_governance(token_contract.contract_address);
let gov_contract_addr = gov_contract.contract_address;
let ipfs_hash_1: ByteArray = "QmTFMPrNQiJ6o5dfyMn4PPjbQhDrJ6Mu93qe2yMvgnJYM6";
let ipfs_hash_2: ByteArray = "Uinienu2G54J6o5dfyMn4PPjbQhDrJ6Mu93qbhwjni2ijnf";
let ipfs_hash_3: ByteArray = "MPrNQiJbdik6o5dfyMn4Pjnislnenoen7hHSU8Ii82jdB56";

let dispatcher = IProposalsDispatcher { contract_address: gov_contract_addr };

prank(
CheatTarget::One(gov_contract_addr),
admin_addr.try_into().unwrap(),
CheatSpan::TargetCalls(1)
);
let prop_id = dispatcher.submit_proposal(42, 1);

prank(
CheatTarget::One(token_contract.contract_address),
admin_addr.try_into().unwrap(),
CheatSpan::TargetCalls(1)
);
token_contract.transfer(first_address.try_into().unwrap(), 100000.try_into().unwrap());

prank(
CheatTarget::One(gov_contract_addr),
first_address.try_into().unwrap(),
CheatSpan::TargetCalls(7)
);

let discussion_dispatcher = IDiscussionDispatcher { contract_address: gov_contract_addr };

discussion_dispatcher.add_comment(prop_id.try_into().unwrap(), ipfs_hash_1.clone());
discussion_dispatcher.add_comment(prop_id.try_into().unwrap(), ipfs_hash_2.clone());
discussion_dispatcher.add_comment(prop_id.try_into().unwrap(), ipfs_hash_3.clone());

let res = discussion_dispatcher.get_comments(prop_id.try_into().unwrap());

let res_span = res.span();

assert_eq!(*res_span.at(0).user, first_address.try_into().unwrap());
assert_eq!(res_span.at(0).ipfs_hash.clone(), ipfs_hash_1);

assert_eq!(*res_span.at(1).user, first_address.try_into().unwrap());
assert_eq!(res_span.at(1).ipfs_hash.clone(), ipfs_hash_2);

assert_eq!(*res_span.at(2).user, first_address.try_into().unwrap());
assert_eq!(res_span.at(2).ipfs_hash.clone(), ipfs_hash_3);
}

0 comments on commit 98e72e2

Please sign in to comment.