1#![cfg_attr(not(feature = "std"), no_std)]23use core::ops::{Deref, DerefMut};4use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};5use sp_std::vec::Vec;6use account::CrossAccountId;7use frame_support::{8 dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo},9 ensure, fail,10 traits::{Imbalance, Get, Currency},11};12use pallet_evm::GasWeightMapping;13use up_data_structs::{14 COLLECTION_NUMBER_LIMIT, Collection, CollectionId, CreateItemData, ExistenceRequirement,15 MAX_COLLECTION_DESCRIPTION_LENGTH, MAX_COLLECTION_NAME_LENGTH, MAX_TOKEN_PREFIX_LENGTH,16 COLLECTION_ADMINS_LIMIT, MetaUpdatePermission, Pays, PostDispatchInfo, TokenId, Weight,17 WithdrawReasons, CollectionStats, MAX_TOKEN_OWNERSHIP, CollectionMode,18 NFT_SPONSOR_TRANSFER_TIMEOUT, FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,19 REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, MAX_SPONSOR_TIMEOUT, CUSTOM_DATA_LIMIT, CollectionLimits,20 CreateCollectionData, SponsorshipState,21};22pub use pallet::*;23use sp_core::H160;24use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};25pub mod account;26#[cfg(feature = "runtime-benchmarks")]27pub mod benchmarking;28pub mod erc;29pub mod eth;3031#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]32pub struct CollectionHandle<T: Config> {33 pub id: CollectionId,34 collection: Collection<T::AccountId>,35 pub recorder: SubstrateRecorder<T>,36}37impl<T: Config> WithRecorder<T> for CollectionHandle<T> {38 fn recorder(&self) -> &SubstrateRecorder<T> {39 &self.recorder40 }41 fn into_recorder(self) -> SubstrateRecorder<T> {42 self.recorder43 }44}45impl<T: Config> CollectionHandle<T> {46 pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {47 <CollectionById<T>>::get(id).map(|collection| Self {48 id,49 collection,50 recorder: SubstrateRecorder::new(eth::collection_id_to_address(id), gas_limit),51 })52 }53 pub fn new(id: CollectionId) -> Option<Self> {54 Self::new_with_gas_limit(id, u64::MAX)55 }56 pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {57 Ok(Self::new(id).ok_or(<Error<T>>::CollectionNotFound)?)58 }59 pub fn log(&self, log: impl evm_coder::ToLog) {60 self.recorder.log(log)61 }62 pub fn consume_store_reads(&self, reads: u64) -> evm_coder::execution::Result<()> {63 self.recorder64 .consume_gas(T::GasWeightMapping::weight_to_gas(65 <T as frame_system::Config>::DbWeight::get()66 .read67 .saturating_mul(reads),68 ))69 }70 pub fn consume_store_writes(&self, writes: u64) -> evm_coder::execution::Result<()> {71 self.recorder72 .consume_gas(T::GasWeightMapping::weight_to_gas(73 <T as frame_system::Config>::DbWeight::get()74 .write75 .saturating_mul(writes),76 ))77 }78 pub fn submit_logs(self) {79 self.recorder.submit_logs()80 }81 pub fn save(self) -> DispatchResult {82 self.recorder.submit_logs();83 <CollectionById<T>>::insert(self.id, self.collection);84 Ok(())85 }86}87impl<T: Config> Deref for CollectionHandle<T> {88 type Target = Collection<T::AccountId>;8990 fn deref(&self) -> &Self::Target {91 &self.collection92 }93}9495impl<T: Config> DerefMut for CollectionHandle<T> {96 fn deref_mut(&mut self) -> &mut Self::Target {97 &mut self.collection98 }99}100101impl<T: Config> CollectionHandle<T> {102 pub fn check_is_owner(&self, subject: &T::CrossAccountId) -> DispatchResult {103 ensure!(*subject.as_sub() == self.owner, <Error<T>>::NoPermission);104 Ok(())105 }106 pub fn is_owner_or_admin(&self, subject: &T::CrossAccountId) -> bool {107 *subject.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, subject))108 }109 pub fn check_is_owner_or_admin(&self, subject: &T::CrossAccountId) -> DispatchResult {110 ensure!(self.is_owner_or_admin(subject), <Error<T>>::NoPermission);111 Ok(())112 }113 pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> bool {114 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)115 }116 pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> bool {117 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)118 }119 pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {120 ensure!(121 <Allowlist<T>>::get((self.id, user)),122 <Error<T>>::AddressNotInAllowlist123 );124 Ok(())125 }126127 pub fn check_can_update_meta(128 &self,129 subject: &T::CrossAccountId,130 item_owner: &T::CrossAccountId,131 ) -> DispatchResult {132 match self.meta_update_permission {133 MetaUpdatePermission::ItemOwner => {134 ensure!(subject == item_owner, <Error<T>>::NoPermission);135 Ok(())136 }137 MetaUpdatePermission::Admin => self.check_is_owner_or_admin(subject),138 MetaUpdatePermission::None => fail!(<Error<T>>::NoPermission),139 }140 }141}142143#[frame_support::pallet]144pub mod pallet {145 use super::*;146 use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key};147 use account::CrossAccountId;148 use frame_support::traits::Currency;149 use up_data_structs::TokenId;150 use scale_info::TypeInfo;151152 #[pallet::config]153 pub trait Config: frame_system::Config + pallet_evm_coder_substrate::Config + TypeInfo {154 type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;155156 type CrossAccountId: CrossAccountId<Self::AccountId>;157158 type EvmAddressMapping: pallet_evm::AddressMapping<Self::AccountId>;159 type EvmBackwardsAddressMapping: up_evm_mapping::EvmBackwardsAddressMapping<Self::AccountId>;160161 type Currency: Currency<Self::AccountId>;162 type CollectionCreationPrice: Get<163 <<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,164 >;165 type TreasuryAccountId: Get<Self::AccountId>;166 }167168 #[pallet::pallet]169 #[pallet::generate_store(pub(super) trait Store)]170 pub struct Pallet<T>(_);171172 #[pallet::extra_constants]173 impl<T: Config> Pallet<T> {174 pub fn collection_admins_limit() -> u32 {175 COLLECTION_ADMINS_LIMIT176 }177 }178179 #[pallet::event]180 #[pallet::generate_deposit(pub fn deposit_event)]181 pub enum Event<T: Config> {182 183 184 185 186 187 188 189 190 191 CollectionCreated(CollectionId, u8, T::AccountId),192193 194 195 196 197 198 CollectionDestroyed(CollectionId),199200 201 202 203 204 205 206 207 208 209 210 211 ItemCreated(CollectionId, TokenId, T::CrossAccountId, u128),212213 214 215 216 217 218 219 220 221 222 223 224 ItemDestroyed(CollectionId, TokenId, T::CrossAccountId, u128),225226 227 228 229 230 231 232 233 234 235 236 237 Transfer(238 CollectionId,239 TokenId,240 T::CrossAccountId,241 T::CrossAccountId,242 u128,243 ),244245 246 247 248 249 250 251 252 253 254 Approved(255 CollectionId,256 TokenId,257 T::CrossAccountId,258 T::CrossAccountId,259 u128,260 ),261 }262263 #[pallet::error]264 pub enum Error<T> {265 266 CollectionNotFound,267 268 MustBeTokenOwner,269 270 NoPermission,271 272 PublicMintingNotAllowed,273 274 AddressNotInAllowlist,275276 277 CollectionNameLimitExceeded,278 279 CollectionDescriptionLimitExceeded,280 281 CollectionTokenPrefixLimitExceeded,282 283 TotalCollectionsLimitExceeded,284 285 TokenVariableDataLimitExceeded,286 287 CollectionAdminCountExceeded,288 289 CollectionLimitBoundsExceeded,290 291 OwnerPermissionsCantBeReverted,292293 294 TransferNotAllowed,295 296 AccountTokenLimitExceeded,297 298 CollectionTokenLimitExceeded,299 300 MetadataFlagFrozen,301302 303 TokenNotFound,304 305 TokenValueTooLow,306 307 TokenValueNotEnough,308 309 CantApproveMoreThanOwned,310311 312 AddressIsZero,313 314 UnsupportedOperation,315 }316317 #[pallet::storage]318 pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;319 #[pallet::storage]320 pub type DestroyedCollectionCount<T> =321 StorageValue<Value = CollectionId, QueryKind = ValueQuery>;322323 324 #[pallet::storage]325 pub type CollectionById<T> = StorageMap<326 Hasher = Blake2_128Concat,327 Key = CollectionId,328 Value = Collection<<T as frame_system::Config>::AccountId>,329 QueryKind = OptionQuery,330 >;331332 #[pallet::storage]333 pub type AdminAmount<T> = StorageMap<334 Hasher = Blake2_128Concat,335 Key = CollectionId,336 Value = u32,337 QueryKind = ValueQuery,338 >;339340 341 #[pallet::storage]342 pub type IsAdmin<T: Config> = StorageNMap<343 Key = (344 Key<Blake2_128Concat, CollectionId>,345 Key<Blake2_128Concat, T::CrossAccountId>,346 ),347 Value = bool,348 QueryKind = ValueQuery,349 >;350351 352 #[pallet::storage]353 pub type Allowlist<T: Config> = StorageNMap<354 Key = (355 Key<Blake2_128Concat, CollectionId>,356 Key<Blake2_128Concat, T::CrossAccountId>,357 ),358 Value = bool,359 QueryKind = ValueQuery,360 >;361362 363 #[pallet::storage]364 pub type DummyStorageValue<T> =365 StorageValue<Value = (CollectionStats, CollectionId, TokenId), QueryKind = OptionQuery>;366}367368impl<T: Config> Pallet<T> {369 370 pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {371 ensure!(372 &T::CrossAccountId::from_eth(H160([0; 20])) != receiver,373 <Error<T>>::AddressIsZero374 );375 Ok(())376 }377 pub fn adminlist(collection: CollectionId) -> Vec<T::CrossAccountId> {378 <IsAdmin<T>>::iter_prefix((collection,))379 .map(|(a, _)| a)380 .collect()381 }382 pub fn allowlist(collection: CollectionId) -> Vec<T::CrossAccountId> {383 <Allowlist<T>>::iter_prefix((collection,))384 .map(|(a, _)| a)385 .collect()386 }387 pub fn allowed(collection: CollectionId, user: T::CrossAccountId) -> bool {388 <Allowlist<T>>::get((collection, user))389 }390 pub fn collection_stats() -> CollectionStats {391 let created = <CreatedCollectionCount<T>>::get();392 let destroyed = <DestroyedCollectionCount<T>>::get();393 CollectionStats {394 created: created.0,395 destroyed: destroyed.0,396 alive: created.0 - destroyed.0,397 }398 }399}400401impl<T: Config> Pallet<T> {402 pub fn init_collection(403 owner: T::AccountId,404 data: CreateCollectionData<T::AccountId>,405 ) -> Result<CollectionId, DispatchError> {406 {407 ensure!(408 data.name.len() <= MAX_COLLECTION_NAME_LENGTH,409 Error::<T>::CollectionNameLimitExceeded410 );411 ensure!(412 data.description.len() <= MAX_COLLECTION_DESCRIPTION_LENGTH,413 Error::<T>::CollectionDescriptionLimitExceeded414 );415 ensure!(416 data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH,417 Error::<T>::CollectionTokenPrefixLimitExceeded418 );419 }420421 let created_count = <CreatedCollectionCount<T>>::get()422 .0423 .checked_add(1)424 .ok_or(ArithmeticError::Overflow)?;425 let destroyed_count = <DestroyedCollectionCount<T>>::get().0;426 let id = CollectionId(created_count);427428 429 ensure!(430 created_count - destroyed_count <= COLLECTION_NUMBER_LIMIT,431 <Error<T>>::TotalCollectionsLimitExceeded432 );433434 435436 let collection = Collection {437 owner: owner.clone(),438 name: data.name,439 mode: data.mode.clone(),440 mint_mode: false,441 access: data.access.unwrap_or_default(),442 description: data.description,443 token_prefix: data.token_prefix,444 offchain_schema: data.offchain_schema,445 schema_version: data.schema_version.unwrap_or_default(),446 sponsorship: data447 .pending_sponsor448 .map(SponsorshipState::Unconfirmed)449 .unwrap_or_default(),450 variable_on_chain_schema: data.variable_on_chain_schema,451 const_on_chain_schema: data.const_on_chain_schema,452 limits: data453 .limits454 .map(|limits| Self::clamp_limits(data.mode.clone(), &Default::default(), limits))455 .unwrap_or_else(|| Ok(CollectionLimits::default()))?,456 meta_update_permission: data.meta_update_permission.unwrap_or_default(),457 };458459 460 {461 let mut imbalance =462 <<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();463 imbalance.subsume(464 <<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(465 &T::TreasuryAccountId::get(),466 T::CollectionCreationPrice::get(),467 ),468 );469 <T as Config>::Currency::settle(470 &owner,471 imbalance,472 WithdrawReasons::TRANSFER,473 ExistenceRequirement::KeepAlive,474 )475 .map_err(|_| Error::<T>::NoPermission)?;476 }477478 <CreatedCollectionCount<T>>::put(created_count);479 <Pallet<T>>::deposit_event(Event::CollectionCreated(id, data.mode.id(), owner.clone()));480 <CollectionById<T>>::insert(id, collection);481 Ok(id)482 }483484 pub fn destroy_collection(485 collection: CollectionHandle<T>,486 sender: &T::CrossAccountId,487 ) -> DispatchResult {488 ensure!(489 collection.limits.owner_can_destroy(),490 <Error<T>>::NoPermission,491 );492 collection.check_is_owner(sender)?;493494 let destroyed_collections = <DestroyedCollectionCount<T>>::get()495 .0496 .checked_add(1)497 .ok_or(ArithmeticError::Overflow)?;498499 500501 <DestroyedCollectionCount<T>>::put(destroyed_collections);502 <CollectionById<T>>::remove(collection.id);503 <AdminAmount<T>>::remove(collection.id);504 <IsAdmin<T>>::remove_prefix((collection.id,), None);505 <Allowlist<T>>::remove_prefix((collection.id,), None);506507 <Pallet<T>>::deposit_event(Event::CollectionDestroyed(collection.id));508 Ok(())509 }510511 pub fn toggle_allowlist(512 collection: &CollectionHandle<T>,513 sender: &T::CrossAccountId,514 user: &T::CrossAccountId,515 allowed: bool,516 ) -> DispatchResult {517 collection.check_is_owner_or_admin(sender)?;518519 520521 if allowed {522 <Allowlist<T>>::insert((collection.id, user), true);523 } else {524 <Allowlist<T>>::remove((collection.id, user));525 }526527 Ok(())528 }529530 pub fn toggle_admin(531 collection: &CollectionHandle<T>,532 sender: &T::CrossAccountId,533 user: &T::CrossAccountId,534 admin: bool,535 ) -> DispatchResult {536 collection.check_is_owner_or_admin(sender)?;537538 let was_admin = <IsAdmin<T>>::get((collection.id, user));539 if was_admin == admin {540 return Ok(());541 }542 let amount = <AdminAmount<T>>::get(collection.id);543544 if admin {545 let amount = amount546 .checked_add(1)547 .ok_or(<Error<T>>::CollectionAdminCountExceeded)?;548 ensure!(549 amount <= Self::collection_admins_limit(),550 <Error<T>>::CollectionAdminCountExceeded,551 );552553 554555 <AdminAmount<T>>::insert(collection.id, amount);556 <IsAdmin<T>>::insert((collection.id, user), true);557 } else {558 <AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));559 <IsAdmin<T>>::remove((collection.id, user));560 }561562 Ok(())563 }564565 pub fn clamp_limits(566 mode: CollectionMode,567 old_limit: &CollectionLimits,568 mut new_limit: CollectionLimits,569 ) -> Result<CollectionLimits, DispatchError> {570 macro_rules! limit_default {571 ($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{572 $(573 if let Some($new) = $new.$field {574 let $old = $old.$field($($arg)?);575 let _ = $new;576 let _ = $old;577 $check578 } else {579 $new.$field = $old.$field580 }581 )*582 }};583 }584585 limit_default!(old_limit, new_limit,586 account_token_ownership_limit => ensure!(587 new_limit <= MAX_TOKEN_OWNERSHIP,588 <Error<T>>::CollectionLimitBoundsExceeded,589 ),590 sponsor_transfer_timeout(match mode {591 CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,592 CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,593 CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,594 }) => ensure!(595 new_limit <= MAX_SPONSOR_TIMEOUT,596 <Error<T>>::CollectionLimitBoundsExceeded,597 ),598 sponsored_data_size => ensure!(599 new_limit <= CUSTOM_DATA_LIMIT,600 <Error<T>>::CollectionLimitBoundsExceeded,601 ),602 token_limit => ensure!(603 old_limit >= new_limit && new_limit > 0,604 <Error<T>>::CollectionTokenLimitExceeded605 ),606 owner_can_transfer => ensure!(607 old_limit || !new_limit,608 <Error<T>>::OwnerPermissionsCantBeReverted,609 ),610 owner_can_destroy => ensure!(611 old_limit || !new_limit,612 <Error<T>>::OwnerPermissionsCantBeReverted,613 ),614 sponsored_data_rate_limit => {},615 transfers_enabled => {},616 );617 Ok(new_limit)618 }619}620621#[macro_export]622macro_rules! unsupported {623 () => {624 Err(<Error<T>>::UnsupportedOperation.into())625 };626}627628629pub trait CommonWeightInfo {630 fn create_item() -> Weight;631 fn create_multiple_items(amount: u32) -> Weight;632 fn burn_item() -> Weight;633 fn transfer() -> Weight;634 fn approve() -> Weight;635 fn transfer_from() -> Weight;636 fn burn_from() -> Weight;637 fn set_variable_metadata(bytes: u32) -> Weight;638}639640pub trait CommonCollectionOperations<T: Config> {641 fn create_item(642 &self,643 sender: T::CrossAccountId,644 to: T::CrossAccountId,645 data: CreateItemData,646 ) -> DispatchResultWithPostInfo;647 fn create_multiple_items(648 &self,649 sender: T::CrossAccountId,650 to: T::CrossAccountId,651 data: Vec<CreateItemData>,652 ) -> DispatchResultWithPostInfo;653 fn burn_item(654 &self,655 sender: T::CrossAccountId,656 token: TokenId,657 amount: u128,658 ) -> DispatchResultWithPostInfo;659660 fn transfer(661 &self,662 sender: T::CrossAccountId,663 to: T::CrossAccountId,664 token: TokenId,665 amount: u128,666 ) -> DispatchResultWithPostInfo;667 fn approve(668 &self,669 sender: T::CrossAccountId,670 spender: T::CrossAccountId,671 token: TokenId,672 amount: u128,673 ) -> DispatchResultWithPostInfo;674 fn transfer_from(675 &self,676 sender: T::CrossAccountId,677 from: T::CrossAccountId,678 to: T::CrossAccountId,679 token: TokenId,680 amount: u128,681 ) -> DispatchResultWithPostInfo;682 fn burn_from(683 &self,684 sender: T::CrossAccountId,685 from: T::CrossAccountId,686 token: TokenId,687 amount: u128,688 ) -> DispatchResultWithPostInfo;689690 fn set_variable_metadata(691 &self,692 sender: T::CrossAccountId,693 token: TokenId,694 data: Vec<u8>,695 ) -> DispatchResultWithPostInfo;696697 fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;698 fn token_exists(&self, token: TokenId) -> bool;699 fn last_token_id(&self) -> TokenId;700701 fn token_owner(&self, token: TokenId) -> T::CrossAccountId;702 fn const_metadata(&self, token: TokenId) -> Vec<u8>;703 fn variable_metadata(&self, token: TokenId) -> Vec<u8>;704705 706 fn collection_tokens(&self) -> u32;707 708 fn account_balance(&self, account: T::CrossAccountId) -> u32;709 710 fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;711 fn allowance(712 &self,713 sender: T::CrossAccountId,714 spender: T::CrossAccountId,715 token: TokenId,716 ) -> u128;717}718719720pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {721 let post_info = PostDispatchInfo {722 actual_weight: Some(weight),723 pays_fee: Pays::Yes,724 };725 match res {726 Ok(()) => Ok(post_info),727 Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),728 }729}