diff --git a/contracts/deployed/registry.wasm b/contracts/deployed/registry.wasm index df84c5b9..d7a67e6f 100755 Binary files a/contracts/deployed/registry.wasm and b/contracts/deployed/registry.wasm differ diff --git a/contracts/registry/README.md b/contracts/registry/README.md index 6bc0d415..872d59e4 100644 --- a/contracts/registry/README.md +++ b/contracts/registry/README.md @@ -45,3 +45,8 @@ The IAH Registry supports the following extra queries, which are not part of the See the function documentation for more details and [integration test](https://github.com/near-ndc/i-am-human/blob/780e8cf8326fd0a7976c48afbbafd4553cc7b639/contracts/human_checker/tests/workspaces.rs#L131) for usage. - `sbt_burn(issuer: AccountId, tokens: Vec, memo: Option)` - every holder can burn some of his tokens. + +## Soul transfer + +- The registry enables atomic `soul_transfers`. It Transfers all SBT tokens from one account to another account. +It will fail if owner is `blacklisted`. diff --git a/contracts/registry/src/lib.rs b/contracts/registry/src/lib.rs index 8f389e7d..a01f51f0 100644 --- a/contracts/registry/src/lib.rs +++ b/contracts/registry/src/lib.rs @@ -182,10 +182,11 @@ impl Contract { /// continued by a subsequent call. /// Emits `Ban` event for the caller at the beginning of the process. /// Emits `SoulTransfer` event only once all the tokens from the caller were transferred - /// and at least one token was trasnfered (caller had at least 1 sbt). + /// and at least one token was transferred (caller had at least 1 sbt). /// + User must keep calling the `sbt_soul_transfer` until `true` is returned. /// + If caller does not have any tokens, nothing will be transfered, the caller /// will be banned and `Ban` event will be emitted. + /// Fails if owner is blacklisted. #[payable] pub fn sbt_soul_transfer( &mut self, @@ -200,6 +201,7 @@ impl Contract { // order to facilitate tests. pub(crate) fn _sbt_soul_transfer(&mut self, recipient: AccountId, limit: usize) -> (u32, bool) { let owner = env::predecessor_account_id(); + self.assert_not_blacklisted(&owner); let (resumed, start) = self.transfer_continuation(&owner, &recipient, true); @@ -600,6 +602,13 @@ impl Contract { ); } + #[inline] + pub(crate) fn assert_not_blacklisted(&self, owner: &AccountId) { + if self.flagged.get(owner) == Some(AccountFlag::Blacklisted) { + env::panic_str("account blacklisted"); + } + } + /// note: use issuer_id() if you need issuer_id pub(crate) fn assert_issuer(&self, issuer: &AccountId) -> IssuerId { // TODO: use Result rather than panic @@ -935,6 +944,7 @@ mod tests { ctr.admin_add_sbt_issuer(issuer1()); ctr.admin_add_sbt_issuer(issuer2()); ctr.admin_add_sbt_issuer(issuer3()); + ctr.admin_set_authorized_flaggers([predecessor.clone()].to_vec()); ctx.predecessor_account_id = predecessor.clone(); testing_env!(ctx.clone()); return (ctx, ctr); @@ -2759,4 +2769,110 @@ mod tests { "{}".to_string(), ); } + + #[test] + fn admin_set_authorized_flaggers() { + let (mut ctx, mut ctr) = setup(&admin(), MINT_DEPOSIT); + + let flaggers = [dan()].to_vec(); + ctr.admin_set_authorized_flaggers(flaggers); + + ctx.predecessor_account_id = dan(); + testing_env!(ctx); + ctr.assert_authorized_flagger(); + } + + #[test] + #[should_panic(expected = "not an admin")] + fn admin_set_authorized_flaggers_fail() { + let (mut ctx, mut ctr) = setup(&admin(), MINT_DEPOSIT); + + ctx.predecessor_account_id = dan(); + testing_env!(ctx.clone()); + + let flaggers = [dan()].to_vec(); + ctr.admin_set_authorized_flaggers(flaggers); + } + + #[test] + fn admin_flag_accounts() { + let (_, mut ctr) = setup(&alice(), MINT_DEPOSIT); + + ctr.admin_flag_accounts(AccountFlag::Blacklisted, [dan(), issuer1()].to_vec(), "memo".to_owned()); + ctr.admin_flag_accounts(AccountFlag::Verified, [issuer2()].to_vec(), "memo".to_owned()); + + let exp = r#"EVENT_JSON:{"standard":"i_am_human","version":"1.0.0","event":"flag_blacklisted","data":["dan.near","sbt.n"]}"#; + // check only flag event is emitted + assert_eq!(test_utils::get_logs().len(), 2); + assert_eq!(test_utils::get_logs()[0], exp); + + assert_eq!(ctr.account_flagged(dan()), Some(AccountFlag::Blacklisted)); + assert_eq!(ctr.account_flagged(issuer1()), Some(AccountFlag::Blacklisted)); + assert_eq!(ctr.account_flagged(issuer2()), Some(AccountFlag::Verified)); + + ctr.admin_unflag_accounts([dan()].to_vec(), "memo".to_owned()); + + let exp = r#"EVENT_JSON:{"standard":"i_am_human","version":"1.0.0","event":"unflag","data":["dan.near"]}"#; + assert_eq!(test_utils::get_logs().len(), 3); + assert_eq!(test_utils::get_logs()[2], exp); + + assert_eq!(ctr.account_flagged(dan()), None); + assert_eq!(ctr.account_flagged(issuer1()), Some(AccountFlag::Blacklisted)); + } + + #[test] + #[should_panic(expected = "not authorized")] + fn admin_flag_accounts_non_authorized() { + let (mut ctx, mut ctr) = setup(&alice(), MINT_DEPOSIT); + + ctx.predecessor_account_id = dan(); + testing_env!(ctx.clone()); + ctr.admin_flag_accounts(AccountFlag::Blacklisted, [dan()].to_vec(), "memo".to_owned()); + } + + #[test] + #[should_panic(expected = "not authorized")] + fn admin_unflag_accounts_non_authorized() { + let (mut ctx, mut ctr) = setup(&alice(), MINT_DEPOSIT); + + ctr.admin_flag_accounts(AccountFlag::Blacklisted, [dan(), issuer1()].to_vec(), "memo".to_owned()); + assert_eq!(ctr.account_flagged(dan()), Some(AccountFlag::Blacklisted)); + + ctx.predecessor_account_id = dan(); + testing_env!(ctx.clone()); + ctr.admin_unflag_accounts([dan()].to_vec(), "memo".to_owned()); + } + + #[test] + fn is_human_flagged() { + let (_, mut ctr) = setup(&fractal_mainnet(), MINT_DEPOSIT); + + let m1_1 = mk_metadata(1, Some(START)); + ctr.sbt_mint(vec![(dan(), vec![m1_1])]); + let human_proof = vec![(fractal_mainnet(), vec![1])]; + ctr.admin_flag_accounts(AccountFlag::Verified, [dan()].to_vec(), "memo".to_owned()); + assert_eq!(ctr.is_human(dan()), human_proof.clone()); + + ctr.admin_flag_accounts(AccountFlag::Blacklisted, [dan()].to_vec(), "memo".to_owned()); + assert_eq!(ctr.is_human(dan()), vec![]); + + ctr.admin_unflag_accounts([dan()].to_vec(), "memo".to_owned()); + assert_eq!(ctr.is_human(dan()), human_proof); + } + + #[test] + #[should_panic(expected = "account blacklisted")] + fn black_listed_soul_transfer() { + let (mut ctx, mut ctr) = setup(&issuer1(), 2 * MINT_DEPOSIT); + + let m1_1 = mk_metadata(1, Some(START + 10)); + ctr.sbt_mint(vec![(alice(), vec![m1_1.clone()])]); + + ctr.admin_flag_accounts(AccountFlag::Blacklisted, [alice()].to_vec(), "memo".to_owned()); + + // make soul transfer + ctx.predecessor_account_id = alice(); + testing_env!(ctx.clone()); + ctr.sbt_soul_transfer(alice2(), None); + } } diff --git a/contracts/registry/src/storage.rs b/contracts/registry/src/storage.rs index d689a40c..3389f1c7 100644 --- a/contracts/registry/src/storage.rs +++ b/contracts/registry/src/storage.rs @@ -24,7 +24,7 @@ pub enum StorageKey { AdminsFlagged, } -#[derive(BorshSerialize, BorshDeserialize, BorshStorageKey, Serialize, Deserialize)] +#[derive(BorshSerialize, BorshDeserialize, BorshStorageKey, Serialize, Deserialize, PartialEq)] #[serde(crate = "near_sdk::serde")] #[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] pub enum AccountFlag { diff --git a/contracts/registry/tests/workspaces.rs b/contracts/registry/tests/workspaces.rs index a9fc1a36..5e6a48f9 100644 --- a/contracts/registry/tests/workspaces.rs +++ b/contracts/registry/tests/workspaces.rs @@ -1,11 +1,11 @@ use anyhow::Ok; use near_sdk::serde_json::json; use near_units::parse_near; -use sbt::TokenMetadata; +use sbt::{TokenMetadata, ClassSet}; use workspaces::{network::Sandbox, Account, AccountId, Contract, Worker}; const MAINNET_REGISTRY_ID: &str = "registry.i-am-human.near"; -const BLOCK_HEIGHT: u64 = 90979963; +const BLOCK_HEIGHT: u64 = 92042705; const IAH_CLASS: u64 = 1; const OG_CLASS: u64 = 2; @@ -89,6 +89,17 @@ async fn assert_data_consistency( .json()?; assert_eq!(bob_og_supply, 0); + let iah_class_set: ClassSet = registry + .call("iah_class_set") + .args_json(json!({})) + .max_gas() + .transact() + .await? + .json()?; + + assert_eq!(iah_class_set[0].0.to_string(), iah_issuer.id().to_string()); + assert_eq!(iah_class_set[0].1[0], 1); + Ok(()) } @@ -116,7 +127,7 @@ async fn init( // init the contract let res = registry_contract .call("new") - .args_json(json!({"authority": authority_acc.id() })) + .args_json(json!({"authority": authority_acc.id(), "iah_issuer": iah_issuer.id(), "iah_classes": [1]})) .max_gas() .transact() .await?; @@ -190,7 +201,7 @@ async fn init( )); } -#[ignore = "this test is not valid after the migration"] +//#[ignore = "this test is not valid after the migration"] #[tokio::test] async fn migration_mainnet() -> anyhow::Result<()> { let worker = workspaces::sandbox().await?; @@ -216,7 +227,7 @@ async fn migration_mainnet() -> anyhow::Result<()> { // call the migrate method let res = new_registry_contract .call("migrate") - .args_json(json!({"iah_issuer": "iah-issuer.testnet", "iah_classes": [1]})) + .args_json(json!({"authorized_flaggers": ["alice.near"]})) .max_gas() .transact() .await?; @@ -236,6 +247,8 @@ async fn migration_mainnet() -> anyhow::Result<()> { } #[ignore = "this test is not valid after the migration"] +// handler error: [State of contract registry.i-am-human.near is too large to be viewed] +// For current block 99,142,922 #[tokio::test] async fn migration_mainnet_real_data() -> anyhow::Result<()> { // import the registry contract from mainnet with data @@ -269,7 +282,7 @@ async fn migration_mainnet_real_data() -> anyhow::Result<()> { // call the migrate method let res = new_registry_mainnet .call("migrate") - .args_json(json!({"iah_issuer": "iah-issuer.testnet", "iah_classes": [1]})) + .args_json(json!({"authorized_flaggers": ["alice.near"]})) .max_gas() .transact() .await?;