Skip to content

Commit

Permalink
Voting contract (#22)
Browse files Browse the repository at this point in the history
* Stores encrypted votes
* Stores decrypted votes later
  • Loading branch information
akorchyn authored Apr 5, 2024
1 parent e965858 commit 6e9a017
Show file tree
Hide file tree
Showing 13 changed files with 536 additions and 3 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,11 @@ jobs:
path: target/near/voting_snapshot/*
if-no-files-found: error
overwrite: true
- uses: actions/upload-artifact@v4
if: env.GIT_DIFF
with:
name: voting-contract
path: target/near/voting_contract/*
if-no-files-found: error
overwrite: true

8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
[workspace]
members = ["contracts/voting_snapshot", "contracts/secret_contract", "common"]
members = [
"contracts/voting_snapshot",
"contracts/voting_contract",
"contracts/secret_contract",
"common",
]
resolver = "2"

[profile.release]
Expand Down Expand Up @@ -41,5 +46,6 @@ thiserror = { version = "1.0" }

# crypto
secp256k1 = "0.28"
aes-siv = "0.7.0"

common-contracts = { path = "common" }
15 changes: 15 additions & 0 deletions contracts/voting_contract/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "voting_contract"
description = "The private voting contract for NDC"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
near-sdk = { workspace = true, features = ["legacy"] }
common-contracts.workspace = true

[dev-dependencies]
near-sdk = { workspace = true, features = ["unit-testing"] }
26 changes: 26 additions & 0 deletions contracts/voting_contract/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# voting-contract

The private voting contract for NDC governance

## How to Build Locally?

Install [`cargo-near`](https://github.com/near/cargo-near) and run:

```bash
cargo near build
```

## How to Test Locally?

```bash
cargo test
```

## How to Deploy?

Deployment is automated with GitHub Actions CI/CD pipeline.
To deploy manually, install [`cargo-near`](https://github.com/near/cargo-near) and run:

```bash
cargo near deploy <account-id>
```
4 changes: 4 additions & 0 deletions contracts/voting_contract/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[toolchain]
channel = "stable"
components = ["rustfmt"]
targets = ["wasm32-unknown-unknown"]
5 changes: 5 additions & 0 deletions contracts/voting_contract/src/consts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub const RELAYER_ONLY: &str = "Only relayer can call this method";
pub const VOTING_PHASE_OVER: &str = "Voting phase is over";
pub const VOTING_PHASE_IN_PROGRESS: &str = "Voting phase is in progress";
pub const DEPOSIT_NOT_ENOUGH: &str = "Deposit is not enough to cover the storage cost";
pub const INVALID_VOTE_DATA: &str = "Invalid vote data";
21 changes: 21 additions & 0 deletions contracts/voting_contract/src/crypto.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Will be moved off-chain

use aes_siv::{aead::generic_array::GenericArray, siv::Aes128Siv, KeyInit};
use secp256k1::{ecdh::SharedSecret, PublicKey, SecretKey};

pub fn decrypt_message(bs58message: &str, secret: &SecretKey, pubkey: [u8; 64]) -> Option<Vec<u8>> {
let pubkey = PublicKey::from_slice(&pubkey).ok()?;

let common_secret = SharedSecret::new(&pubkey, secret);

let bytes = near_sdk::bs58::decode(bs58message).into_vec().ok()?;

let ad_data: &[&[u8]] = &[];
let mut cipher = Aes128Siv::new(&GenericArray::clone_from_slice(
&common_secret.secret_bytes(),
));

let decrypted_data = cipher.decrypt(ad_data, &bytes).ok()?;

Some(decrypted_data)
}
262 changes: 262 additions & 0 deletions contracts/voting_contract/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
use near_sdk::borsh::{BorshDeserialize, BorshSerialize};
use near_sdk::collections::{UnorderedMap, Vector};
use near_sdk::env::panic_str;
use near_sdk::{env, near_bindgen, require, AccountId, PanicOnDefault, Timestamp};

pub mod consts;
pub mod storage;
pub mod types;
pub mod views;

#[cfg(test)]
pub mod test_utils;

use consts::*;
use storage::StorageKey;
use types::{EncryptedVoteStorage, EncryptedVoteView};

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
#[borsh(crate = "near_sdk::borsh")]
pub struct Contract {
votes: Vector<EncryptedVoteStorage>,

candidate_weights: UnorderedMap<AccountId, u64>,

relayer: AccountId,
end_time_in_ms: Timestamp,
}

#[near_bindgen]
impl Contract {
#[init]
pub fn new(relayer: AccountId, end_time_in_ms: Timestamp) -> Self {
Contract {
votes: Vector::new(StorageKey::Votes),
candidate_weights: UnorderedMap::new(StorageKey::CandidatesWeights),
relayer,
end_time_in_ms,
}
}

#[payable]
pub fn send_encrypted_votes(&mut self, votes: Vec<EncryptedVoteView>) {
let storage_start = env::storage_usage();
require!(
env::block_timestamp_ms() < self.end_time_in_ms,
VOTING_PHASE_OVER
);
self.assert_relayer();
let votes: Option<Vec<_>> = votes.into_iter().map(Into::into).collect();

if let Some(votes) = votes {
self.votes.extend(votes);
} else {
panic_str(INVALID_VOTE_DATA);
}

require!(
common_contracts::finalize_storage_check(storage_start, 0),
DEPOSIT_NOT_ENOUGH
);
}

#[payable]
pub fn sumbit_results(&mut self, results: Vec<(AccountId, u64)>) {
let storage_start = env::storage_usage();
println!("{}, {}", env::block_timestamp_ms(), self.end_time_in_ms);

require!(
env::block_timestamp_ms() > self.end_time_in_ms,
VOTING_PHASE_IN_PROGRESS
);
self.assert_relayer();

self.candidate_weights.extend(results);

require!(
common_contracts::finalize_storage_check(storage_start, 0),
DEPOSIT_NOT_ENOUGH
);
}

fn assert_relayer(&self) {
require!(
env::predecessor_account_id() == self.relayer,
consts::RELAYER_ONLY
);
}
}

#[cfg(test)]
mod relayer_tests {
use near_sdk::{json_types::Base64VecU8, testing_env, NearToken};

use crate::{test_utils::*, types::EncryptedVoteView};

#[test]
fn can_init_contract() {
let (context, contract) = setup_ctr();
testing_env!(context.clone());
assert_eq!(contract.get_relayer(), relayer());
assert_eq!(contract.get_end_time(), end_time());
}

#[test]
fn can_send_encrypted_votes() {
let (mut context, mut contract) = setup_ctr();
context.predecessor_account_id = relayer();
context.attached_deposit = NearToken::from_near(1);
testing_env!(context.clone());

let votes: Vec<_> = vec![
EncryptedVoteView {
vote: "vote1".to_string(),
pubkey: Base64VecU8([1; 64].to_vec()),
},
EncryptedVoteView {
vote: "vote2".to_string(),
pubkey: Base64VecU8([2; 64].to_vec()),
},
];

contract.send_encrypted_votes(votes.clone());

assert_eq!(contract.get_votes(0, 10), votes);
}

#[test]
fn can_submit_results() {
let (mut context, mut contract) = setup_ctr();
context.predecessor_account_id = relayer();
context.attached_deposit = NearToken::from_near(1);
context.block_timestamp = (end_time() + 1) * MSECOND;
testing_env!(context.clone());

let results: Vec<_> = vec![(acc(1), 1), (acc(2), 2), (acc(3), 3)];

contract.sumbit_results(results.clone());

assert_eq!(contract.get_candidate_weights(0, 10), results);
}

#[test]
#[should_panic(expected = "Only relayer can call this method")]
fn anybody_cant_add_votes() {
let (mut context, mut contract) = setup_ctr();
context.predecessor_account_id = acc(1);
context.attached_deposit = NearToken::from_near(1);
testing_env!(context.clone());

let votes: Vec<_> = vec![
EncryptedVoteView {
vote: "vote1".to_string(),
pubkey: Base64VecU8([1; 64].to_vec()),
},
EncryptedVoteView {
vote: "vote2".to_string(),
pubkey: Base64VecU8([2; 64].to_vec()),
},
];

contract.send_encrypted_votes(votes.clone());
}

#[test]
#[should_panic(expected = "Only relayer can call this method")]
fn anybody_cant_submit_results() {
let (mut context, mut contract) = setup_ctr();
context.predecessor_account_id = acc(1);
context.attached_deposit = NearToken::from_near(1);
context.block_timestamp = (end_time() + 1) * MSECOND;
testing_env!(context.clone());

let results: Vec<_> = vec![(acc(1), 1), (acc(2), 2), (acc(3), 3)];

contract.sumbit_results(results.clone());
}

#[test]
#[should_panic(expected = "Voting phase is over")]
fn cant_add_votes_after_voting_phase() {
let (mut context, mut contract) = setup_ctr();
context.predecessor_account_id = relayer();
context.attached_deposit = NearToken::from_near(1);
context.block_timestamp = (end_time() + 1) * MSECOND;
testing_env!(context.clone());

let votes: Vec<_> = vec![
EncryptedVoteView {
vote: "vote1".to_string(),
pubkey: Base64VecU8([1; 64].to_vec()),
},
EncryptedVoteView {
vote: "vote2".to_string(),
pubkey: Base64VecU8([2; 64].to_vec()),
},
];

contract.send_encrypted_votes(votes.clone());
}

#[test]
#[should_panic(expected = "Voting phase is in progress")]
fn cant_submit_results_before_voting_phase() {
let (mut context, mut contract) = setup_ctr();
context.predecessor_account_id = relayer();
context.attached_deposit = NearToken::from_near(1);
testing_env!(context.clone());

let results: Vec<_> = vec![(acc(1), 1), (acc(2), 2), (acc(3), 3)];

contract.sumbit_results(results.clone());
}

#[test]
#[should_panic(expected = "Deposit is not enough to cover the storage cost")]
fn cant_add_votes_with_insufficient_deposit() {
let (mut context, mut contract) = setup_ctr();
context.predecessor_account_id = relayer();
context.attached_deposit = NearToken::from_near(0);
testing_env!(context.clone());

let votes: Vec<_> = vec![
EncryptedVoteView {
vote: "vote1".to_string(),
pubkey: Base64VecU8([1; 64].to_vec()),
},
EncryptedVoteView {
vote: "vote2".to_string(),
pubkey: Base64VecU8([2; 64].to_vec()),
},
];

contract.send_encrypted_votes(votes.clone());
}

#[test]
#[should_panic(expected = "Invalid vote data")]
fn cant_add_invalid_votes() {
let (mut context, mut contract) = setup_ctr();
context.predecessor_account_id = relayer();
context.attached_deposit = NearToken::from_near(1);
testing_env!(context.clone());

let votes: Vec<_> = vec![
EncryptedVoteView {
vote: "vote1".to_string(),
pubkey: Base64VecU8([1; 64].to_vec()),
},
EncryptedVoteView {
vote: "vote2".to_string(),
pubkey: Base64VecU8([2; 66].to_vec()),
},
EncryptedVoteView {
vote: "vote3".to_string(),
pubkey: Base64VecU8([3; 64].to_vec()),
},
];

contract.send_encrypted_votes(votes.clone());
}
}
9 changes: 9 additions & 0 deletions contracts/voting_contract/src/storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use near_sdk::borsh::BorshSerialize;
use near_sdk::BorshStorageKey;

#[derive(BorshSerialize, BorshStorageKey)]
#[borsh(crate = "near_sdk::borsh")]
pub enum StorageKey {
Votes,
CandidatesWeights,
}
Loading

0 comments on commit 6e9a017

Please sign in to comment.