From db686fc63f6e448b28c4abcf280a4b1bf59eaed1 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 2 Nov 2023 18:34:43 +0300 Subject: [PATCH] registry: is_human_call_lock integration tests (#98) Co-authored-by: sczembor <43810037+sczembor@users.noreply.github.com> --- contracts/Cargo.lock | 1 + contracts/human_checker/Cargo.toml | 1 + contracts/human_checker/src/ext.rs | 11 ++++ contracts/human_checker/src/lib.rs | 54 +++++++++++++++++- contracts/human_checker/tests/workspaces.rs | 61 ++++++++++++++++++++- 5 files changed, 125 insertions(+), 3 deletions(-) diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index b1b0a9c2..40f0214c 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -1645,6 +1645,7 @@ dependencies = [ "near-sdk", "near-units", "near-workspaces", + "registry", "sbt", "serde_json", "tokio", diff --git a/contracts/human_checker/Cargo.toml b/contracts/human_checker/Cargo.toml index 2df13a9c..448d3ec6 100644 --- a/contracts/human_checker/Cargo.toml +++ b/contracts/human_checker/Cargo.toml @@ -15,6 +15,7 @@ near-sdk.workspace = true serde_json.workspace = true sbt = { path = "../sbt" } +registry = { path = "../registry" } [dev-dependencies] anyhow.workspace = true diff --git a/contracts/human_checker/src/ext.rs b/contracts/human_checker/src/ext.rs index 82a9e044..756e2aa4 100644 --- a/contracts/human_checker/src/ext.rs +++ b/contracts/human_checker/src/ext.rs @@ -2,6 +2,8 @@ use near_sdk::json_types::Base64VecU8; use near_sdk::serde::Deserialize; use near_sdk::{ext_contract, AccountId, PromiseOrValue}; +use registry::errors::IsHumanCallErr; + // imports needed for conditional derive (required for tests) #[allow(unused_imports)] use near_sdk::serde::Serialize; @@ -15,4 +17,13 @@ pub trait ExtSbtRegistry { function: String, payload: String, ) -> PromiseOrValue; + + fn is_human_call_lock( + &mut self, + ctr: AccountId, + function: String, + payload: String, + lock_duration: u64, + with_proof: bool, + ) -> Result; } diff --git a/contracts/human_checker/src/lib.rs b/contracts/human_checker/src/lib.rs index 9eccefce..bc5c3eb2 100644 --- a/contracts/human_checker/src/lib.rs +++ b/contracts/human_checker/src/lib.rs @@ -1,12 +1,14 @@ use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::collections::LookupMap; use near_sdk::serde::{Deserialize, Serialize}; -use near_sdk::{env, near_bindgen, require, AccountId, Balance, NearSchema, PanicOnDefault}; +use near_sdk::{env, near_bindgen, require, AccountId, Balance, PanicOnDefault}; use sbt::*; pub const MILI_NEAR: Balance = 1_000_000_000_000_000_000_000; pub const REG_HUMAN_DEPOSIT: Balance = 3 * MILI_NEAR; +/// maximum time for proposal voting in milliseconds. +pub const VOTING_DURATION: u64 = 20_000; #[near_bindgen] #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] @@ -63,16 +65,64 @@ impl Contract { pub fn recorded_sbts(&self, user: AccountId) -> Option { self.used_tokens.get(&user) } + + /// Simulates a governance voting. Every valid human (as per IAH registry) can vote. + /// To avoid double voting by an account who is doing soul_transfer while a proposal is + /// active, we require that voing must be called through `iah_registry.is_human_call_lock`. + /// We check that the caller set enough `lock_duration` for soul transfers. + /// Arguments: + /// * `caller`: account ID making a vote (passed by `iah_registry.is_human_call`) + /// * `locked_until`: time in milliseconds, untile when the caller is locked for soul + /// transfers (reported by `iah_registry.is_human_call`). + /// * `iah_proof`: proof of humanity. It's not required and will be ignored. + /// * `payload`: the proposal ID and the vote (approve or reject). + #[payable] + pub fn vote( + &mut self, + caller: AccountId, + locked_until: u64, + #[allow(unused_variables)] iah_proof: Option, + payload: VotePayload, + ) { + // for this simulation we imagine that every proposal ID is valid and it's finishing + // at "now" + VOTING_DURATION + require!( + env::predecessor_account_id() == self.registry, + "must be called by registry" + ); + require!( + locked_until >= env::block_timestamp_ms() + VOTING_DURATION, + "account not locked for soul transfer for sufficient amount of time" + ); + require!(payload.prop_id > 0, "invalid proposal id"); + require!( + payload.vote == "approve" || payload.vote == "reject", + "invalid vote: must be either 'approve' or 'reject'" + ); + + env::log_str(&format!( + "VOTED: voter={}, proposal={}, vote={}", + caller, payload.prop_id, payload.vote, + )); + } } #[derive(Serialize, Deserialize)] -#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, NearSchema, Clone))] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, Clone))] #[serde(crate = "near_sdk::serde")] pub struct RegisterHumanPayload { pub memo: String, pub numbers: Vec, } +#[derive(Serialize, Deserialize)] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, Clone))] +#[serde(crate = "near_sdk::serde")] +pub struct VotePayload { + pub prop_id: u32, + pub vote: String, +} + pub(crate) fn expected_vec_payload() -> Vec { vec![2, 3, 5, 7, 11] } diff --git a/contracts/human_checker/tests/workspaces.rs b/contracts/human_checker/tests/workspaces.rs index a5d8f325..6d13394f 100644 --- a/contracts/human_checker/tests/workspaces.rs +++ b/contracts/human_checker/tests/workspaces.rs @@ -4,9 +4,10 @@ use near_workspaces::{network::Sandbox, result::ExecutionFinalResult, Account, C use sbt::{SBTs, TokenMetadata}; use serde_json::json; -use human_checker::RegisterHumanPayload; +use human_checker::{RegisterHumanPayload, VotePayload, VOTING_DURATION}; const REGISTER_HUMAN_TOKEN: &str = "register_human_token"; +const MSECOND : u64 = 1000; struct Suite { registry: Contract, @@ -29,6 +30,23 @@ impl Suite { Ok(res) } + pub async fn is_human_call_lock( + &self, + caller: &Account, + lock_duration: u64, + payload: &VotePayload, + ) -> anyhow::Result { + let res = caller + .call(self.registry.id(), "is_human_call_lock") + .args_json(json!({"ctr": self.human_checker.id(), "function": "vote", "payload": serde_json::to_string(payload).unwrap(), "lock_duration": lock_duration, "with_proof": false})) + .max_gas() + .transact() + .await?; + println!(">>> is_human_call_lock logs {:?}\n", res.logs()); + Ok(res) + } + + pub async fn query_sbts(&self, user: &Account) -> anyhow::Result> { // check the key does not exists in human checker let r = self @@ -171,5 +189,46 @@ async fn is_human_call() -> anyhow::Result<()> { tokens = suite.query_sbts(&john).await?; assert_eq!(tokens, None); + + // + // Test Vote with lock duration + // + + // + // test1: too short lock duration: should fail + let mut payload = VotePayload{prop_id: 10, vote: "approve".to_string()}; + let r = suite.is_human_call_lock(&alice, VOTING_DURATION / 3 *2, &payload).await?; + assert!(r.is_failure()); + let failure_str = format!("{:?}",r.failures()); + assert!(failure_str.contains("sufficient amount of time"), "{}", failure_str); + + // + // test2: second call, should not change + let r = suite.is_human_call_lock(&alice, VOTING_DURATION / 3*2, &payload).await?; + assert!(r.is_failure()); + let failure_str = format!("{:?}",r.failures()); + assert!(failure_str.contains("sufficient amount of time"), "{}", failure_str); + + // + // test3: longer call should be accepted, but should fail on wrong payload (vote option) + payload.vote = "wrong-wrong".to_string(); + let r = suite.is_human_call_lock(&alice, VOTING_DURATION +MSECOND, &payload).await?; + assert!(r.is_failure()); + let failure_str = format!("{:?}",r.failures()); + assert!(failure_str.contains("invalid vote: must be either"), "{}", failure_str); + + // + // test4: should work with correct input + payload.vote = "approve".to_string(); + let r = suite.is_human_call_lock(&alice, VOTING_DURATION +MSECOND, &payload).await?; + assert!(r.is_success()); + + // + // test5: should fail with not a human + let r = suite.is_human_call_lock(&john, VOTING_DURATION + MSECOND, &payload).await?; + assert!(r.is_failure()); + let failure_str = format!("{:?}",r.failures()); + assert!(failure_str.contains("is not a human"), "{}", failure_str); + Ok(()) }