diff --git a/pallets/asset/src/benchmarking.rs b/pallets/asset/src/benchmarking.rs index 745dddc7b1..932a41335c 100644 --- a/pallets/asset/src/benchmarking.rs +++ b/pallets/asset/src/benchmarking.rs @@ -1011,4 +1011,11 @@ benchmarks! { ) ); } + + set_address_frozen { + let bob = UserBuilder::::default().generate_did().build("Bob"); + let alice = UserBuilder::::default().generate_did().build("Alice"); + let asset_id = create_sample_asset::(&alice, true); + let bob_account: [u8; 32] = bob.account().encode().try_into().unwrap(); + }: _(alice.origin, asset_id, true, bob_account.into()) } diff --git a/pallets/asset/src/lib.rs b/pallets/asset/src/lib.rs index d198c0ef3a..adb66e37c2 100644 --- a/pallets/asset/src/lib.rs +++ b/pallets/asset/src/lib.rs @@ -366,6 +366,13 @@ pub mod pallet { asset_id: AssetId, frozen_balance: Balance, }, + /// The account status has been set to `freeze`. + SetAccountFreeze { + caller_did: IdentityId, + account: AccountId32, + asset_id: AssetId, + freeze: bool, + }, } /// Map each [`Ticker`] to its registration details ([`TickerRegistration`]). @@ -646,6 +653,11 @@ pub mod pallet { ValueQuery, >; + /// Tracks if the account is frozen. + #[pallet::storage] + pub type FrozenAccounts = + StorageDoubleMap<_, Twox64Concat, AccountId32, Blake2_128Concat, AssetId, bool, ValueQuery>; + /// Storage version. #[pallet::storage] pub type StorageVersion = StorageValue<_, Version, ValueQuery>; @@ -1833,6 +1845,18 @@ pub mod pallet { ) -> DispatchResult { Self::base_set_frozen_tokens(origin, asset_id, asset_holder, amount) } + + /// Set the status of `account` for `asset_id` to `freeze`. + #[pallet::call_index(39)] + #[pallet::weight(::WeightInfo::set_address_frozen())] + pub fn set_address_frozen( + origin: OriginFor, + asset_id: AssetId, + freeze: bool, + account: AccountId32, + ) -> DispatchResult { + Self::base_set_address_frozen(origin, asset_id, freeze, account) + } } #[pallet::error] @@ -1947,6 +1971,8 @@ pub mod pallet { SelfOwnershipTransferNotAllowed, /// The weight Limit for the extrinsic has been exceeded. WeightLimitExceeded, + /// The account is frozen and cannot transfer assets. + InvalidTransferFrozenAccount, } pub trait WeightInfo { @@ -1994,6 +2020,7 @@ pub mod pallet { fn get_holders_frozen_balance() -> Weight; fn transfer_is_allowed_for_holder_best_case() -> Weight; fn transfer_is_allowed_for_holder_worst_case() -> Weight; + fn set_address_frozen() -> Weight; } } @@ -2994,6 +3021,31 @@ impl Pallet { Self::unverified_set_frozen_tokens(caller_did, asset_holder, asset_id, amount); Ok(()) } + + /// Sets whether `account` is frozen for transfers of `asset_id`. + fn base_set_address_frozen( + origin: T::RuntimeOrigin, + asset_id: AssetId, + freeze: bool, + account: AccountId32, + ) -> DispatchResult { + let caller_did = ExternalAgents::::ensure_perms(origin, &asset_id)?; + + if freeze { + FrozenAccounts::::insert(account.clone(), asset_id, true); + } else { + FrozenAccounts::::remove(&account, &asset_id); + } + + Self::deposit_event(Event::SetAccountFreeze { + caller_did, + account, + asset_id, + freeze, + }); + + Ok(()) + } } //========================================================================== @@ -3377,6 +3429,17 @@ impl Pallet { Ok(()) } + /// Returns `Ok` if the holder is not frozen for the given asset. + fn ensure_holder_is_not_frozen(holder: &AssetHolder, asset_id: &AssetId) -> DispatchResult { + if let AssetHolder::Account(account_id) = holder { + ensure!( + !FrozenAccounts::::get(account_id, asset_id), + Error::::InvalidTransferFrozenAccount + ); + } + Ok(()) + } + /// Returns a vector containing all errors for the transfer. An empty vec means there's no error. pub fn asset_transfer_report( sender: &AssetHolder, @@ -3769,6 +3832,7 @@ impl Pallet { if is_controller_transfer { current_balance.saturating_sub(locked_balance) } else { + Self::ensure_holder_is_not_frozen(holder, asset_id)?; let frozen_balance = Self::get_holders_frozen_balance(holder, asset_id); let unavailable_balance = locked_balance.saturating_add(frozen_balance); current_balance.saturating_sub(unavailable_balance) @@ -3913,6 +3977,12 @@ impl Pallet { return false; } + if holder_is_the_sender { + if Self::ensure_holder_is_not_frozen(asset_holder, asset_id).is_err() { + return false; + } + } + let holder_did = { match pallet_identity::Pallet::::asset_holder_did(&asset_holder) { Ok(did) => did, diff --git a/pallets/precompiles/src/interface/erc20.rs b/pallets/precompiles/src/interface/erc20.rs index 7d78d3947c..4442b3f35d 100644 --- a/pallets/precompiles/src/interface/erc20.rs +++ b/pallets/precompiles/src/interface/erc20.rs @@ -32,7 +32,7 @@ use polymesh_primitives::WeightMeter; use crate::common::{revert, revert_err, Common}; use crate::interface::FungibleAssetInterface; -use crate::interface::{ERR_ASSET_NOT_FOUND, ERR_INST_NOT_EXECUTED}; +use crate::interface::{DECIMALS, ERR_ASSET_NOT_FOUND, ERR_INST_NOT_EXECUTED}; use crate::Config; impl FungibleAssetInterface { @@ -320,6 +320,6 @@ impl FungibleAssetInterface { _asset_id: AssetId, _env: &mut impl Ext, ) -> Result, Error> { - Ok(IFungibleAsset::decimalsCall::abi_encode_returns(&6)) + Ok(IFungibleAsset::decimalsCall::abi_encode_returns(&DECIMALS)) } } diff --git a/pallets/precompiles/src/interface/erc3643.rs b/pallets/precompiles/src/interface/erc3643.rs new file mode 100644 index 0000000000..a0bae71e12 --- /dev/null +++ b/pallets/precompiles/src/interface/erc3643.rs @@ -0,0 +1,189 @@ +// This file is part of the Polymesh distribution (https://github.com/PolymeshAssociation/Polymesh). +// Copyright (c) 2020 Polymesh Association + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. + +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use alloc::vec::Vec; +use frame_support::traits::Get; + +use pallet_revive::precompiles::alloy::primitives::keccak256; +use pallet_revive::precompiles::alloy::primitives::FixedBytes; +use pallet_revive::precompiles::alloy::sol_types::Revert; +use pallet_revive::precompiles::Error; +use pallet_revive::precompiles::Ext; + +use pallet_asset::{AssetIdTicker, AssetNames}; +use polymesh_precompiles::{IFungibleAsset, IFungibleAssetEvents}; +use polymesh_primitives::asset::{AssetId, AssetName}; +use polymesh_primitives::ticker::TICKER_LEN; +use polymesh_primitives::Ticker; + +use crate::common::Common; +use crate::interface::FungibleAssetInterface; +use crate::interface::{DECIMALS, ERR_INVALID_SYMBOL}; +use crate::Config; + +impl FungibleAssetInterface { + /// Freezes the asset, preventing token transfers. Only an agent of the token can call this function. + pub(crate) fn pause(asset_id: AssetId, env: &mut impl Ext) -> Result, Error> { + let caller = Common::::caller(env)?; + + Common::::call_runtime( + env, + caller.runtime_origin(), + pallet_asset::Call::::freeze { asset_id }, + )?; + + Common::::deposit_event( + env, + IFungibleAssetEvents::Paused(IFungibleAsset::Paused { + userAddress: caller.address.0.into(), + }), + )?; + Ok(Vec::new()) + } + + /// Unfreezes the token contract, allowing token transfers. Only an agent of the token can call this function. + pub(crate) fn unpause(asset_id: AssetId, env: &mut impl Ext) -> Result, Error> { + let caller = Common::::caller(env)?; + + Common::::call_runtime( + env, + caller.runtime_origin(), + pallet_asset::Call::::unfreeze { asset_id }, + )?; + + Common::::deposit_event( + env, + IFungibleAssetEvents::Unpaused(IFungibleAsset::Unpaused { + userAddress: caller.address.0.into(), + }), + )?; + Ok(Vec::new()) + } + + /// Sets the token name. Only the owner of the token contract can call this function. + pub(crate) fn set_name( + asset_id: AssetId, + call: &IFungibleAsset::setNameCall, + env: &mut impl Ext, + ) -> Result, Error> { + // Charges the additional `AssetIdTicker` read + env.charge(T::DbWeight::get().reads(1))?; + + let caller = Common::::caller(env)?; + let new_asset_name = AssetName::from(&call.name.as_bytes().to_vec()); + + Common::::call_runtime( + env, + caller.runtime_origin(), + pallet_asset::Call::::rename_asset { + asset_id, + asset_name: new_asset_name.clone(), + }, + )?; + + let ticker = AssetIdTicker::::get(&asset_id).unwrap_or_default(); + Common::::deposit_event( + env, + IFungibleAssetEvents::UpdatedTokenInformation( + IFungibleAsset::UpdatedTokenInformation { + newName: FixedBytes::from(keccak256(new_asset_name.0.as_slice())), + newSymbol: FixedBytes::from(keccak256(ticker.as_ref())), + newDecimals: DECIMALS, + newVersion: Default::default(), + newOnchainID: Default::default(), + }, + ), + )?; + Ok(Vec::new()) + } + + /// Sets the token symbol. Only the owner of the token contract can call this function. + pub(crate) fn set_symbol( + asset_id: AssetId, + call: &IFungibleAsset::setSymbolCall, + env: &mut impl Ext, + ) -> Result, Error> { + // Charges the additional `AssetNames` read + env.charge(T::DbWeight::get().reads(1))?; + + let new_symbol = &call.symbol.as_bytes(); + + if new_symbol.len() > TICKER_LEN { + return Err(Error::Revert(Revert { + reason: ERR_INVALID_SYMBOL.into(), + })); + } + + let ticker = Ticker::from_slice_truncated(new_symbol); + let caller = Common::::caller(env)?; + + Common::::call_runtime( + env, + caller.runtime_origin(), + pallet_asset::Call::::register_unique_ticker { ticker }, + )?; + Common::::call_runtime( + env, + caller.runtime_origin(), + pallet_asset::Call::::link_ticker_to_asset_id { ticker, asset_id }, + )?; + + let asset_name = AssetNames::::get(&asset_id).unwrap_or_default(); + Common::::deposit_event( + env, + IFungibleAssetEvents::UpdatedTokenInformation( + IFungibleAsset::UpdatedTokenInformation { + newName: FixedBytes::from(keccak256(asset_name.0.as_slice())), + newSymbol: FixedBytes::from(keccak256(ticker.as_ref())), + newDecimals: DECIMALS, + newVersion: Default::default(), + newOnchainID: Default::default(), + }, + ), + )?; + Ok(Vec::new()) + } + + /// Sets the frozen status of a specific address. Only an agent of the token can call this function. + pub(crate) fn set_address_frozen( + asset_id: AssetId, + call: &IFungibleAsset::setAddressFrozenCall, + env: &mut impl Ext, + ) -> Result, Error> { + let caller = Common::::caller(env)?; + + let acc_to_freeze = Common::::account_id32(call.account)?; + + Common::::call_runtime( + env, + caller.runtime_origin(), + pallet_asset::Call::::set_address_frozen { + asset_id, + freeze: call.freeze, + account: acc_to_freeze, + }, + )?; + + Common::::deposit_event( + env, + IFungibleAssetEvents::AddressFrozen(IFungibleAsset::AddressFrozen { + account: call.account, + freeze: call.freeze, + owner: caller.address.0.into(), + }), + )?; + Ok(Vec::new()) + } +} diff --git a/pallets/precompiles/src/interface/mod.rs b/pallets/precompiles/src/interface/mod.rs index 2b50d2d941..f47661819c 100644 --- a/pallets/precompiles/src/interface/mod.rs +++ b/pallets/precompiles/src/interface/mod.rs @@ -32,6 +32,7 @@ use crate::common::{revert, revert_err, Common}; use crate::Config; mod erc20; +mod erc3643; mod erc7943; mod polymesh_specific; @@ -39,8 +40,11 @@ mod polymesh_specific; pub(crate) const ERR_ASSET_NOT_FOUND: &str = "Asset not found"; pub(crate) const ERR_ASSET_NOT_FUNGIBLE: &str = "Asset is not fungible"; pub(crate) const ERR_INST_NOT_EXECUTED: &str = "Instruction was not executed; Most likely the instruction is missing an affirmation from the receiver/mediator"; +pub(crate) const ERR_INVALID_SYMBOL: &str = "Invalid symbol; Ticker is too long"; // ======================================================== +pub const DECIMALS: u8 = 6; + /// All precompile calls exposed by the Polymesh runtime. pub struct FungibleAssetInterface(PhantomData); @@ -75,6 +79,11 @@ impl Precompile for FungibleAssetInterface { | IFungibleAssetCalls::permit(_) | IFungibleAssetCalls::forcedTransfer(_) | IFungibleAssetCalls::setFrozenTokens(_) + | IFungibleAssetCalls::setName(_) + | IFungibleAssetCalls::setSymbol(_) + | IFungibleAssetCalls::pause(_) + | IFungibleAssetCalls::unpause(_) + | IFungibleAssetCalls::setAddressFrozen(_) if env.is_read_only() => { Err(Common::::state_change_denied()) @@ -115,6 +124,15 @@ impl Precompile for FungibleAssetInterface { IFungibleAssetCalls::setFrozenTokens(call) => { Self::set_frozen_tokens(asset_id, call, env) } + + // ERC3643 functions + IFungibleAssetCalls::pause(_) => Self::pause(asset_id, env), + IFungibleAssetCalls::unpause(_) => Self::unpause(asset_id, env), + IFungibleAssetCalls::setName(call) => Self::set_name(asset_id, call, env), + IFungibleAssetCalls::setSymbol(call) => Self::set_symbol(asset_id, call, env), + IFungibleAssetCalls::setAddressFrozen(call) => { + Self::set_address_frozen(asset_id, call, env) + } } } } diff --git a/pallets/weights/src/pallet_asset.rs b/pallets/weights/src/pallet_asset.rs index f18dce7db6..635e328d59 100644 --- a/pallets/weights/src/pallet_asset.rs +++ b/pallets/weights/src/pallet_asset.rs @@ -931,4 +931,20 @@ impl pallet_asset::WeightInfo for SubstrateWeight { // Minimum execution time: 92_745 nanoseconds. Weight::from_parts(94_474_000, 0).saturating_add(DbWeight::get().reads(6)) } + // Storage: `Identity::KeyRecords` (r:1 w:0) + // Proof: `Identity::KeyRecords` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + // Storage: `ExternalAgents::GroupOfAgent` (r:1 w:0) + // Proof: `ExternalAgents::GroupOfAgent` (`max_values`: None, `max_size`: Some(77), added: 2552, mode: `MaxEncodedLen`) + // Storage: `Permissions::CurrentPalletName` (r:1 w:0) + // Proof: `Permissions::CurrentPalletName` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + // Storage: `Permissions::CurrentDispatchableName` (r:1 w:0) + // Proof: `Permissions::CurrentDispatchableName` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + // Storage: `Asset::FrozenAccounts` (r:0 w:1) + // Proof: `Asset::FrozenAccounts` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + fn set_address_frozen() -> Weight { + // Minimum execution time: 42_414 nanoseconds. + Weight::from_parts(43_671_000, 0) + .saturating_add(DbWeight::get().reads(4)) + .saturating_add(DbWeight::get().writes(1)) + } } diff --git a/precompiles/src/interfaces/FungibleAssetStub.bin b/precompiles/src/interfaces/FungibleAssetStub.bin index 7490fb2805..e9a24b2766 100644 Binary files a/precompiles/src/interfaces/FungibleAssetStub.bin and b/precompiles/src/interfaces/FungibleAssetStub.bin differ diff --git a/precompiles/src/interfaces/FungibleAssetStub.sol b/precompiles/src/interfaces/FungibleAssetStub.sol index fa4478acf5..e777b96583 100644 --- a/precompiles/src/interfaces/FungibleAssetStub.sol +++ b/precompiles/src/interfaces/FungibleAssetStub.sol @@ -209,6 +209,43 @@ interface IFungibleAsset { /// @param account The address to check. /// @return True if the account is allowed to receive, false otherwise. function canReceive(address account) external view returns (bool); + + // ============================================================ + // ERC-3643 + // ============================================================ + + /// @notice Emitted when the token information is updated. This includes updates to the token's name, symbol, decimals, version, and onchainID. + event UpdatedTokenInformation( + string indexed newName, + string indexed newSymbol, + uint8 newDecimals, + string newVersion, + address indexed newOnchainID + ); + + /// @notice Emitted when the token contract is paused. + event Paused(address userAddress); + + /// @notice Emitted when the token contract is unpaused. + event Unpaused(address userAddress); + + /// @notice Emitted when the account of an investor is frozen or unfrozen. + event AddressFrozen(address indexed account, bool freeze, address indexed owner); + + /// @notice Sets the token name. Only the owner of the token contract can call this function. + function setName(string calldata name) external; + + /// @notice Sets the token symbol. Only the owner of the token contract can call this function. + function setSymbol(string calldata symbol) external; + + /// @notice Pauses the token contract, preventing token transfers. Only an agent of the token can call this function. + function pause() external; + + /// @notice Unpauses the token contract, allowing token transfers. Only an agent of the token can call this function. + function unpause() external; + + /// @notice Sets the frozen status of a specific address. Only an agent of the token can call this function. + function setAddressFrozen(address account, bool freeze) external; } contract FungibleAssetStub is IFungibleAsset { @@ -337,4 +374,33 @@ contract FungibleAssetStub is IFungibleAsset { account; revert NotExecutable(); } + + /// @notice Sets the token name. Only the owner of the token contract can call this function. + function setName(string calldata name) external override { + name; + revert NotExecutable(); + } + + /// @notice Sets the token symbol. Only the owner of the token contract can call this function. + function setSymbol(string calldata symbol) external override { + symbol; + revert NotExecutable(); + } + + /// @notice Pauses the token contract, preventing token transfers. Only an agent of the token can call this function. + function pause() external override { + revert NotExecutable(); + } + + /// @notice Unpauses the token contract, allowing token transfers. Only an agent of the token can call this function. + function unpause() external override { + revert NotExecutable(); + } + + /// @notice Sets the frozen status of a specific address. Only an agent of the token can call this function. + function setAddressFrozen(address userAddress, bool freeze) external override { + userAddress; + freeze; + revert NotExecutable(); + } }