12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879#![cfg_attr(not(feature = "std"), no_std)]8081use core::ops::Deref;8283use evm_coder::ToLog;84use frame_support::{dispatch::PostDispatchInfo, ensure, pallet_prelude::*};85pub use pallet::*;86use pallet_common::{87 eth::collection_id_to_address, helpers::add_weight_to_post_info,88 weights::WeightInfo as CommonWeightInfo, Error as CommonError, Event as CommonEvent,89 Pallet as PalletCommon, SelfWeightOf as PalletCommonWeightOf,90};91use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};92use pallet_evm_coder_substrate::WithRecorder;93use pallet_structure::Pallet as PalletStructure;94use sp_core::H160;95use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};96use sp_std::{collections::btree_map::BTreeMap, vec::Vec};97use up_data_structs::{98 budget::Budget, mapping::TokenAddressMapping, AccessMode, CollectionId, CreateCollectionData,99 Property, PropertyKey, TokenId,100};101use weights::WeightInfo;102103use crate::erc::ERC20Events;104#[cfg(feature = "runtime-benchmarks")]105pub mod benchmarking;106pub mod common;107pub mod erc;108pub mod weights;109110pub type CreateItemData<T> = (<T as pallet_evm::Config>::CrossAccountId, u128);111pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;112113#[frame_support::pallet]114pub mod pallet {115 use frame_support::{116 pallet_prelude::*, storage::Key, Blake2_128, Blake2_128Concat, Twox64Concat,117 };118 use up_data_structs::CollectionId;119120 use super::weights::WeightInfo;121122 #[pallet::error]123 pub enum Error<T> {124 125 NotFungibleDataUsedToMintFungibleCollectionToken,126 127 FungibleItemsDontHaveData,128 129 FungibleDisallowsNesting,130 131 SettingPropertiesNotAllowed,132 133 SettingAllowanceForAllNotAllowed,134 135 FungibleTokensAreAlwaysValid,136 }137138 #[pallet::config]139 pub trait Config:140 frame_system::Config + pallet_common::Config + pallet_structure::Config + pallet_evm::Config141 {142 type WeightInfo: WeightInfo;143 }144145 #[pallet::pallet]146 pub struct Pallet<T>(_);147148 149 #[pallet::storage]150 pub type TotalSupply<T: Config> =151 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u128, QueryKind = ValueQuery>;152153 154 #[pallet::storage]155 pub type Balance<T: Config> = StorageNMap<156 Key = (157 Key<Twox64Concat, CollectionId>,158 Key<Blake2_128Concat, T::CrossAccountId>,159 ),160 Value = u128,161 QueryKind = ValueQuery,162 >;163164 165 #[pallet::storage]166 pub type Allowance<T: Config> = StorageNMap<167 Key = (168 Key<Twox64Concat, CollectionId>,169 Key<Blake2_128, T::CrossAccountId>, 170 Key<Blake2_128Concat, T::CrossAccountId>, 171 ),172 Value = u128,173 QueryKind = ValueQuery,174 >;175}176177178179pub struct FungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);180181182impl<T: Config> FungibleHandle<T> {183 184 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {185 Self(inner)186 }187188 189 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {190 self.0191 }192 193 pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {194 &mut self.0195 }196}197impl<T: Config> WithRecorder<T> for FungibleHandle<T> {198 fn recorder(&self) -> &pallet_evm_coder_substrate::SubstrateRecorder<T> {199 self.0.recorder()200 }201 fn into_recorder(self) -> pallet_evm_coder_substrate::SubstrateRecorder<T> {202 self.0.into_recorder()203 }204}205impl<T: Config> Deref for FungibleHandle<T> {206 type Target = pallet_common::CollectionHandle<T>;207208 fn deref(&self) -> &Self::Target {209 &self.0210 }211}212213214impl<T: Config> Pallet<T> {215 216 pub fn init_collection(217 owner: T::CrossAccountId,218 payer: T::CrossAccountId,219 data: CreateCollectionData<T::CrossAccountId>,220 ) -> Result<CollectionId, DispatchError> {221 <PalletCommon<T>>::init_collection(owner, payer, data)222 }223224 225 pub fn init_foreign_collection(226 owner: T::CrossAccountId,227 payer: T::CrossAccountId,228 data: CreateCollectionData<T::CrossAccountId>,229 ) -> Result<CollectionId, DispatchError> {230 <PalletCommon<T>>::init_foreign_collection(owner, payer, data)231 }232233 234 pub fn destroy_collection(235 collection: FungibleHandle<T>,236 sender: &T::CrossAccountId,237 ) -> DispatchResult {238 let id = collection.id;239240 if Self::collection_has_tokens(id) {241 return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());242 }243244 245246 PalletCommon::destroy_collection(collection.0, sender)?;247248 <TotalSupply<T>>::remove(id);249 let _ = <Balance<T>>::clear_prefix((id,), u32::MAX, None);250 let _ = <Allowance<T>>::clear_prefix((id,), u32::MAX, None);251 Ok(())252 }253254 255 pub fn set_collection_properties(256 collection: &FungibleHandle<T>,257 sender: &T::CrossAccountId,258 properties: Vec<Property>,259 ) -> DispatchResult {260 <PalletCommon<T>>::set_collection_properties(collection, sender, properties.into_iter())261 }262263 264 pub fn delete_collection_properties(265 collection: &FungibleHandle<T>,266 sender: &T::CrossAccountId,267 property_keys: Vec<PropertyKey>,268 ) -> DispatchResult {269 <PalletCommon<T>>::delete_collection_properties(270 collection,271 sender,272 property_keys.into_iter(),273 )274 }275276 277 fn collection_has_tokens(collection_id: CollectionId) -> bool {278 <TotalSupply<T>>::get(collection_id) != 0279 }280281 282 283 284 pub fn burn(285 collection: &FungibleHandle<T>,286 owner: &T::CrossAccountId,287 amount: u128,288 ) -> DispatchResult {289 let total_supply = <TotalSupply<T>>::get(collection.id)290 .checked_sub(amount)291 .ok_or(<CommonError<T>>::TokenValueTooLow)?;292293 let balance = <Balance<T>>::get((collection.id, owner))294 .checked_sub(amount)295 .ok_or(<CommonError<T>>::TokenValueTooLow)?;296297 298 ensure!(!collection.flags.foreign, <CommonError<T>>::NoPermission);299300 if collection.permissions.access() == AccessMode::AllowList {301 collection.check_allowlist(owner)?;302 }303304 305306 if balance == 0 {307 <Balance<T>>::remove((collection.id, owner));308 <PalletStructure<T>>::unnest_if_nested(owner, collection.id, TokenId::default());309 } else {310 <Balance<T>>::insert((collection.id, owner), balance);311 }312 <TotalSupply<T>>::insert(collection.id, total_supply);313314 <PalletEvm<T>>::deposit_log(315 ERC20Events::Transfer {316 from: *owner.as_eth(),317 to: H160::default(),318 value: amount.into(),319 }320 .to_log(collection_id_to_address(collection.id)),321 );322 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(323 collection.id,324 TokenId::default(),325 owner.clone(),326 amount,327 ));328 Ok(())329 }330331 332 pub fn burn_foreign(333 collection: &FungibleHandle<T>,334 owner: &T::CrossAccountId,335 amount: u128,336 ) -> DispatchResult {337 let total_supply = <TotalSupply<T>>::get(collection.id)338 .checked_sub(amount)339 .ok_or(<CommonError<T>>::TokenValueTooLow)?;340341 let balance = <Balance<T>>::get((collection.id, owner))342 .checked_sub(amount)343 .ok_or(<CommonError<T>>::TokenValueTooLow)?;344 345346 if balance == 0 {347 <Balance<T>>::remove((collection.id, owner));348 <PalletStructure<T>>::unnest_if_nested(owner, collection.id, TokenId::default());349 } else {350 <Balance<T>>::insert((collection.id, owner), balance);351 }352 <TotalSupply<T>>::insert(collection.id, total_supply);353354 <PalletEvm<T>>::deposit_log(355 ERC20Events::Transfer {356 from: *owner.as_eth(),357 to: H160::default(),358 value: amount.into(),359 }360 .to_log(collection_id_to_address(collection.id)),361 );362 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(363 collection.id,364 TokenId::default(),365 owner.clone(),366 amount,367 ));368 Ok(())369 }370371 372 373 374 375 376 377 378 pub fn transfer(379 collection: &FungibleHandle<T>,380 from: &T::CrossAccountId,381 to: &T::CrossAccountId,382 amount: u128,383 nesting_budget: &dyn Budget,384 ) -> DispatchResultWithPostInfo {385 let depositor = from;386 Self::transfer_internal(collection, depositor, from, to, amount, nesting_budget)387 }388389 390 391 392 fn transfer_internal(393 collection: &FungibleHandle<T>,394 depositor: &T::CrossAccountId,395 from: &T::CrossAccountId,396 to: &T::CrossAccountId,397 amount: u128,398 nesting_budget: &dyn Budget,399 ) -> DispatchResultWithPostInfo {400 ensure!(401 collection.limits.transfers_enabled(),402 <CommonError<T>>::TransferNotAllowed,403 );404405 let mut actual_weight = <SelfWeightOf<T>>::transfer_raw();406407 if collection.permissions.access() == AccessMode::AllowList {408 collection.check_allowlist(from)?;409 collection.check_allowlist(to)?;410 actual_weight += <PalletCommonWeightOf<T>>::check_accesslist() * 2;411 }412 <PalletCommon<T>>::ensure_correct_receiver(to)?;413 let balance_from = <Balance<T>>::get((collection.id, from))414 .checked_sub(amount)415 .ok_or(<CommonError<T>>::TokenValueTooLow)?;416 let balance_to = if from != to && amount != 0 {417 Some(418 <Balance<T>>::get((collection.id, to))419 .checked_add(amount)420 .ok_or(ArithmeticError::Overflow)?,421 )422 } else {423 None424 };425426 427428 if let Some(balance_to) = balance_to {429 430431 <PalletStructure<T>>::nest_if_sent_to_token(432 depositor,433 to,434 collection.id,435 TokenId::default(),436 nesting_budget,437 )?;438439 if balance_from == 0 {440 <Balance<T>>::remove((collection.id, from));441 <PalletStructure<T>>::unnest_if_nested(from, collection.id, TokenId::default());442 } else {443 <Balance<T>>::insert((collection.id, from), balance_from);444 }445 <Balance<T>>::insert((collection.id, to), balance_to);446 }447448 <PalletEvm<T>>::deposit_log(449 ERC20Events::Transfer {450 from: *from.as_eth(),451 to: *to.as_eth(),452 value: amount.into(),453 }454 .to_log(collection_id_to_address(collection.id)),455 );456 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(457 collection.id,458 TokenId::default(),459 from.clone(),460 to.clone(),461 amount,462 ));463464 Ok(PostDispatchInfo {465 actual_weight: Some(actual_weight),466 pays_fee: Pays::Yes,467 })468 }469470 471 472 473 pub fn create_multiple_items_common(474 collection: &FungibleHandle<T>,475 sender: &T::CrossAccountId,476 data: BTreeMap<T::CrossAccountId, u128>,477 nesting_budget: &dyn Budget,478 ) -> DispatchResult {479 let total_supply = data480 .values()481 .copied()482 .try_fold(<TotalSupply<T>>::get(collection.id), |acc, v| {483 acc.checked_add(v)484 })485 .ok_or(ArithmeticError::Overflow)?;486487 for (to, _) in data.iter() {488 <PalletStructure<T>>::check_nesting(489 sender,490 to,491 collection.id,492 TokenId::default(),493 nesting_budget,494 )?;495 }496497 let updated_balances = data498 .into_iter()499 .map(|(user, amount)| {500 let updated_balance = <Balance<T>>::get((collection.id, &user))501 .checked_add(amount)502 .ok_or(ArithmeticError::Overflow)?;503 Ok((user, amount, updated_balance))504 })505 .collect::<Result<Vec<_>, DispatchError>>()?;506507 508509 <TotalSupply<T>>::insert(collection.id, total_supply);510 for (user, amount, updated_balance) in updated_balances {511 <Balance<T>>::insert((collection.id, &user), updated_balance);512 <PalletStructure<T>>::nest_if_sent_to_token_unchecked(513 &user,514 collection.id,515 TokenId::default(),516 );517 <PalletEvm<T>>::deposit_log(518 ERC20Events::Transfer {519 from: H160::default(),520 to: *user.as_eth(),521 value: amount.into(),522 }523 .to_log(collection_id_to_address(collection.id)),524 );525 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(526 collection.id,527 TokenId::default(),528 user.clone(),529 amount,530 ));531 }532533 Ok(())534 }535536 537 538 pub fn create_multiple_items(539 collection: &FungibleHandle<T>,540 sender: &T::CrossAccountId,541 data: BTreeMap<T::CrossAccountId, u128>,542 nesting_budget: &dyn Budget,543 ) -> DispatchResult {544 545 ensure!(!collection.flags.foreign, <CommonError<T>>::NoPermission);546547 if !collection.is_owner_or_admin(sender) {548 ensure!(549 collection.permissions.mint_mode(),550 <CommonError<T>>::PublicMintingNotAllowed551 );552 collection.check_allowlist(sender)?;553554 for (owner, _) in data.iter() {555 collection.check_allowlist(owner)?;556 }557 }558559 Self::create_multiple_items_common(collection, sender, data, nesting_budget)560 }561562 563 564 pub fn create_multiple_items_foreign(565 collection: &FungibleHandle<T>,566 sender: &T::CrossAccountId,567 data: BTreeMap<T::CrossAccountId, u128>,568 nesting_budget: &dyn Budget,569 ) -> DispatchResult {570 Self::create_multiple_items_common(collection, sender, data, nesting_budget)571 }572573 fn set_allowance_unchecked(574 collection: &FungibleHandle<T>,575 owner: &T::CrossAccountId,576 spender: &T::CrossAccountId,577 amount: u128,578 ) {579 if amount == 0 {580 <Allowance<T>>::remove((collection.id, owner, spender));581 } else {582 <Allowance<T>>::insert((collection.id, owner, spender), amount);583 }584585 <PalletEvm<T>>::deposit_log(586 ERC20Events::Approval {587 owner: *owner.as_eth(),588 spender: *spender.as_eth(),589 value: amount.into(),590 }591 .to_log(collection_id_to_address(collection.id)),592 );593 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(594 collection.id,595 TokenId(0),596 owner.clone(),597 spender.clone(),598 amount,599 ));600 }601602 603 604 605 606 607 608 pub fn set_allowance(609 collection: &FungibleHandle<T>,610 owner: &T::CrossAccountId,611 spender: &T::CrossAccountId,612 amount: u128,613 ) -> DispatchResult {614 if collection.permissions.access() == AccessMode::AllowList {615 collection.check_allowlist(owner)?;616 collection.check_allowlist(spender)?;617 }618619 if <Balance<T>>::get((collection.id, owner)) < amount {620 ensure!(621 collection.ignores_owned_amount(owner),622 <CommonError<T>>::CantApproveMoreThanOwned623 );624 }625626 627628 Self::set_allowance_unchecked(collection, owner, spender, amount);629 Ok(())630 }631632 633 634 635 636 637 638 639 pub fn set_allowance_from(640 collection: &FungibleHandle<T>,641 sender: &T::CrossAccountId,642 from: &T::CrossAccountId,643 to: &T::CrossAccountId,644 amount: u128,645 ) -> DispatchResult {646 if collection.permissions.access() == AccessMode::AllowList {647 collection.check_allowlist(sender)?;648 collection.check_allowlist(from)?;649 collection.check_allowlist(to)?;650 }651652 ensure!(653 sender.conv_eq(from),654 <CommonError<T>>::AddressIsNotEthMirror655 );656657 if <Balance<T>>::get((collection.id, from)) < amount {658 ensure!(659 collection.limits.owner_can_transfer()660 && (collection.is_owner_or_admin(sender) || collection.is_owner_or_admin(from)),661 <CommonError<T>>::CantApproveMoreThanOwned662 );663 }664665 666667 Self::set_allowance_unchecked(collection, from, to, amount);668 Ok(())669 }670671 672 673 674 675 676 677 678 fn check_allowed(679 collection: &FungibleHandle<T>,680 spender: &T::CrossAccountId,681 from: &T::CrossAccountId,682 amount: u128,683 nesting_budget: &dyn Budget,684 ) -> Result<Option<u128>, DispatchError> {685 if spender.conv_eq(from) {686 return Ok(None);687 }688 if collection.permissions.access() == AccessMode::AllowList {689 690 collection.check_allowlist(spender)?;691 }692693 if collection.ignores_token_restrictions(spender) {694 return Ok(Self::compute_allowance_decrease(695 collection, from, spender, amount,696 ));697 }698699 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {700 ensure!(701 <PalletStructure<T>>::check_indirectly_owned(702 spender.clone(),703 source.0,704 source.1,705 None,706 nesting_budget707 )?,708 <CommonError<T>>::ApprovedValueTooLow,709 );710 return Ok(None);711 }712713 let allowance = Self::compute_allowance_decrease(collection, from, spender, amount);714 ensure!(allowance.is_some(), <CommonError<T>>::ApprovedValueTooLow);715716 Ok(allowance)717 }718719 720 721 fn compute_allowance_decrease(722 collection: &FungibleHandle<T>,723 from: &T::CrossAccountId,724 spender: &T::CrossAccountId,725 amount: u128,726 ) -> Option<u128> {727 <Allowance<T>>::get((collection.id, from, spender)).checked_sub(amount)728 }729730 731 732 733 734 pub fn transfer_from(735 collection: &FungibleHandle<T>,736 spender: &T::CrossAccountId,737 from: &T::CrossAccountId,738 to: &T::CrossAccountId,739 amount: u128,740 nesting_budget: &dyn Budget,741 ) -> DispatchResultWithPostInfo {742 let allowance = Self::check_allowed(collection, spender, from, amount, nesting_budget)?;743744 745746 let mut result =747 Self::transfer_internal(collection, spender, from, to, amount, nesting_budget);748 add_weight_to_post_info(&mut result, <SelfWeightOf<T>>::check_allowed_raw());749 result?;750751 if let Some(allowance) = allowance {752 Self::set_allowance_unchecked(collection, from, spender, allowance);753 add_weight_to_post_info(754 &mut result,755 <SelfWeightOf<T>>::set_allowance_unchecked_raw(),756 )757 }758 result759 }760761 762 763 764 765 766 pub fn burn_from(767 collection: &FungibleHandle<T>,768 spender: &T::CrossAccountId,769 from: &T::CrossAccountId,770 amount: u128,771 nesting_budget: &dyn Budget,772 ) -> DispatchResult {773 let allowance = Self::check_allowed(collection, spender, from, amount, nesting_budget)?;774775 776777 Self::burn(collection, from, amount)?;778 if let Some(allowance) = allowance {779 Self::set_allowance_unchecked(collection, from, spender, allowance);780 }781 Ok(())782 }783784 785 786 787 788 789 790 791 pub fn create_item(792 collection: &FungibleHandle<T>,793 sender: &T::CrossAccountId,794 data: CreateItemData<T>,795 nesting_budget: &dyn Budget,796 ) -> DispatchResult {797 Self::create_multiple_items(798 collection,799 sender,800 [(data.0, data.1)].into_iter().collect(),801 nesting_budget,802 )803 }804805 806 807 808 809 pub fn create_item_foreign(810 collection: &FungibleHandle<T>,811 sender: &T::CrossAccountId,812 data: CreateItemData<T>,813 nesting_budget: &dyn Budget,814 ) -> DispatchResult {815 Self::create_multiple_items_foreign(816 collection,817 sender,818 [(data.0, data.1)].into_iter().collect(),819 nesting_budget,820 )821 }822823 824 825 826 827 828 829 pub fn token_owners(830 collection: CollectionId,831 _token: TokenId,832 ) -> Option<Vec<T::CrossAccountId>> {833 let res: Vec<T::CrossAccountId> = <Balance<T>>::iter_prefix((collection,))834 .map(|(owner, _amount)| owner)835 .take(10)836 .collect();837838 if res.is_empty() {839 None840 } else {841 Some(res)842 }843 }844}