Skip to content

Commit 060b000

Browse files
committedSep 17, 2024·
add split teams and revote functionality. Still need tests
gate approval with nft combine create_shortlist and split_shortlist update project list collection and add tests
1 parent 12dddc1 commit 060b000

File tree

4 files changed

+427
-52
lines changed

4 files changed

+427
-52
lines changed
 

‎contracts/voting/Move.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ dependencies = [
2121
]
2222

2323
[move.toolchain-version]
24-
compiler-version = "1.33.0"
24+
compiler-version = "1.33.1"
2525
edition = "2024.beta"
2626
flavor = "sui"
2727

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
module voting::approval;
2+
3+
use voting::voting::{Votes, Project, AdminCap, new_votes, vote_internal,
4+
assert_token_in_whitelist, assert_valid_project_ids, assert_voting_is_active};
5+
use sui::random::{Random};
6+
use sui::table;
7+
use std::debug;
8+
9+
public struct ProjectWithAddress has drop {
10+
project: Project,
11+
address: address,
12+
}
13+
14+
public struct TeamOrca has key { id: UID }
15+
public struct TeamPolarBear has key { id: UID }
16+
17+
fun init(_: &mut TxContext) {}
18+
19+
// AdminCap is required to create a shortlist
20+
entry fun create_shortlist(_: &AdminCap, votes: &Votes, project_ids: vector<u64>, addresses: vector<address>, r: &Random, ctx: &mut TxContext) {
21+
let mut projects = project_ids.zip_map!(addresses, |i, a| {
22+
let p = votes.project_list(i);
23+
ProjectWithAddress {
24+
project: p,
25+
address: a,
26+
}
27+
});
28+
29+
// Split shortlist into 2 teams
30+
let mut rng = r.new_generator(ctx);
31+
rng.shuffle(&mut projects);
32+
33+
let mut team_orca = new_votes<TeamOrca>(
34+
0,
35+
table::new(ctx),
36+
table::new(ctx),
37+
false,
38+
ctx
39+
);
40+
let mut team_polar_bear = new_votes<TeamPolarBear>(
41+
0,
42+
table::new(ctx),
43+
table::new(ctx),
44+
false,
45+
ctx
46+
);
47+
48+
// divide the projects into 2 teams
49+
// if the number of projects is odd, the first team will have 1 more project
50+
let n = projects.length();
51+
debug::print(&b"project length".to_string());
52+
debug::print(&n);
53+
let n1 = n / 2;
54+
55+
vector::tabulate!(n, |i| {
56+
let p = projects.pop_back();
57+
if (i <= n1) {
58+
team_orca.append_project_list(p.project);
59+
transfer::transfer(TeamPolarBear{id: object::new(ctx)}, p.address);
60+
debug::print(&b"TeamPolarBear".to_string());
61+
debug::print(&p.address);
62+
} else {
63+
team_polar_bear.append_project_list(p.project);
64+
transfer::transfer(TeamOrca{id: object::new(ctx)}, p.address);
65+
debug::print(&b"TeamOrca".to_string());
66+
debug::print(&p.address);
67+
};
68+
true
69+
});
70+
71+
team_orca.share_votes();
72+
team_polar_bear.share_votes();
73+
projects.destroy_empty();
74+
}
75+
76+
public fun approve<T>(nft: &T, ballot: vector<u64>, v: &mut Votes, ctx: &TxContext) {
77+
assert_voting_is_active(v);
78+
assert_token_in_whitelist(nft, v);
79+
assert_valid_project_ids(ballot, v);
80+
81+
vote_internal(v, ballot, ctx);
82+
}

‎contracts/voting/sources/voting.move

+102-37
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,64 @@ module voting::voting {
66
use sui::vec_map;
77
use sui::url;
88
use sui::zklogin_verified_issuer::check_zklogin_issuer;
9+
use sui::vec_set::{Self,VecSet};
10+
use std::type_name::{Self, TypeName};
911

1012
const EInvalidProof: u64 = 1;
1113
const EUserAlreadyVoted: u64 = 2;
12-
const ETooManyVotes: u64 = 3;
1314
const EInvalidProjectId: u64 = 4;
1415
const EVotingInactive: u64 = 5;
16+
const ENotInWhitelist: u64 = 6;
1517

1618
public struct Votes has key {
1719
id: UID,
1820
total_votes: u64,
19-
project_list: vector<Project>,
20-
votes: table::Table<address, vector<u64>>,
21-
voting_active: bool
21+
project_list: table::Table<u64, Project>,
22+
ballots: table::Table<address, vector<u64>>,
23+
voting_active: bool,
24+
whitelist_tokens: VecSet<TypeName>,
2225
}
2326

24-
public struct Project has store {
27+
public(package) fun project_list(self: &Votes, i: u64): Project {
28+
return self.project_list[i]
29+
}
30+
31+
public(package) fun append_project_list(self: &mut Votes, p: Project) {
32+
self.project_list.add(p.id, p);
33+
}
34+
35+
public(package) fun share_votes(self: Votes) {
36+
transfer::share_object(self);
37+
}
38+
39+
public fun total_votes(self: &Votes): u64 {
40+
self.total_votes
41+
}
42+
43+
public fun ballots(self: &Votes, user: address): vector<u64> {
44+
*self.ballots.borrow(user)
45+
}
46+
47+
public(package) fun new_votes<T>(
48+
total_votes: u64,
49+
project_list: table::Table<u64, Project>,
50+
ballots: table::Table<address, vector<u64>>,
51+
voting_active: bool,
52+
ctx: &mut TxContext
53+
): Votes {
54+
let tn = type_name::get<T>();
55+
return Votes {
56+
id: object::new(ctx),
57+
total_votes,
58+
project_list,
59+
// project_ids: vec_set::from_keys(project_list.map!(|p| p.id)),
60+
ballots,
61+
voting_active: voting_active,
62+
whitelist_tokens: vec_set::singleton(tn),
63+
}
64+
}
65+
66+
public struct Project has store, copy, drop {
2567
id: u64,
2668
name: string::String,
2769
description: string::String,
@@ -31,6 +73,10 @@ module voting::voting {
3173
votes: u64
3274
}
3375

76+
public fun project_votes(self: &Project): u64 {
77+
self.votes
78+
}
79+
3480
public struct AdminCap has key, store {
3581
id: UID
3682
}
@@ -520,12 +566,12 @@ module voting::voting {
520566
],
521567
];
522568

523-
let mut project_list = vector[];
569+
let mut project_list = table::new(ctx);
524570

525571
let mut index = 0;
526572

527573
while (index < projects.length()) {
528-
project_list.push_back(Project {
574+
project_list.add(index, Project {
529575
id: index,
530576
votes: 0,
531577
name: projects[index][0].to_string(),
@@ -542,8 +588,9 @@ module voting::voting {
542588
id: object::new(ctx),
543589
total_votes: 0,
544590
project_list,
545-
votes: table::new(ctx),
546-
voting_active: false
591+
ballots: table::new(ctx),
592+
voting_active: false,
593+
whitelist_tokens: vec_set::empty()
547594
};
548595
transfer::share_object(votes);
549596

@@ -554,9 +601,8 @@ module voting::voting {
554601
ctx.sender()
555602
);
556603
}
557-
604+
558605
public fun vote(project_ids: vector<u64>, votes: &mut Votes, address_seed: u256, ctx: &TxContext) {
559-
560606
let voter = ctx.sender();
561607

562608
assert_user_has_not_voted(voter, votes);
@@ -565,58 +611,72 @@ module voting::voting {
565611
assert_voting_is_active(votes);
566612

567613
// Update project's vote
568-
let mut curr_index = 0;
569-
while (curr_index < project_ids.length()) {
570-
let project = &mut votes.project_list[project_ids[curr_index]];
571-
project.votes = project.votes + 1;
572-
573-
// Increment total votes
574-
votes.total_votes = votes.total_votes + 1;
575-
576-
curr_index = curr_index + 1;
577-
};
614+
vote_internal(votes, project_ids, ctx);
578615

579616
// Record user's vote
580617
table::add(
581-
&mut votes.votes,
618+
&mut votes.ballots,
582619
voter,
583620
project_ids
584621
);
585622
}
586623

624+
public(package) fun vote_internal(votes: &mut Votes, ballot: vector<u64>, ctx: &TxContext) {
625+
let voter = ctx.sender();
626+
let already_voted = votes.ballots.contains(voter);
627+
// Clean up old ballot
628+
if (already_voted) {
629+
let og_ballot = votes.ballots[voter];
630+
og_ballot.do!(|v| {
631+
let p = &mut votes.project_list[v];
632+
p.votes = p.votes - 1;
633+
votes.total_votes = votes.total_votes - 1;
634+
});
635+
votes.ballots.remove(voter);
636+
};
637+
// add new ballot
638+
votes.ballots.add(voter, ballot);
639+
ballot.do!(|v| {
640+
let p = &mut votes.project_list[v];
641+
p.votes = p.votes + 1;
642+
votes.total_votes = votes.total_votes + 1;
643+
});
644+
}
645+
587646
public entry fun toggle_voting(_: &AdminCap, can_vote: bool, votes: &mut Votes) {
588647
votes.voting_active = can_vote;
589648
}
590649

650+
public(package) fun assert_token_in_whitelist<T>(_: &T, votes: &Votes) {
651+
let tn = type_name::get<T>();
652+
assert!(
653+
votes.whitelist_tokens.contains(&tn),
654+
ENotInWhitelist
655+
);
656+
}
657+
591658
fun assert_user_has_not_voted(user: address, votes: &Votes) {
592659
assert!(
593660
table::contains(
594-
&votes.votes,
661+
&votes.ballots,
595662
user
596663
) == false,
597664
EUserAlreadyVoted
598665
);
599666
}
600667

601-
fun assert_valid_project_ids(project_ids: vector<u64>, votes: &Votes) {
602-
// assert!(
603-
// project_ids.length() <= 3,
604-
// ETooManyVotes
605-
// );
606-
607-
let mut curr_index = 0;
668+
public(package) fun assert_valid_project_ids(project_ids: vector<u64>, votes: &Votes) {
608669
let mut ids = vec_map::empty();
609-
while (curr_index < project_ids.length()) {
670+
project_ids.do!(|id| {
610671
assert!(
611-
project_ids[curr_index] < votes.project_list.length(),
612-
EInvalidProjectId
672+
votes.project_list.contains(id),
673+
EInvalidProjectId
613674
);
614-
vec_map::insert(&mut ids, project_ids[curr_index], 0); // this will abort if there is a dup
615-
curr_index = curr_index + 1;
616-
};
675+
vec_map::insert(&mut ids, id, 0); // this will abort if there is a dup
676+
});
617677
}
618678

619-
fun assert_voting_is_active(votes: &Votes) {
679+
public(package) fun assert_voting_is_active(votes: &Votes) {
620680
assert!(
621681
votes.voting_active,
622682
EVotingInactive
@@ -628,4 +688,9 @@ module voting::voting {
628688
let issuer = string::utf8(b"https://accounts.google.com");
629689
assert!(check_zklogin_issuer(sender, address_seed, &issuer), EInvalidProof);
630690
}
691+
692+
#[test_only]
693+
public fun init_for_test(ctx: &mut TxContext) {
694+
init(ctx);
695+
}
631696
}

‎contracts/voting/tests/voting_tests.move

+242-14
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,247 @@
1-
/*
21
#[test_only]
3-
module voting::voting_tests {
4-
// uncomment this line to import the module
5-
// use voting::voting;
2+
module voting::voting_tests;
3+
// uncomment this line to import the module
4+
use voting::voting::{Self, AdminCap, Votes, toggle_voting, ENotInWhitelist, EInvalidProjectId, EVotingInactive};
5+
use voting::approval::{create_shortlist, TeamOrca, approve};
6+
use sui::test_scenario::{Self as ts, Scenario};
7+
use sui::random::{Self,Random};
8+
use std::debug;
69

7-
const ENotImplemented: u64 = 0;
10+
const ORGANIZER: address = @0xAAA;
11+
const Voter1: address = @0x111;
12+
const Voter2: address = @0x222;
13+
const Voter3: address = @0x333;
14+
const Voter4: address = @0x444;
15+
const Voter5: address = @0x555;
816

9-
#[test]
10-
fun test_voting() {
11-
// pass
12-
}
17+
#[test]
18+
fun initialize(): Scenario {
19+
let mut scenario = ts::begin(ORGANIZER);
20+
voting::init_for_test(scenario.ctx());
1321

14-
#[test, expected_failure(abort_code = ::voting::voting_tests::ENotImplemented)]
15-
fun test_voting_fail() {
16-
abort ENotImplemented
17-
}
22+
ts::next_tx(&mut scenario, @0x0);
23+
{
24+
random::create_for_testing(scenario.ctx());
25+
};
26+
27+
ts::next_tx(&mut scenario, @0x0);
28+
let mut random_state: Random = scenario.take_shared();
29+
{
30+
random_state.update_randomness_state_for_testing(
31+
0,
32+
x"1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1FFF",
33+
scenario.ctx(),
34+
);
35+
ts::return_shared(random_state);
36+
};
37+
ts::next_tx(&mut scenario, ORGANIZER);
38+
{
39+
let random_state: Random = scenario.take_shared<Random>();
40+
let cap = scenario.take_from_sender<AdminCap>();
41+
let og_votes = scenario.take_shared<Votes>();
42+
let shortlisted_projects = vector[1, 2, 3, 4, 5];
43+
let whitelist_addresses = vector[Voter1, Voter2, Voter3, Voter4, Voter5];
44+
// Guaranteed to split the team the same way due to hardcoded randomness
45+
// TeamPolarBear: @0x222, @0x555, @0x444
46+
// TeamOrca: @0x111, @0x333
47+
create_shortlist(&cap, &og_votes, shortlisted_projects, whitelist_addresses, &random_state, scenario.ctx());
48+
ts::return_shared(og_votes);
49+
ts::return_shared(random_state);
50+
ts::return_to_sender(&scenario, cap);
51+
};
52+
scenario
53+
}
54+
55+
#[test]
56+
fun happy_path(): Scenario {
57+
let mut scenario = initialize();
58+
59+
let effects = ts::next_tx(&mut scenario, ORGANIZER);
60+
let approval_votes = ts::shared(&effects);
61+
{
62+
let mut team_polar_bear_projects = scenario.take_shared_by_id<Votes>(approval_votes[0]);
63+
let mut team_orca_projects = scenario.take_shared_by_id<Votes>(approval_votes[1]);
64+
let cap = scenario.take_from_sender<AdminCap>();
65+
toggle_voting(&cap, true, &mut team_polar_bear_projects);
66+
toggle_voting(&cap, true, &mut team_orca_projects);
67+
ts::return_shared(team_polar_bear_projects);
68+
ts::return_shared(team_orca_projects);
69+
ts::return_to_sender(&scenario, cap);
70+
};
71+
72+
73+
// Team Orca member votes for project 4 and 5
74+
ts::next_tx(&mut scenario, Voter1);
75+
{
76+
let mut votes = scenario.take_shared_by_id<Votes>(approval_votes[0]);
77+
let orca = scenario.take_from_sender<TeamOrca>();
78+
debug::print(&votes);
79+
80+
approve<TeamOrca>(&orca, vector[4, 5], &mut votes, scenario.ctx());
81+
82+
ts::return_shared(votes);
83+
ts::return_to_sender(&scenario, orca);
84+
};
85+
ts::next_tx(&mut scenario, Voter3);
86+
{
87+
let mut votes = scenario.take_shared_by_id<Votes>(approval_votes[0]);
88+
let orca = scenario.take_from_sender<TeamOrca>();
89+
debug::print(&votes);
90+
91+
approve<TeamOrca>(&orca, vector[4], &mut votes, scenario.ctx());
92+
93+
ts::return_shared(votes);
94+
ts::return_to_sender(&scenario, orca);
95+
};
96+
// Revote #1
97+
ts::next_tx(&mut scenario, Voter1);
98+
{
99+
let mut votes = scenario.take_shared_by_id<Votes>(approval_votes[0]);
100+
let orca = scenario.take_from_sender<TeamOrca>();
101+
debug::print(&votes);
102+
103+
approve<TeamOrca>(&orca, vector[5, 4], &mut votes, scenario.ctx());
104+
105+
ts::return_shared(votes);
106+
ts::return_to_sender(&scenario, orca);
107+
};
108+
109+
// Assert first vote
110+
ts::next_tx(&mut scenario, Voter1);
111+
{
112+
let votes = scenario.take_shared_by_id<Votes>(approval_votes[0]);
113+
debug::print(&votes);
114+
assert!(votes.project_list(4).project_votes() == 2);
115+
assert!(votes.project_list(5).project_votes() == 1);
116+
assert!(votes.total_votes() == 3);
117+
assert!(votes.ballots(Voter1) == vector[5, 4]);
118+
assert!(votes.ballots(Voter3) == vector[4]);
119+
ts::return_shared(votes);
120+
};
121+
122+
// Revote #2
123+
ts::next_tx(&mut scenario, Voter1);
124+
{
125+
let mut votes = scenario.take_shared_by_id<Votes>(approval_votes[0]);
126+
let orca = scenario.take_from_sender<TeamOrca>();
127+
debug::print(&votes);
128+
129+
// Team Orca member votes for project 4 and 5
130+
approve<TeamOrca>(&orca, vector[2], &mut votes, scenario.ctx());
131+
132+
ts::return_shared(votes);
133+
ts::return_to_sender(&scenario, orca);
134+
};
135+
// Assert revote
136+
ts::next_tx(&mut scenario, Voter1);
137+
{
138+
let votes = scenario.take_shared_by_id<Votes>(approval_votes[0]);
139+
debug::print(&votes);
140+
assert!(votes.project_list(2).project_votes() == 1);
141+
assert!(votes.project_list(4).project_votes() == 1);
142+
assert!(votes.project_list(5).project_votes() == 0);
143+
assert!(votes.total_votes() == 2);
144+
assert!(votes.ballots(Voter1) == vector[2]);
145+
assert!(votes.ballots(Voter3) == vector[4]);
146+
ts::return_shared(votes);
147+
};
148+
149+
scenario
150+
}
151+
152+
#[test]
153+
#[expected_failure(abort_code = EVotingInactive)]
154+
fun voting_not_active_error(): Scenario {
155+
let mut scenario = initialize();
156+
157+
let effects = ts::next_tx(&mut scenario, ORGANIZER);
158+
let approval_votes = ts::shared(&effects);
159+
let team_polar_bear_id = approval_votes[0];
160+
161+
// Team Orca member votes for project 4 and 5
162+
ts::next_tx(&mut scenario, Voter1);
163+
{
164+
let mut votes = scenario.take_shared_by_id<Votes>(team_polar_bear_id);
165+
let orca = scenario.take_from_sender<TeamOrca>();
166+
debug::print(&votes);
167+
168+
approve<TeamOrca>(&orca, vector[4, 5], &mut votes, scenario.ctx());
169+
170+
ts::return_shared(votes);
171+
ts::return_to_sender(&scenario, orca);
172+
};
173+
174+
scenario
175+
}
176+
177+
#[test]
178+
#[expected_failure(abort_code = ENotInWhitelist)]
179+
fun not_in_whitelist_error(): Scenario {
180+
let mut scenario = initialize();
181+
182+
let effects = ts::next_tx(&mut scenario, ORGANIZER);
183+
let approval_votes = ts::shared(&effects);
184+
let team_polar_bear_id = approval_votes[0];
185+
let team_orca_id = approval_votes[1];
186+
{
187+
let mut team_polar_bear_projects = scenario.take_shared_by_id<Votes>(team_polar_bear_id);
188+
let mut team_orca_projects = scenario.take_shared_by_id<Votes>(team_orca_id);
189+
let cap = scenario.take_from_sender<AdminCap>();
190+
toggle_voting(&cap, true, &mut team_polar_bear_projects);
191+
toggle_voting(&cap, true, &mut team_orca_projects);
192+
ts::return_shared(team_polar_bear_projects);
193+
ts::return_shared(team_orca_projects);
194+
ts::return_to_sender(&scenario, cap);
195+
};
196+
197+
// Team Orca member votes for Team Orca projects
198+
ts::next_tx(&mut scenario, Voter1);
199+
{
200+
let mut votes = scenario.take_shared_by_id<Votes>(team_orca_id);
201+
let orca = scenario.take_from_sender<TeamOrca>();
202+
debug::print(&votes);
203+
204+
approve<TeamOrca>(&orca, vector[3], &mut votes, scenario.ctx());
205+
206+
ts::return_shared(votes);
207+
ts::return_to_sender(&scenario, orca);
208+
};
209+
210+
scenario
211+
}
212+
213+
#[test]
214+
#[expected_failure(abort_code = EInvalidProjectId)]
215+
fun invalid_project_id_error(): Scenario {
216+
let mut scenario = initialize();
217+
218+
let effects = ts::next_tx(&mut scenario, ORGANIZER);
219+
let approval_votes = ts::shared(&effects);
220+
let team_polar_bear_id = approval_votes[0];
221+
let team_orca_id = approval_votes[1];
222+
{
223+
let mut team_polar_bear_projects = scenario.take_shared_by_id<Votes>(team_polar_bear_id);
224+
let mut team_orca_projects = scenario.take_shared_by_id<Votes>(team_orca_id);
225+
let cap = scenario.take_from_sender<AdminCap>();
226+
toggle_voting(&cap, true, &mut team_polar_bear_projects);
227+
toggle_voting(&cap, true, &mut team_orca_projects);
228+
ts::return_shared(team_polar_bear_projects);
229+
ts::return_shared(team_orca_projects);
230+
ts::return_to_sender(&scenario, cap);
231+
};
232+
233+
// Team Orca member tries to vote for project 1
234+
ts::next_tx(&mut scenario, Voter1);
235+
{
236+
let mut votes = scenario.take_shared_by_id<Votes>(team_polar_bear_id);
237+
let orca = scenario.take_from_sender<TeamOrca>();
238+
debug::print(&votes);
239+
240+
approve<TeamOrca>(&orca, vector[1], &mut votes, scenario.ctx());
241+
242+
ts::return_shared(votes);
243+
ts::return_to_sender(&scenario, orca);
244+
};
245+
246+
scenario
18247
}
19-
*/

0 commit comments

Comments
 (0)
Please sign in to comment.