Skip to content

Commit

Permalink
add revoke_vote method (#36)
Browse files Browse the repository at this point in the history
Co-authored-by: Amit Yadav <[email protected]>
Co-authored-by: Robert Zaremba <[email protected]>
  • Loading branch information
3 people authored Aug 16, 2023
1 parent 8f1c14a commit 8430344
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 9 deletions.
13 changes: 9 additions & 4 deletions elections/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
- Only the authority (set during contract initialization) can create proposals. Each proposal specifies:

- `typ`: must be HouseType variant
- `start`: voting start time as UNIX time (in seconds)
- `end`: voting start time as UNIX time (in seconds)
- `start`: voting start time as UNIX time (in miliseconds)
- `end`: voting start time as UNIX time (in miliseconds)
- `cooldown`: cooldown duration when votes from blacklisted accounts can be revoked by an authority (in miliseconds)
- `ref_link`: string (can't be empty) - a link to external resource with more details (eg near social post). Max length is 120 characters.
- `quorum`: minimum amount of legit accounts to vote to legitimize the elections.
- `seats`: max number of candidates to elect, also max number of credits each user has when casting a vote.
Expand All @@ -21,6 +22,7 @@
- Once the proposals are created and the elections start (`now >= proposal.start`), all human verified near accounts can vote according to the NDC Elections [v1 Framework](../README.md#elections).
- Anyone can query the proposal and the ongoing result at any time.
- Voting is active until the `proposal.end` time.
- Vote revocation is active until the `proposal.end` + `cooldown` time.

## Usage

Expand All @@ -31,9 +33,9 @@ CTR=elections-v1.gwg.testnet
REGISTRY=registry-1.i-am-human.testnet

# create proposal
# note: start and end time must be in milliseconds
# note: start time, end time and cooldown must be in milliseconds

near call $CTR create_proposal '{"start": 1686221747000, "end": 1686653747000, "ref_link": "example.com", "quorum": 10, "candidates": ["candidate1.testnet", "candidate2.testnet", "candidate3.testnet", "candidate4.testnet"], "typ": "HouseOfMerit", "seats": 3, "policy": "f1c09f8686fe7d0d798517111a66675da0012d8ad1693a47e0e2a7d3ae1c69d4"}' --accountId $CTR
near call $CTR create_proposal '{"start": 1686221747000, "end": 1686653747000, "cooldown": 604800000 "ref_link": "example.com", "quorum": 10, "candidates": ["candidate1.testnet", "candidate2.testnet", "candidate3.testnet", "candidate4.testnet"], "typ": "HouseOfMerit", "seats": 3, "policy": "f1c09f8686fe7d0d798517111a66675da0012d8ad1693a47e0e2a7d3ae1c69d4"}' --accountId $CTR

# fetch all proposal
near view $CTR proposals ''
Expand All @@ -49,6 +51,9 @@ near call $CTR accepted_policy '{"user": "alice.testnet"}' --accountId me.testne

# vote
near call $CTR vote '{"prop_id": 1, "vote": ["candidate1.testnet", "candidate3.testnet"]}' --gas 70000000000000 --deposit 0.0005 --accountId me.testnet

# revoke vote (authority only)
near call $CTR revoke_vote '{"prop_id": 1, "token_id": 1}'
```

## Deployed Contracts
Expand Down
8 changes: 8 additions & 0 deletions elections/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ pub enum VoteError {
NoSBTs,
DuplicateCandidate,
DoubleVote(TokenId),
RevokeNotActive,
NotVoted,
DoubleRevoke,
}

impl FunctionError for VoteError {
Expand All @@ -24,6 +27,11 @@ impl FunctionError for VoteError {
VoteError::DoubleVote(sbt) => {
panic_str(&format!("user already voted with sbt={}", sbt))
}
VoteError::RevokeNotActive => panic_str(
"can only revoke votes between proposal start and (end time + cooldown)"
),
VoteError::NotVoted => panic_str("voter did not vote on this proposal"),
VoteError::DoubleRevoke => panic_str("vote already revoked"),
}
}
}
52 changes: 52 additions & 0 deletions elections/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ impl Contract {
typ: HouseType,
start: u64,
end: u64,
cooldown: u64,
ref_link: String,
quorum: u32,
seats: u16,
Expand Down Expand Up @@ -90,13 +91,15 @@ impl Contract {
typ,
start,
end,
cooldown,
quorum,
ref_link,
seats,
candidates,
result: vec![0; l],
voters: LookupSet::new(StorageKey::ProposalVoters(self.prop_counter)),
voters_num: 0,
voters_candidates: LookupMap::new(StorageKey::VotersCandidates(self.prop_counter)),
policy,
};

Expand Down Expand Up @@ -153,6 +156,18 @@ impl Contract {
)
}

/// Method for the authority to revoke votes from blacklisted accounts.
/// Panics if the proposal doesn't exists or the it's called before the proposal starts or after proposal `end+cooldown`.
#[handle_result]
pub fn revoke_vote(&mut self, prop_id: u32, token_id: TokenId) -> Result<(), VoteError> {
// check if the caller is the authority allowed to revoke votes
self.assert_admin();
let mut p = self._proposal(prop_id);
p.revoke_votes(token_id)?;
self.proposals.insert(&prop_id, &p);
Ok(())
}

/*****************
* QUERIES
****************/
Expand Down Expand Up @@ -280,6 +295,7 @@ mod unit_tests {
crate::HouseType::HouseOfMerit,
START - 1,
START + 100,
100,
String::from("ref_link.io"),
2,
2,
Expand All @@ -297,6 +313,7 @@ mod unit_tests {
crate::HouseType::HouseOfMerit,
START + 10,
START,
100,
String::from("ref_link.io"),
2,
2,
Expand All @@ -314,6 +331,7 @@ mod unit_tests {
crate::HouseType::HouseOfMerit,
START + 1,
START + 10,
100,
String::from("short"),
2,
2,
Expand All @@ -331,6 +349,7 @@ mod unit_tests {
crate::HouseType::HouseOfMerit,
START + 1,
START + 10,
100,
String::from("ref_link.io"),
2,
2,
Expand All @@ -344,6 +363,7 @@ mod unit_tests {
crate::HouseType::HouseOfMerit,
START + 1,
START + 10,
100,
String::from("ref_link.io"),
2,
2,
Expand Down Expand Up @@ -648,4 +668,36 @@ mod unit_tests {
ctr.vote(prop_id, vec![]);
// note: we can only check vote result and state change through an integration test.
}

#[test]
#[should_panic(expected = "not an admin")]
fn revoke_vote_not_admin() {
let (_, mut ctr) = setup(&alice());
let prop_id = mk_proposal(&mut ctr);
let res = ctr.revoke_vote(prop_id, 1);
// this will never be checked since the method is panicing not returning an error
assert!(res.is_err());
}

#[test]
fn revoke_vote_no_votes() {
let (mut ctx, mut ctr) = setup(&admin());
let prop_id = mk_proposal(&mut ctr);
ctx.block_timestamp = (START + 100) * MSECOND;
testing_env!(ctx);
match ctr.revoke_vote(prop_id, 1) {
Err(VoteError::NotVoted) => (),
x => panic!("expected NotVoted, got: {:?}", x),
}
}

#[test]
#[should_panic(expected = "proposal not found")]
fn revoke_vote_no_proposal() {
let (_, mut ctr) = setup(&admin());
let prop_id = 2;
match ctr.revoke_vote(prop_id, 1) {
x => panic!("{:?}", x),
}
}
}
Loading

0 comments on commit 8430344

Please sign in to comment.