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 BoundedVec,12};13use pallet_evm::GasWeightMapping;14use up_data_structs::{15 COLLECTION_NUMBER_LIMIT, Collection, CollectionId, CreateItemData, ExistenceRequirement,16 MAX_TOKEN_PREFIX_LENGTH,17 COLLECTION_ADMINS_LIMIT, MetaUpdatePermission, Pays, PostDispatchInfo, TokenId, Weight,18 WithdrawReasons, CollectionStats, MAX_TOKEN_OWNERSHIP, CollectionMode,19 NFT_SPONSOR_TRANSFER_TIMEOUT, FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,20 REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, MAX_SPONSOR_TIMEOUT, CUSTOM_DATA_LIMIT, CollectionLimits,21 CustomDataLimit, CreateCollectionData, SponsorshipState,22};23pub use pallet::*;24use sp_core::H160;25use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};26pub mod account;27#[cfg(feature = "runtime-benchmarks")]28pub mod benchmarking;29pub mod erc;30pub mod eth;3132#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]33pub struct CollectionHandle<T: Config> {34 pub id: CollectionId,35 collection: Collection<T::AccountId>,36 pub recorder: SubstrateRecorder<T>,37}38impl<T: Config> WithRecorder<T> for CollectionHandle<T> {39 fn recorder(&self) -> &SubstrateRecorder<T> {40 &self.recorder41 }42 fn into_recorder(self) -> SubstrateRecorder<T> {43 self.recorder44 }45}46impl<T: Config> CollectionHandle<T> {47 pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {48 <CollectionById<T>>::get(id).map(|collection| Self {49 id,50 collection,51 recorder: SubstrateRecorder::new(eth::collection_id_to_address(id), gas_limit),52 })53 }54 pub fn new(id: CollectionId) -> Option<Self> {55 Self::new_with_gas_limit(id, u64::MAX)56 }57 pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {58 Ok(Self::new(id).ok_or(<Error<T>>::CollectionNotFound)?)59 }60 pub fn log_mirrored(&self, log: impl evm_coder::ToLog) {61 self.recorder.log_mirrored(log)62 }63 pub fn log_direct(&self, log: impl evm_coder::ToLog) {64 self.recorder.log_direct(log)65 }66 pub fn consume_store_reads(&self, reads: u64) -> evm_coder::execution::Result<()> {67 self.recorder68 .consume_gas(T::GasWeightMapping::weight_to_gas(69 <T as frame_system::Config>::DbWeight::get()70 .read71 .saturating_mul(reads),72 ))73 }74 pub fn consume_store_writes(&self, writes: u64) -> evm_coder::execution::Result<()> {75 self.recorder76 .consume_gas(T::GasWeightMapping::weight_to_gas(77 <T as frame_system::Config>::DbWeight::get()78 .write79 .saturating_mul(writes),80 ))81 }82 pub fn submit_logs(self) {83 self.recorder.submit_logs()84 }85 pub fn save(self) -> DispatchResult {86 self.recorder.submit_logs();87 <CollectionById<T>>::insert(self.id, self.collection);88 Ok(())89 }90}91impl<T: Config> Deref for CollectionHandle<T> {92 type Target = Collection<T::AccountId>;9394 fn deref(&self) -> &Self::Target {95 &self.collection96 }97}9899impl<T: Config> DerefMut for CollectionHandle<T> {100 fn deref_mut(&mut self) -> &mut Self::Target {101 &mut self.collection102 }103}104105impl<T: Config> CollectionHandle<T> {106 pub fn check_is_owner(&self, subject: &T::CrossAccountId) -> DispatchResult {107 ensure!(*subject.as_sub() == self.owner, <Error<T>>::NoPermission);108 Ok(())109 }110 pub fn is_owner_or_admin(&self, subject: &T::CrossAccountId) -> bool {111 *subject.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, subject))112 }113 pub fn check_is_owner_or_admin(&self, subject: &T::CrossAccountId) -> DispatchResult {114 ensure!(self.is_owner_or_admin(subject), <Error<T>>::NoPermission);115 Ok(())116 }117 pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> bool {118 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)119 }120 pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> bool {121 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)122 }123 pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {124 ensure!(125 <Allowlist<T>>::get((self.id, user)),126 <Error<T>>::AddressNotInAllowlist127 );128 Ok(())129 }130131 pub fn check_can_update_meta(132 &self,133 subject: &T::CrossAccountId,134 item_owner: &T::CrossAccountId,135 ) -> DispatchResult {136 match self.meta_update_permission {137 MetaUpdatePermission::ItemOwner => {138 ensure!(subject == item_owner, <Error<T>>::NoPermission);139 Ok(())140 }141 MetaUpdatePermission::Admin => self.check_is_owner_or_admin(subject),142 MetaUpdatePermission::None => fail!(<Error<T>>::NoPermission),143 }144 }145}146147#[frame_support::pallet]148pub mod pallet {149 use super::*;150 use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key};151 use account::CrossAccountId;152 use frame_support::traits::Currency;153 use up_data_structs::TokenId;154 use scale_info::TypeInfo;155156 #[pallet::config]157 pub trait Config: frame_system::Config + pallet_evm_coder_substrate::Config + TypeInfo {158 type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;159160 type CrossAccountId: CrossAccountId<Self::AccountId>;161162 type EvmAddressMapping: pallet_evm::AddressMapping<Self::AccountId>;163 type EvmBackwardsAddressMapping: up_evm_mapping::EvmBackwardsAddressMapping<Self::AccountId>;164165 type Currency: Currency<Self::AccountId>;166 type CollectionCreationPrice: Get<167 <<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,168 >;169 type TreasuryAccountId: Get<Self::AccountId>;170 }171172 #[pallet::pallet]173 #[pallet::generate_store(pub(super) trait Store)]174 pub struct Pallet<T>(_);175176 #[pallet::extra_constants]177 impl<T: Config> Pallet<T> {178 pub fn collection_admins_limit() -> u32 {179 COLLECTION_ADMINS_LIMIT180 }181 }182183 #[pallet::event]184 #[pallet::generate_deposit(pub fn deposit_event)]185 pub enum Event<T: Config> {186 187 188 189 190 191 192 193 194 195 CollectionCreated(CollectionId, u8, T::AccountId),196197 198 199 200 201 202 CollectionDestroyed(CollectionId),203204 205 206 207 208 209 210 211 212 213 214 215 ItemCreated(CollectionId, TokenId, T::CrossAccountId, u128),216217 218 219 220 221 222 223 224 225 226 227 228 ItemDestroyed(CollectionId, TokenId, T::CrossAccountId, u128),229230 231 232 233 234 235 236 237 238 239 240 241 Transfer(242 CollectionId,243 TokenId,244 T::CrossAccountId,245 T::CrossAccountId,246 u128,247 ),248249 250 251 252 253 254 255 256 257 258 Approved(259 CollectionId,260 TokenId,261 T::CrossAccountId,262 T::CrossAccountId,263 u128,264 ),265 }266267 #[pallet::error]268 pub enum Error<T> {269 270 CollectionNotFound,271 272 MustBeTokenOwner,273 274 NoPermission,275 276 PublicMintingNotAllowed,277 278 AddressNotInAllowlist,279280 281 CollectionNameLimitExceeded,282 283 CollectionDescriptionLimitExceeded,284 285 CollectionTokenPrefixLimitExceeded,286 287 TotalCollectionsLimitExceeded,288 289 TokenVariableDataLimitExceeded,290 291 CollectionAdminCountExceeded,292 293 CollectionLimitBoundsExceeded,294 295 OwnerPermissionsCantBeReverted,296297 298 TransferNotAllowed,299 300 AccountTokenLimitExceeded,301 302 CollectionTokenLimitExceeded,303 304 MetadataFlagFrozen,305306 307 TokenNotFound,308 309 TokenValueTooLow,310 311 TokenValueNotEnough,312 313 CantApproveMoreThanOwned,314315 316 AddressIsZero,317 318 UnsupportedOperation,319 }320321 #[pallet::storage]322 pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;323 #[pallet::storage]324 pub type DestroyedCollectionCount<T> =325 StorageValue<Value = CollectionId, QueryKind = ValueQuery>;326327 328 #[pallet::storage]329 pub type CollectionById<T> = StorageMap<330 Hasher = Blake2_128Concat,331 Key = CollectionId,332 Value = Collection<<T as frame_system::Config>::AccountId>,333 QueryKind = OptionQuery,334 >;335336 #[pallet::storage]337 pub type AdminAmount<T> = StorageMap<338 Hasher = Blake2_128Concat,339 Key = CollectionId,340 Value = u32,341 QueryKind = ValueQuery,342 >;343344 345 #[pallet::storage]346 pub type IsAdmin<T: Config> = StorageNMap<347 Key = (348 Key<Blake2_128Concat, CollectionId>,349 Key<Blake2_128Concat, T::CrossAccountId>,350 ),351 Value = bool,352 QueryKind = ValueQuery,353 >;354355 356 #[pallet::storage]357 pub type Allowlist<T: Config> = StorageNMap<358 Key = (359 Key<Blake2_128Concat, CollectionId>,360 Key<Blake2_128Concat, T::CrossAccountId>,361 ),362 Value = bool,363 QueryKind = ValueQuery,364 >;365366 367 #[pallet::storage]368 pub type DummyStorageValue<T> =369 StorageValue<Value = (CollectionStats, CollectionId, TokenId), QueryKind = OptionQuery>;370}371372impl<T: Config> Pallet<T> {373 374 pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {375 ensure!(376 &T::CrossAccountId::from_eth(H160([0; 20])) != receiver,377 <Error<T>>::AddressIsZero378 );379 Ok(())380 }381 pub fn adminlist(collection: CollectionId) -> Vec<T::CrossAccountId> {382 <IsAdmin<T>>::iter_prefix((collection,))383 .map(|(a, _)| a)384 .collect()385 }386 pub fn allowlist(collection: CollectionId) -> Vec<T::CrossAccountId> {387 <Allowlist<T>>::iter_prefix((collection,))388 .map(|(a, _)| a)389 .collect()390 }391 pub fn allowed(collection: CollectionId, user: T::CrossAccountId) -> bool {392 <Allowlist<T>>::get((collection, user))393 }394 pub fn collection_stats() -> CollectionStats {395 let created = <CreatedCollectionCount<T>>::get();396 let destroyed = <DestroyedCollectionCount<T>>::get();397 CollectionStats {398 created: created.0,399 destroyed: destroyed.0,400 alive: created.0 - destroyed.0,401 }402 }403}404405impl<T: Config> Pallet<T> {406 pub fn init_collection(407 owner: T::AccountId,408 data: CreateCollectionData<T::AccountId>,409 ) -> Result<CollectionId, DispatchError> {410 {411 ensure!(412 data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH as usize,413 Error::<T>::CollectionTokenPrefixLimitExceeded414 );415 }416417 let created_count = <CreatedCollectionCount<T>>::get()418 .0419 .checked_add(1)420 .ok_or(ArithmeticError::Overflow)?;421 let destroyed_count = <DestroyedCollectionCount<T>>::get().0;422 let id = CollectionId(created_count);423424 425 ensure!(426 created_count - destroyed_count <= COLLECTION_NUMBER_LIMIT,427 <Error<T>>::TotalCollectionsLimitExceeded428 );429430 431432 let collection = Collection {433 owner: owner.clone(),434 name: data.name,435 mode: data.mode.clone(),436 mint_mode: false,437 access: data.access.unwrap_or_default(),438 description: data.description,439 token_prefix: data.token_prefix,440 offchain_schema: data.offchain_schema,441 schema_version: data.schema_version.unwrap_or_default(),442 sponsorship: data443 .pending_sponsor444 .map(SponsorshipState::Unconfirmed)445 .unwrap_or_default(),446 variable_on_chain_schema: data.variable_on_chain_schema,447 const_on_chain_schema: data.const_on_chain_schema,448 limits: data449 .limits450 .map(|limits| Self::clamp_limits(data.mode.clone(), &Default::default(), limits))451 .unwrap_or_else(|| Ok(CollectionLimits::default()))?,452 meta_update_permission: data.meta_update_permission.unwrap_or_default(),453 };454455 456 {457 let mut imbalance =458 <<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();459 imbalance.subsume(460 <<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(461 &T::TreasuryAccountId::get(),462 T::CollectionCreationPrice::get(),463 ),464 );465 <T as Config>::Currency::settle(466 &owner,467 imbalance,468 WithdrawReasons::TRANSFER,469 ExistenceRequirement::KeepAlive,470 )471 .map_err(|_| Error::<T>::NoPermission)?;472 }473474 <CreatedCollectionCount<T>>::put(created_count);475 <Pallet<T>>::deposit_event(Event::CollectionCreated(id, data.mode.id(), owner.clone()));476 <CollectionById<T>>::insert(id, collection);477 Ok(id)478 }479480 pub fn destroy_collection(481 collection: CollectionHandle<T>,482 sender: &T::CrossAccountId,483 ) -> DispatchResult {484 ensure!(485 collection.limits.owner_can_destroy(),486 <Error<T>>::NoPermission,487 );488 collection.check_is_owner(sender)?;489490 let destroyed_collections = <DestroyedCollectionCount<T>>::get()491 .0492 .checked_add(1)493 .ok_or(ArithmeticError::Overflow)?;494495 496497 <DestroyedCollectionCount<T>>::put(destroyed_collections);498 <CollectionById<T>>::remove(collection.id);499 <AdminAmount<T>>::remove(collection.id);500 <IsAdmin<T>>::remove_prefix((collection.id,), None);501 <Allowlist<T>>::remove_prefix((collection.id,), None);502503 <Pallet<T>>::deposit_event(Event::CollectionDestroyed(collection.id));504 Ok(())505 }506507 pub fn toggle_allowlist(508 collection: &CollectionHandle<T>,509 sender: &T::CrossAccountId,510 user: &T::CrossAccountId,511 allowed: bool,512 ) -> DispatchResult {513 collection.check_is_owner_or_admin(sender)?;514515 516517 if allowed {518 <Allowlist<T>>::insert((collection.id, user), true);519 } else {520 <Allowlist<T>>::remove((collection.id, user));521 }522523 Ok(())524 }525526 pub fn toggle_admin(527 collection: &CollectionHandle<T>,528 sender: &T::CrossAccountId,529 user: &T::CrossAccountId,530 admin: bool,531 ) -> DispatchResult {532 collection.check_is_owner_or_admin(sender)?;533534 let was_admin = <IsAdmin<T>>::get((collection.id, user));535 if was_admin == admin {536 return Ok(());537 }538 let amount = <AdminAmount<T>>::get(collection.id);539540 if admin {541 let amount = amount542 .checked_add(1)543 .ok_or(<Error<T>>::CollectionAdminCountExceeded)?;544 ensure!(545 amount <= Self::collection_admins_limit(),546 <Error<T>>::CollectionAdminCountExceeded,547 );548549 550551 <AdminAmount<T>>::insert(collection.id, amount);552 <IsAdmin<T>>::insert((collection.id, user), true);553 } else {554 <AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));555 <IsAdmin<T>>::remove((collection.id, user));556 }557558 Ok(())559 }560561 pub fn clamp_limits(562 mode: CollectionMode,563 old_limit: &CollectionLimits,564 mut new_limit: CollectionLimits,565 ) -> Result<CollectionLimits, DispatchError> {566 macro_rules! limit_default {567 ($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{568 $(569 if let Some($new) = $new.$field {570 let $old = $old.$field($($arg)?);571 let _ = $new;572 let _ = $old;573 $check574 } else {575 $new.$field = $old.$field576 }577 )*578 }};579 }580581 limit_default!(old_limit, new_limit,582 account_token_ownership_limit => ensure!(583 new_limit <= MAX_TOKEN_OWNERSHIP,584 <Error<T>>::CollectionLimitBoundsExceeded,585 ),586 sponsor_transfer_timeout(match mode {587 CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,588 CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,589 CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,590 }) => ensure!(591 new_limit <= MAX_SPONSOR_TIMEOUT,592 <Error<T>>::CollectionLimitBoundsExceeded,593 ),594 sponsored_data_size => ensure!(595 new_limit <= CUSTOM_DATA_LIMIT,596 <Error<T>>::CollectionLimitBoundsExceeded,597 ),598 token_limit => ensure!(599 old_limit >= new_limit && new_limit > 0,600 <Error<T>>::CollectionTokenLimitExceeded601 ),602 owner_can_transfer => ensure!(603 old_limit || !new_limit,604 <Error<T>>::OwnerPermissionsCantBeReverted,605 ),606 owner_can_destroy => ensure!(607 old_limit || !new_limit,608 <Error<T>>::OwnerPermissionsCantBeReverted,609 ),610 sponsored_data_rate_limit => {},611 transfers_enabled => {},612 );613 Ok(new_limit)614 }615}616617#[macro_export]618macro_rules! unsupported {619 () => {620 Err(<Error<T>>::UnsupportedOperation.into())621 };622}623624625pub trait CommonWeightInfo {626 fn create_item() -> Weight;627 fn create_multiple_items(amount: u32) -> Weight;628 fn burn_item() -> Weight;629 fn transfer() -> Weight;630 fn approve() -> Weight;631 fn transfer_from() -> Weight;632 fn burn_from() -> Weight;633 fn set_variable_metadata(bytes: u32) -> Weight;634}635636pub trait CommonCollectionOperations<T: Config> {637 fn create_item(638 &self,639 sender: T::CrossAccountId,640 to: T::CrossAccountId,641 data: CreateItemData,642 ) -> DispatchResultWithPostInfo;643 fn create_multiple_items(644 &self,645 sender: T::CrossAccountId,646 to: T::CrossAccountId,647 data: Vec<CreateItemData>,648 ) -> DispatchResultWithPostInfo;649 fn burn_item(650 &self,651 sender: T::CrossAccountId,652 token: TokenId,653 amount: u128,654 ) -> DispatchResultWithPostInfo;655656 fn transfer(657 &self,658 sender: T::CrossAccountId,659 to: T::CrossAccountId,660 token: TokenId,661 amount: u128,662 ) -> DispatchResultWithPostInfo;663 fn approve(664 &self,665 sender: T::CrossAccountId,666 spender: T::CrossAccountId,667 token: TokenId,668 amount: u128,669 ) -> DispatchResultWithPostInfo;670 fn transfer_from(671 &self,672 sender: T::CrossAccountId,673 from: T::CrossAccountId,674 to: T::CrossAccountId,675 token: TokenId,676 amount: u128,677 ) -> DispatchResultWithPostInfo;678 fn burn_from(679 &self,680 sender: T::CrossAccountId,681 from: T::CrossAccountId,682 token: TokenId,683 amount: u128,684 ) -> DispatchResultWithPostInfo;685686 fn set_variable_metadata(687 &self,688 sender: T::CrossAccountId,689 token: TokenId,690 data: BoundedVec<u8, CustomDataLimit>,691 ) -> DispatchResultWithPostInfo;692693 fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;694 fn token_exists(&self, token: TokenId) -> bool;695 fn last_token_id(&self) -> TokenId;696697 fn token_owner(&self, token: TokenId) -> Option<T::CrossAccountId>;698 fn const_metadata(&self, token: TokenId) -> Vec<u8>;699 fn variable_metadata(&self, token: TokenId) -> Vec<u8>;700701 702 fn collection_tokens(&self) -> u32;703 704 fn account_balance(&self, account: T::CrossAccountId) -> u32;705 706 fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;707 fn allowance(708 &self,709 sender: T::CrossAccountId,710 spender: T::CrossAccountId,711 token: TokenId,712 ) -> u128;713}714715716pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {717 let post_info = PostDispatchInfo {718 actual_weight: Some(weight),719 pays_fee: Pays::Yes,720 };721 match res {722 Ok(()) => Ok(post_info),723 Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),724 }725}