--- a/pallets/common/src/erc.rs +++ b/pallets/common/src/erc.rs @@ -87,18 +87,16 @@ check_is_owner(caller, self)?; let sponsor = T::CrossAccountId::from_eth(sponsor); - self.set_sponsor(sponsor.as_sub().clone()); - save(self); - Ok(()) + self.set_sponsor(sponsor.as_sub().clone()).map_err(dispatch_to_evm::)?; + save(self) } fn confirm_collection_sponsorship(&mut self, caller: caller) -> Result { let caller = T::CrossAccountId::from_eth(caller); - if !self.confirm_sponsorship(caller.as_sub()) { + if !self.confirm_sponsorship(caller.as_sub()).map_err(dispatch_to_evm::)? { return Err(Error::Revert("Caller is not set as sponsor".into())); } - save(self); - Ok(()) + save(self) } #[solidity(rename_selector = "setCollectionLimit")] @@ -134,8 +132,7 @@ } self.limits = >::clamp_limits(self.mode.clone(), &self.limits, limits) .map_err(dispatch_to_evm::)?; - save(self); - Ok(()) + save(self) } #[solidity(rename_selector = "setCollectionLimit")] @@ -162,8 +159,7 @@ } self.limits = >::clamp_limits(self.mode.clone(), &self.limits, limits) .map_err(dispatch_to_evm::)?; - save(self); - Ok(()) + save(self) } fn contract_address(&self, _caller: caller) -> Result
{ @@ -296,7 +292,7 @@ } } -fn check_is_owner(caller: caller, collection: &CollectionHandle) -> Result<()> { +fn check_is_owner(caller: caller, collection: &CollectionHandle) -> Result { let caller = T::CrossAccountId::from_eth(caller); collection .check_is_owner(&caller) @@ -315,8 +311,10 @@ Ok(caller) } -fn save(collection: &CollectionHandle) { +fn save(collection: &CollectionHandle) -> Result { + collection.check_is_read_only().map_err(dispatch_to_evm::)?; >::insert(collection.id, collection.collection.clone()); + Ok(()) } pub fn token_uri_key() -> up_data_structs::PropertyKey { --- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -148,23 +148,35 @@ .saturating_mul(writes), )) } - - pub fn save(self) -> DispatchResult { + pub fn save(self) -> Result<(), DispatchError> { + self.check_is_read_only()?; >::insert(self.id, self.collection); Ok(()) } - pub fn set_sponsor(&mut self, sponsor: T::AccountId) { + pub fn set_sponsor(&mut self, sponsor: T::AccountId) -> DispatchResult { + self.check_is_read_only()?; self.collection.sponsorship = SponsorshipState::Unconfirmed(sponsor); + Ok(()) } - pub fn confirm_sponsorship(&mut self, sender: &T::AccountId) -> bool { + pub fn confirm_sponsorship(&mut self, sender: &T::AccountId) -> Result { + self.check_is_read_only()?; + if self.collection.sponsorship.pending_sponsor() != Some(sender) { - return false; - }; + return Ok(false); + } self.collection.sponsorship = SponsorshipState::Confirmed(sender.clone()); - true + Ok(true) + } + + pub fn check_is_read_only(&self) -> DispatchResult { + if self.read_only { + return Err(>::CollectionNotFound)?; + } + + Ok(()) } } @@ -434,6 +446,9 @@ /// Empty property keys are forbidden EmptyPropertyKey, + + /// Collection is read only + CollectionIsReadOnly, } #[pallet::storage] @@ -669,6 +684,7 @@ sponsorship, limits, permissions, + read_only, } = >::get(collection)?; let token_property_permissions = >::get(collection) @@ -698,6 +714,7 @@ permissions, token_property_permissions, properties, + read_only, }) } } @@ -778,6 +795,7 @@ Self::clamp_permissions(data.mode.clone(), &Default::default(), permissions) }) .unwrap_or_else(|| Ok(CollectionPermissions::default()))?, + read_only: false, }; let mut collection_properties = up_data_structs::CollectionProperties::get(); @@ -834,6 +852,7 @@ collection: CollectionHandle, sender: &T::CrossAccountId, ) -> DispatchResult { + collection.check_is_read_only()?; ensure!( collection.limits.owner_can_destroy(), >::NoPermission, @@ -863,6 +882,7 @@ sender: &T::CrossAccountId, property: Property, ) -> DispatchResult { + collection.check_is_read_only()?; collection.check_is_owner_or_admin(sender)?; CollectionProperties::::try_mutate(collection.id, |properties| { @@ -908,6 +928,8 @@ sender: &T::CrossAccountId, properties: Vec, ) -> DispatchResult { + collection.check_is_read_only()?; + for property in properties { Self::set_collection_property(collection, sender, property)?; } @@ -920,6 +942,7 @@ sender: &T::CrossAccountId, property_key: PropertyKey, ) -> DispatchResult { + collection.check_is_read_only()?; collection.check_is_owner_or_admin(sender)?; CollectionProperties::::try_mutate(collection.id, |properties| { @@ -941,6 +964,8 @@ sender: &T::CrossAccountId, property_keys: Vec, ) -> DispatchResult { + collection.check_is_read_only()?; + for key in property_keys { Self::delete_collection_property(collection, sender, key)?; } @@ -965,6 +990,7 @@ sender: &T::CrossAccountId, property_permission: PropertyKeyPermission, ) -> DispatchResult { + collection.check_is_read_only()?; collection.check_is_owner_or_admin(sender)?; let all_permissions = CollectionPropertyPermissions::::get(collection.id); @@ -996,6 +1022,8 @@ sender: &T::CrossAccountId, property_permissions: Vec, ) -> DispatchResult { + collection.check_is_read_only()?; + for prop_pemission in property_permissions { Self::set_property_permission(collection, sender, prop_pemission)?; } @@ -1083,6 +1111,7 @@ user: &T::CrossAccountId, allowed: bool, ) -> DispatchResult { + collection.check_is_read_only()?; collection.check_is_owner_or_admin(sender)?; // ========= @@ -1102,6 +1131,7 @@ user: &T::CrossAccountId, admin: bool, ) -> DispatchResult { + collection.check_is_read_only()?; collection.check_is_owner_or_admin(sender)?; let was_admin = >::get((collection.id, user)); --- a/pallets/fungible/src/lib.rs +++ b/pallets/fungible/src/lib.rs @@ -168,6 +168,8 @@ owner: &T::CrossAccountId, amount: u128, ) -> DispatchResult { + collection.check_is_read_only()?; + let total_supply = >::get(collection.id) .checked_sub(amount) .ok_or(>::TokenValueTooLow)?; @@ -214,6 +216,8 @@ amount: u128, nesting_budget: &dyn Budget, ) -> DispatchResult { + collection.check_is_read_only()?; + ensure!( collection.limits.transfers_enabled(), >::TransferNotAllowed, @@ -283,6 +287,8 @@ data: BTreeMap, nesting_budget: &dyn Budget, ) -> DispatchResult { + collection.check_is_read_only()?; + if !collection.is_owner_or_admin(sender) { ensure!( collection.permissions.mint_mode(), @@ -384,6 +390,7 @@ spender: &T::CrossAccountId, amount: u128, ) -> DispatchResult { + collection.check_is_read_only()?; if collection.permissions.access() == AccessMode::AllowList { collection.check_allowlist(owner)?; collection.check_allowlist(spender)?; --- a/pallets/nonfungible/src/lib.rs +++ b/pallets/nonfungible/src/lib.rs @@ -336,6 +336,8 @@ sender: &T::CrossAccountId, token: TokenId, ) -> DispatchResult { + collection.check_is_read_only()?; + let token_data = >::get((collection.id, token)).ok_or(>::TokenNotFound)?; ensure!( @@ -456,6 +458,7 @@ &property.key, is_token_create, )?; + collection.check_is_read_only()?; >::try_mutate((collection.id, token_id), |properties| { let property = property.clone(); @@ -494,6 +497,7 @@ property_key: PropertyKey, ) -> DispatchResult { Self::check_token_change_permission(collection, sender, token_id, &property_key, false)?; + collection.check_is_read_only()?; >::try_mutate((collection.id, token_id), |properties| { properties.remove(&property_key) @@ -570,6 +574,8 @@ token_id: TokenId, property_keys: Vec, ) -> DispatchResult { + collection.check_is_read_only()?; + for key in property_keys { Self::delete_token_property(collection, sender, token_id, key)?; } @@ -616,6 +622,8 @@ token: TokenId, nesting_budget: &dyn Budget, ) -> DispatchResult { + collection.check_is_read_only()?; + ensure!( collection.limits.transfers_enabled(), >::TransferNotAllowed @@ -894,6 +902,8 @@ token: TokenId, spender: Option<&T::CrossAccountId>, ) -> DispatchResult { + collection.check_is_read_only()?; + if collection.permissions.access() == AccessMode::AllowList { collection.check_allowlist(sender)?; if let Some(spender) = spender { --- a/pallets/refungible/src/lib.rs +++ b/pallets/refungible/src/lib.rs @@ -234,6 +234,7 @@ } pub fn burn_token(collection: &RefungibleHandle, token_id: TokenId) -> DispatchResult { + collection.check_is_read_only()?; let burnt = >::get(collection.id) .checked_add(1) .ok_or(ArithmeticError::Overflow)?; @@ -253,6 +254,7 @@ token: TokenId, amount: u128, ) -> DispatchResult { + collection.check_is_read_only()?; let total_supply = >::get((collection.id, token)) .checked_sub(amount) .ok_or(>::TokenValueTooLow)?; @@ -325,6 +327,7 @@ amount: u128, nesting_budget: &dyn Budget, ) -> DispatchResult { + collection.check_is_read_only()?; ensure!( collection.limits.transfers_enabled(), >::TransferNotAllowed @@ -573,6 +576,7 @@ token: TokenId, amount: u128, ) -> DispatchResult { + collection.check_is_read_only()?; if collection.permissions.access() == AccessMode::AllowList { collection.check_allowlist(sender)?; collection.check_allowlist(spender)?; --- a/pallets/unique/src/lib.rs +++ b/pallets/unique/src/lib.rs @@ -304,6 +304,7 @@ pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult { let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); let collection = >::try_get(collection_id)?; + collection.check_is_read_only()?; // ========= @@ -406,6 +407,7 @@ let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); let mut target_collection = >::try_get(collection_id)?; + target_collection.check_is_read_only()?; target_collection.check_is_owner(&sender)?; target_collection.owner = new_owner.clone(); @@ -487,7 +489,7 @@ let mut target_collection = >::try_get(collection_id)?; target_collection.check_is_owner(&sender)?; - target_collection.set_sponsor(new_sponsor.clone()); + target_collection.set_sponsor(new_sponsor.clone())?; >::deposit_event(Event::::CollectionSponsorSet( collection_id, @@ -511,7 +513,7 @@ let mut target_collection = >::try_get(collection_id)?; ensure!( - target_collection.confirm_sponsorship(&sender), + target_collection.confirm_sponsorship(&sender)?, Error::::ConfirmUnsetSponsorFail ); --- a/primitives/data-structs/src/lib.rs +++ b/primitives/data-structs/src/lib.rs @@ -316,6 +316,9 @@ #[version(2.., upper(Default::default()))] pub permissions: CollectionPermissions, + #[version(2.., upper(false))] + pub read_only: bool, + #[version(..2)] pub variable_on_chain_schema: BoundedVec>, @@ -340,6 +343,7 @@ pub permissions: CollectionPermissions, pub token_property_permissions: Vec, pub properties: Vec, + pub read_only: bool, } #[derive(Encode, Decode, Clone, PartialEq, TypeInfo, Derivative, MaxEncodedLen)] --- a/tests/src/createCollection.test.ts +++ b/tests/src/createCollection.test.ts @@ -87,6 +87,20 @@ expect(collection.limits.accountTokenOwnershipLimit.unwrap().toNumber()).to.equal(3); }); }); + + it('Create new collection is not read only', async () => { + await usingApi(async api => { + const alice = privateKey('//Alice'); + const tx = api.tx.unique.createCollectionEx({ + readOnly: true + }); + const events = await submitTransactionAsync(alice, tx); + const result = getCreateCollectionResult(events); + + const collection = (await getDetailedCollectionInfo(api, result.collectionId))!; + expect(collection.readOnly.toHuman()).to.be.false; + }); + }); }); describe('(!negative test!) integration test: ext. createCollection():', () => {