--- a/pallets/nft/src/lib.rs +++ b/pallets/nft/src/lib.rs @@ -395,7 +395,7 @@ // Basic collections pub Collection get(fn collection) config(): map hasher(identity) CollectionId => CollectionType; pub AdminList get(fn admin_list_collection): map hasher(identity) CollectionId => Vec; - pub WhiteList get(fn white_list): map hasher(identity) CollectionId => Vec; + pub WhiteList get(fn white_list): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => bool; /// Balance owner per collection map pub Balance get(fn balance_count): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => u128; @@ -421,6 +421,8 @@ pub ContractSelfSponsoring get(fn contract_self_sponsoring): map hasher(twox_64_concat) T::AccountId => bool; pub ContractSponsorBasket get(fn contract_sponsor_basket): map hasher(twox_64_concat) (T::AccountId, T::AccountId) => T::BlockNumber; pub ContractSponsoringRateLimit get(fn contract_sponsoring_rate_limit): map hasher(twox_64_concat) T::AccountId => T::BlockNumber; + pub ContractWhiteListEnabled get(fn contract_white_list_enabled): map hasher(twox_64_concat) T::AccountId => bool; + pub ContractWhiteList get(fn contract_white_list): double_map hasher(twox_64_concat) T::AccountId, hasher(twox_64_concat) T::AccountId => bool; } add_extra_genesis { build(|config: &GenesisConfig| { @@ -610,7 +612,7 @@ ::remove(collection_id); >::remove(collection_id); >::remove(collection_id); - >::remove(collection_id); + >::remove_prefix(collection_id); >::remove_prefix(collection_id); >::remove_prefix(collection_id); @@ -651,20 +653,8 @@ let sender = ensure_signed(origin)?; Self::check_owner_or_admin_permissions(collection_id, sender)?; - let mut white_list_collection: Vec; - if >::contains_key(collection_id) { - white_list_collection = >::get(collection_id); - if !white_list_collection.contains(&address.clone()) - { - white_list_collection.push(address.clone()); - } - } - else { - white_list_collection = Vec::new(); - white_list_collection.push(address.clone()); - } - - >::insert(collection_id, white_list_collection); + >::insert(collection_id, address, true); + Ok(()) } @@ -686,14 +676,7 @@ let sender = ensure_signed(origin)?; Self::check_owner_or_admin_permissions(collection_id, sender)?; - if >::contains_key(collection_id) { - let mut white_list_collection = >::get(collection_id); - if white_list_collection.contains(&address.clone()) - { - white_list_collection.retain(|i| *i != address.clone()); - >::insert(collection_id, white_list_collection); - } - } + >::remove(collection_id, address); Ok(()) } @@ -1396,12 +1379,7 @@ #[cfg(feature = "runtime-benchmarks")] >::insert(contract_address.clone(), sender.clone()); - let mut is_owner = false; - if >::contains_key(contract_address.clone()) { - let owner = >::get(&contract_address); - is_owner = sender == owner; - } - ensure!(is_owner, Error::::NoPermission); + Self::ensure_contract_owned(sender, &contract_address)?; >::insert(contract_address, enable); Ok(()) @@ -1431,18 +1409,85 @@ rate_limit: T::BlockNumber ) -> DispatchResult { let sender = ensure_signed(origin)?; - let mut is_owner = false; - if >::contains_key(contract_address.clone()) { - let owner = >::get(&contract_address); - is_owner = sender == owner; - } - ensure!(is_owner, Error::::NoPermission); + Self::ensure_contract_owned(sender, &contract_address)?; >::insert(contract_address, rate_limit); Ok(()) } + /// Enable the white list for a contract. Only addresses added to the white list with addToContractWhiteList will be able to call this smart contract. + /// + /// # Permissions + /// + /// * Address that deployed smart contract. + /// + /// # Arguments + /// + /// -`contract_address`: Address of the contract. + /// + /// - `enable`: . + #[weight = 0] + pub fn toggle_contract_white_list( + origin, + contract_address: T::AccountId, + enable: bool + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + Self::ensure_contract_owned(sender, &contract_address)?; + + >::insert(contract_address, enable); + Ok(()) + } + + /// Add an address to smart contract white list. + /// + /// # Permissions + /// + /// * Address that deployed smart contract. + /// + /// # Arguments + /// + /// -`contract_address`: Address of the contract. + /// + /// -`account_address`: Address to add. + #[weight = 0] + pub fn add_to_contract_white_list( + origin, + contract_address: T::AccountId, + account_address: T::AccountId + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + Self::ensure_contract_owned(sender, &contract_address)?; + + >::insert(contract_address, account_address, true); + Ok(()) + } + + /// Remove an address from smart contract white list. + /// + /// # Permissions + /// + /// * Address that deployed smart contract. + /// + /// # Arguments + /// + /// -`contract_address`: Address of the contract. + /// + /// -`account_address`: Address to remove. #[weight = 0] + pub fn remove_from_contract_white_list( + origin, + contract_address: T::AccountId, + account_address: T::AccountId + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + Self::ensure_contract_owned(sender, &contract_address)?; + + >::remove(contract_address, account_address); + Ok(()) + } + + #[weight = 0] pub fn set_collection_limits( origin, collection_id: u32, @@ -1778,9 +1823,7 @@ fn check_white_list(collection_id: CollectionId, address: &T::AccountId) -> DispatchResult { let mes = Error::::AddresNotInWhiteList; - ensure!(>::contains_key(collection_id), mes); - let wl = >::get(collection_id); - ensure!(wl.contains(address), mes); + ensure!(>::contains_key(collection_id, address), mes); Ok(()) } @@ -2138,6 +2181,17 @@ Ok(()) } + + fn ensure_contract_owned(account: T::AccountId, contract: &T::AccountId) -> DispatchResult { + if >::contains_key(contract.clone()) { + let owner = >::get(contract); + ensure!(account == owner, Error::::NoPermission); + } else { + fail!(Error::::NoPermission); + } + + Ok(()) + } } //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -2357,6 +2411,16 @@ let called_contract: T::AccountId = T::Lookup::lookup((*dest).clone()).unwrap_or(T::AccountId::default()); + let owned_contract = >::contains_key(called_contract.clone()) + && >::get(called_contract.clone()) == *who; + let white_list_enabled = >::contains_key(called_contract.clone()) && >::get(called_contract.clone()); + + if !owned_contract && white_list_enabled { + if !>::contains_key(called_contract.clone(), who) { + return Err(InvalidTransaction::Call.into()); + } + } + let mut sponsor_transfer = false; if >::contains_key(called_contract.clone()) { let last_tx_block = >::get((&called_contract, &who)); --- a/pallets/nft/src/tests.rs +++ b/pallets/nft/src/tests.rs @@ -960,7 +960,7 @@ let origin1 = Origin::signed(1); assert_ok!(TemplateModule::add_to_white_list(origin1.clone(), collection_id, 2)); - assert_eq!(TemplateModule::white_list(collection_id)[0], 2); + assert_eq!(TemplateModule::white_list(collection_id, 2), true); }); } @@ -975,7 +975,7 @@ assert_ok!(TemplateModule::add_collection_admin(origin1.clone(), collection_id, 2)); assert_ok!(TemplateModule::add_to_white_list(origin2.clone(), collection_id, 3)); - assert_eq!(TemplateModule::white_list(collection_id)[0], 3); + assert_eq!(TemplateModule::white_list(collection_id, 3), true); }); } @@ -1035,8 +1035,7 @@ assert_ok!(TemplateModule::add_to_white_list(origin1.clone(), collection_id, 2)); assert_ok!(TemplateModule::add_to_white_list(origin1.clone(), collection_id, 2)); - assert_eq!(TemplateModule::white_list(collection_id)[0], 2); - assert_eq!(TemplateModule::white_list(collection_id).len(), 1); + assert_eq!(TemplateModule::white_list(collection_id, 2), true); }); } @@ -1054,7 +1053,7 @@ collection_id, 2 )); - assert_eq!(TemplateModule::white_list(collection_id).len(), 0); + assert_eq!(TemplateModule::white_list(collection_id, 2), false); }); } @@ -1075,7 +1074,7 @@ collection_id, 3 )); - assert_eq!(TemplateModule::white_list(collection_id).len(), 0); + assert_eq!(TemplateModule::white_list(collection_id, 3), false); }); } @@ -1093,7 +1092,7 @@ TemplateModule::remove_from_white_list(origin2.clone(), collection_id, 2), Error::::NoPermission ); - assert_eq!(TemplateModule::white_list(collection_id)[0], 2); + assert_eq!(TemplateModule::white_list(collection_id, 2), true); }); } @@ -1125,7 +1124,7 @@ TemplateModule::remove_from_white_list(origin2.clone(), collection_id, 2), Error::::CollectionNotFound ); - assert_eq!(TemplateModule::white_list(collection_id).len(), 0); + assert_eq!(TemplateModule::white_list(collection_id, 2), false); }); } @@ -1149,7 +1148,7 @@ collection_id, 2 )); - assert_eq!(TemplateModule::white_list(collection_id).len(), 0); + assert_eq!(TemplateModule::white_list(collection_id, 2), false); }); } --- a/tests/src/contracts.test.ts +++ b/tests/src/contracts.test.ts @@ -1,16 +1,15 @@ -// -// This file is subject to the terms and conditions defined in -// file 'LICENSE', which is part of this source code package. -// - -import { ApiPromise } from "@polkadot/api"; -import { expect } from "chai"; -import { default as usingApi, submitTransactionAsync } from "./substrate/substrate-api"; +import chai from "chai"; +import chaiAsPromised from 'chai-as-promised'; +import usingApi, { submitTransactionAsync, submitTransactionExpectFailAsync } from "./substrate/substrate-api"; import fs from "fs"; -import { Abi, BlueprintPromise, CodePromise } from "@polkadot/api-contract"; +import { Abi, BlueprintPromise as Blueprint, CodePromise, ContractPromise as Contract } from "@polkadot/api-contract"; import { IKeyringPair } from "@polkadot/types/types"; -import { Keyring } from "@polkadot/api"; +import { ApiPromise, Keyring } from "@polkadot/api"; import { ApiTypes, SubmittableExtrinsic } from "@polkadot/api/types"; +import privateKey from "./substrate/privateKey"; + +chai.use(chaiAsPromised); +const expect = chai.expect; import { BigNumber } from 'bignumber.js'; import { findUnusedAddress } from './util/helpers' @@ -18,8 +17,8 @@ const gasLimit = 3000n * 1000000n; const endowment = `1000000000000000`; -function deployBlueprint(alice: IKeyringPair, code: CodePromise): Promise { - return new Promise(async (resolve, reject) => { +function deployBlueprint(alice: IKeyringPair, code: CodePromise): Promise { + return new Promise(async (resolve, reject) => { const unsub = await code .createBlueprint() .signAndSend(alice, (result) => { @@ -32,7 +31,7 @@ }); } -function deployContract(alice: IKeyringPair, blueprint: BlueprintPromise) : Promise { +function deployContract(alice: IKeyringPair, blueprint: Blueprint) : Promise { return new Promise(async (resolve, reject) => { const initValue = true; @@ -62,39 +61,112 @@ return deployer; } -describe('Contracts smoke test', () => { +async function deployFlipper(api: ApiPromise): Promise<[Contract, IKeyringPair]> { + const metadata = JSON.parse(fs.readFileSync('./src/flipper/metadata.json').toString('utf-8')); + const abi = new Abi(metadata); + + const deployer = await prepareDeployer(api); + + const wasm = fs.readFileSync('./src/flipper/flipper.wasm'); + + const code = new CodePromise(api, abi, wasm); + + const blueprint = await deployBlueprint(deployer, code); + const contract = (await deployContract(deployer, blueprint))['contract'] as Contract; + + const initialGetResponse = await getFlipValue(contract, deployer); + expect(initialGetResponse).to.be.true; + + return [contract, deployer]; +} + +async function getFlipValue(contract: Contract, deployer: IKeyringPair) { + const result = await contract.query.get(deployer.address, value, gasLimit); + + if(!result.result.isSuccess) { + throw `Failed to get flipper value`; + } + return (result.result.asSuccess.data[0] == 0x00) ? false : true; +} + +describe('Contracts', () => { it(`Can deploy smart contract Flipper, instantiate it and call it's get and flip messages.`, async () => { await usingApi(async api => { - const deployer = await prepareDeployer(api); + const [contract, deployer] = await deployFlipper(api); + const initialGetResponse = await getFlipValue(contract, deployer); + + const bob = privateKey("//Bob"); + const flip = contract.exec('flip', value, gasLimit); + await submitTransactionAsync(bob, flip); + + const afterFlipGetResponse = await getFlipValue(contract, deployer); + expect(afterFlipGetResponse).not.to.be.eq(initialGetResponse, 'Flipping should change value.'); + }); + }); + + it(`Whitelisted account can call contract.`, async () => { + await usingApi(async api => { + const bob = privateKey("//Bob"); - const wasm = fs.readFileSync('./src/flipper/flipper.wasm'); - - const metadata = JSON.parse(fs.readFileSync('./src/flipper/metadata.json').toString('utf-8')); - const abi = new Abi(metadata); + const [contract, deployer] = await deployFlipper(api); + const consoleError = console.error; + console.error = (...data: any[]) => { + }; - const code = new CodePromise(api, abi, wasm); + let expectedFlipValue = await getFlipValue(contract, deployer); - const blueprint = await deployBlueprint(deployer, code); - const contract = (await deployContract(deployer, blueprint))['contract']; + const flip = contract.exec('flip', value, gasLimit); + await submitTransactionAsync(bob, flip); + expectedFlipValue = !expectedFlipValue; + const afterFlip = await getFlipValue(contract,deployer); + expect(afterFlip).to.be.eq(expectedFlipValue, `Anyone can call new contract.`); - const getFlipValue = async () => { - const result = await contract.query.get(deployer.address, value, gasLimit); + const deployerCanFlip = async () => { + expectedFlipValue = !expectedFlipValue; + const deployerFlip = contract.exec('flip', value, gasLimit); + await submitTransactionAsync(deployer, deployerFlip); + const aliceFlip1Response = await getFlipValue(contract, deployer); + expect(aliceFlip1Response).to.be.eq(expectedFlipValue, `Deployer always can flip.`); + }; + await deployerCanFlip(); - if(!result.result.isSuccess) { - throw `Failed to get flipper value`; - } - return (result.result.asSuccess.data[0] == 0x00) ? false : true; - } + const enableWhiteListTx = api.tx.nft.toggleContractWhiteList(contract.address, true); + const enableResult = await submitTransactionAsync(deployer, enableWhiteListTx); + const flipWithEnabledWhiteList = contract.exec('flip', value, gasLimit); + await expect(submitTransactionExpectFailAsync(bob, flipWithEnabledWhiteList)).to.be.rejected; + const flipValueAfterEnableWhiteList = await getFlipValue(contract, deployer); + expect(flipValueAfterEnableWhiteList).to.be.eq(expectedFlipValue, `Enabling whitelist doesn't make it possible to call contract for everyone.`); - const initialGetResponse = await getFlipValue(); - expect(initialGetResponse).to.be.true; + await deployerCanFlip(); - const flip = contract.exec('flip', value, gasLimit); - await submitTransactionAsync(deployer, flip); + const addBobToWhiteListTx = api.tx.nft.addToContractWhiteList(contract.address, bob.address); + const addBobResult = await submitTransactionAsync(deployer, addBobToWhiteListTx); + const flipWithWhitelistedBob = contract.exec('flip', value, gasLimit); + await submitTransactionAsync(bob, flipWithWhitelistedBob); + expectedFlipValue = !expectedFlipValue; + const flipAfterWhiteListed = await getFlipValue(contract,deployer); + expect(flipAfterWhiteListed).to.be.eq(expectedFlipValue, `Bob was whitelisted, now he can flip.`); - const afterFlipGetResponse = await getFlipValue(); + await deployerCanFlip(); - expect(afterFlipGetResponse).to.be.false; + const removeBobFromWhiteListTx = api.tx.nft.removeFromContractWhiteList(contract.address, bob.address); + const removeBobResult = await submitTransactionAsync(deployer, removeBobFromWhiteListTx); + const bobRemoved = contract.exec('flip', value, gasLimit); + await expect(submitTransactionExpectFailAsync(bob, bobRemoved)).to.be.rejected; + const afterBobRemoved = await getFlipValue(contract, deployer); + expect(afterBobRemoved).to.be.eq(expectedFlipValue, `Bob can't call contract, now when he is removeed from white list.`); + + await deployerCanFlip(); + + const disableWhiteListTx = api.tx.nft.toggleContractWhiteList(contract.address, false); + const disableWhiteListResult = await submitTransactionAsync(deployer, disableWhiteListTx); + const whiteListDisabledFlip = contract.exec('flip', value, gasLimit); + await submitTransactionAsync(bob, whiteListDisabledFlip); + expectedFlipValue = !expectedFlipValue; + const afterWhiteListDisabled = await getFlipValue(contract,deployer); + expect(afterWhiteListDisabled).to.be.eq(expectedFlipValue, `Anyone can call contract with disabled whitelist.`); + + console.error = consoleError; }); }); --- a/tests/src/substrate/substrate-api.ts +++ b/tests/src/substrate/substrate-api.ts @@ -56,3 +56,25 @@ } }); } + +export function submitTransactionExpectFailAsync(sender: IKeyringPair, transaction: SubmittableExtrinsic): Promise { + return new Promise(async function(resolve, reject) { + try { + await transaction.signAndSend(sender, ({ events = [], status }) => { + if (status.isReady) { + // nothing to do + // console.log(`Current tx status is Ready`); + } else if (status.isBroadcast) { + // nothing to do + // console.log(`Current tx status is Broadcast`); + } else if (status.isInBlock || status.isFinalized) { + resolve(events); + } else { + reject("Transaction failed"); + } + }); + } catch (e) { + reject(e); + } + }); +} \ No newline at end of file