git.delta.rocks / unique-network / refs/commits / 713b8d223b2f

difftreelog

Merge pull request #831 from UniqueNetwork/fix/rft-collection-admin-permissions

Yaroslav Bolyukin2023-01-16parents: #5c9ffb7 #c7a326d.patch.diff
in: master

5 files changed

modifiedpallets/common/src/lib.rsdiffbeforeafterboth
390 Ok(())390 Ok(())
391 }391 }
392392
393 /// Return **true** if `user` was not allowed to have tokens, and he can ignore such restrictions.393 /// Returns **true** if
394 /// * the `user`is a collection owner or admin
395 /// * the collection limits allow the owner/admins to transfer/burn any collection token
394 pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> bool {396 pub fn ignores_token_restrictions(&self, user: &T::CrossAccountId) -> bool {
395 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)397 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)
396 }398 }
397399
modifiedpallets/fungible/src/lib.rsdiffbeforeafterboth
678 collection.check_allowlist(spender)?;678 collection.check_allowlist(spender)?;
679 }679 }
680
681 if collection.ignores_token_restrictions(spender) {
682 return Ok(Self::compute_allowance_decrease(
683 collection, from, spender, amount,
684 ));
685 }
686
680 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {687 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {
681 // TODO: should collection owner be allowed to perform this transfer?
682 ensure!(688 ensure!(
683 <PalletStructure<T>>::check_indirectly_owned(689 <PalletStructure<T>>::check_indirectly_owned(
684 spender.clone(),690 spender.clone(),
692 return Ok(None);698 return Ok(None);
693 }699 }
700
694 let allowance = <Allowance<T>>::get((collection.id, from, spender)).checked_sub(amount);701 let allowance = Self::compute_allowance_decrease(collection, from, spender, amount);
695 if allowance.is_none() {
696 ensure!(702 ensure!(allowance.is_some(), <CommonError<T>>::ApprovedValueTooLow);
697 collection.ignores_allowance(spender),
698 <CommonError<T>>::ApprovedValueTooLow
699 );
700 }
701703
702 Ok(allowance)704 Ok(allowance)
703 }705 }
706
707 /// Returns `Some(amount)` if the `spender` have allowance to spend this amount.
708 /// Otherwise, it returns `None`.
709 fn compute_allowance_decrease(
710 collection: &FungibleHandle<T>,
711 from: &T::CrossAccountId,
712 spender: &T::CrossAccountId,
713 amount: u128,
714 ) -> Option<u128> {
715 <Allowance<T>>::get((collection.id, from, spender)).checked_sub(amount)
716 }
704717
705 /// Transfer fungible tokens from one account to another.718 /// Transfer fungible tokens from one account to another.
706 /// Same as the [`transfer`][`Pallet::transfer`] but spender doesn't needs to be an owner of the token pieces.719 /// Same as the [`transfer`][`Pallet::transfer`] but spender doesn't needs to be an owner of the token pieces.
modifiedpallets/nonfungible/src/lib.rsdiffbeforeafterboth
1246 collection.check_allowlist(spender)?;1246 collection.check_allowlist(spender)?;
1247 }1247 }
12481248
1249 if collection.limits.owner_can_transfer() && collection.is_owner_or_admin(spender) {1249 if collection.ignores_token_restrictions(spender) {
1250 return Ok(());1250 return Ok(());
1251 }1251 }
12521252
1269 if <CollectionAllowance<T>>::get((collection.id, from, spender)) {1269 if <CollectionAllowance<T>>::get((collection.id, from, spender)) {
1270 return Ok(());1270 return Ok(());
1271 }1271 }
1272 ensure!(1272
1273 collection.ignores_allowance(spender),
1274 <CommonError<T>>::ApprovedValueTooLow1273 Err(<CommonError<T>>::ApprovedValueTooLow.into())
1275 );
1276 Ok(())
1277 }1274 }
12781275
1279 /// Transfer NFT token from one account to another.1276 /// Transfer NFT token from one account to another.
modifiedpallets/refungible/src/lib.rsdiffbeforeafterboth
1178 collection.check_allowlist(spender)?;1178 collection.check_allowlist(spender)?;
1179 }1179 }
1180
1181 if collection.ignores_token_restrictions(spender) {
1182 return Ok(Self::compute_allowance_decrease(
1183 collection, token, from, &spender, amount,
1184 ));
1185 }
1186
1180 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {1187 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {
1181 // TODO: should collection owner be allowed to perform this transfer?1188 // TODO: should collection owner be allowed to perform this transfer?
1192 return Ok(None);1199 return Ok(None);
1193 }1200 }
1201
1194 let allowance =1202 let allowance = Self::compute_allowance_decrease(collection, token, from, &spender, amount);
1195 <Allowance<T>>::get((collection.id, token, from, &spender)).checked_sub(amount);1203 if allowance.is_some() {
1204 return Ok(allowance);
1205 }
11961206
1197 // Allowance (if any) would be reduced if spender is also wallet operator1207 // Allowance (if any) would be reduced if spender is also wallet operator
1198 if <CollectionAllowance<T>>::get((collection.id, from, spender)) {1208 if <CollectionAllowance<T>>::get((collection.id, from, spender)) {
1199 return Ok(allowance);1209 return Ok(allowance);
1200 }1210 }
12011211
1202 if allowance.is_none() {
1203 ensure!(
1204 collection.ignores_allowance(spender),
1205 <CommonError<T>>::ApprovedValueTooLow1212 Err(<CommonError<T>>::ApprovedValueTooLow.into())
1206 );
1207 }
1208 Ok(allowance)
1209 }1213 }
1214
1215 /// Returns `Some(amount)` if the `spender` have allowance to spend this amount.
1216 /// Otherwise, it returns `None`.
1217 fn compute_allowance_decrease(
1218 collection: &RefungibleHandle<T>,
1219 token: TokenId,
1220 from: &T::CrossAccountId,
1221 spender: &T::CrossAccountId,
1222 amount: u128,
1223 ) -> Option<u128> {
1224 <Allowance<T>>::get((collection.id, token, from, spender)).checked_sub(amount)
1225 }
12101226
1211 /// Transfer RFT token pieces from one account to another.1227 /// Transfer RFT token pieces from one account to another.
1212 ///1228 ///
modifiedtests/src/nesting/unnest.test.tsdiffbeforeafterboth
1616
17import {IKeyringPair} from '@polkadot/types/types';17import {IKeyringPair} from '@polkadot/types/types';
18import {expect, itSub, Pallets, usingPlaygrounds} from '../util';18import {expect, itSub, Pallets, usingPlaygrounds} from '../util';
19import {UniqueFTCollection, UniqueNFToken, UniqueRFToken} from '../util/playgrounds/unique';
1920
20describe('Integration Test: Unnesting', () => {21describe('Integration Test: Unnesting', () => {
21 let alice: IKeyringPair;22 let alice: IKeyringPair;
23 let bob: IKeyringPair;
24 let charlie: IKeyringPair;
2225
23 before(async () => {26 before(async () => {
24 await usingPlaygrounds(async (helper, privateKey) => {27 await usingPlaygrounds(async (helper, privateKey) => {
25 const donor = await privateKey({filename: __filename});28 const donor = await privateKey({filename: __filename});
26 [alice] = await helper.arrange.createAccounts([50n], donor);29 [alice, bob, charlie] = await helper.arrange.createAccounts([200n, 50n, 50n], donor);
27 });30 });
28 });31 });
2932
84 expect(await targetToken.getChildren()).to.be.length(0);87 expect(await targetToken.getChildren()).to.be.length(0);
85 });88 });
89
90 async function checkNestedAmountState({
91 expectedBalance,
92 childrenShouldPresent,
93 nested,
94 targetNft,
95 }: {
96 expectedBalance: bigint,
97 childrenShouldPresent: boolean,
98 nested: UniqueFTCollection | UniqueRFToken,
99 targetNft: UniqueNFToken,
100 }) {
101 const balance = await nested.getBalance(targetNft.nestingAccount());
102 expect(balance).to.be.equal(expectedBalance);
103
104 const children = await targetNft.getChildren();
105
106 if (childrenShouldPresent) {
107 expect(children[0]).to.be.deep.equal({
108 collectionId: nested.collectionId,
109 tokenId: (nested instanceof UniqueFTCollection) ? 0 : nested.tokenId,
110 });
111 } else {
112 expect(children.length).to.be.equal(0);
113 }
114 }
115
116 function ownerOrAdminUnnestCases(modes: ('ft' | 'nft' | 'rft')[]): {
117 mode: 'ft' | 'nft' | 'rft',
118 sender: string,
119 op: 'transfer' | 'burn',
120 requiredPallets: Pallets[],
121 }[] {
122 const senders = ['owner', 'admin'];
123 const ops = ['transfer', 'burn'];
124
125 const cases = [];
126 for (const mode of modes) {
127 const requiredPallets = (mode === 'rft')
128 ? [Pallets.ReFungible]
129 : [];
130
131 for (const sender of senders) {
132 for (const op of ops) {
133 cases.push({
134 mode: mode as 'ft' | 'nft' | 'rft',
135 sender,
136 op: op as 'transfer' | 'burn',
137 requiredPallets,
138 });
139 }
140 }
141 }
142
143 return cases;
144 }
145
146 ownerOrAdminUnnestCases(['ft', 'rft']).map(testCase =>
147 itSub.ifWithPallets(`[${testCase.mode}]: allows a collection ${testCase.sender} to ${testCase.op} nested token`, testCase.requiredPallets, async({helper}) => {
148 const owner = alice;
149 const admin = bob;
150
151 const unnester = (testCase.sender === 'owner')
152 ? owner
153 : admin;
154
155 const collectionNFT = await helper.nft.mintCollection(owner);
156 await collectionNFT.setPermissions(owner, {nesting: {tokenOwner: true}});
157
158 const collectionNested = await helper[testCase.mode as 'ft' | 'rft'].mintCollection(owner, {
159 limits: {
160 ownerCanTransfer: true,
161 },
162 });
163 await collectionNested.addAdmin(owner, {Substrate: admin.address});
164
165 const targetNft = await collectionNFT.mintToken(owner, {Substrate: charlie.address});
166
167 let nested: UniqueFTCollection | UniqueRFToken;
168 const totalAmount = 5n;
169 const firstUnnestAmount = 2n;
170 const restUnnestAmount = totalAmount - firstUnnestAmount;
171
172 if (collectionNested instanceof UniqueFTCollection) {
173 await collectionNested.mint(owner, totalAmount, {Substrate: charlie.address});
174 nested = collectionNested;
175 } else {
176 nested = await collectionNested.mintToken(owner, totalAmount, {Substrate: charlie.address});
177 }
178
179 // transfer/burn `amount` of nested assets by `unnester`.
180 const doOperationAndCheck = async ({
181 amount,
182 shouldBeNestedAfterOp,
183 }: {
184 amount: bigint,
185 shouldBeNestedAfterOp: boolean,
186 }) => {
187 const nestedBalanceBeforeOp = await nested.getBalance(targetNft.nestingAccount());
188
189 if (testCase.op === 'transfer') {
190 const bobBalanceBeforeOp = await nested.getBalance({Substrate: bob.address});
191
192 await nested.transferFrom(unnester, targetNft.nestingAccount(), {Substrate: bob.address}, amount);
193 expect(await nested.getBalance({Substrate: bob.address})).to.be.equal(bobBalanceBeforeOp + amount);
194 } else {
195 if (nested instanceof UniqueFTCollection) {
196 await nested.burnTokensFrom(unnester, targetNft.nestingAccount(), amount);
197 } else {
198 await nested.burnFrom(unnester, targetNft.nestingAccount(), amount);
199 }
200 }
201
202 await checkNestedAmountState({
203 expectedBalance: nestedBalanceBeforeOp - amount,
204 childrenShouldPresent: shouldBeNestedAfterOp,
205 nested,
206 targetNft,
207 });
208 };
209
210 // Initial setup: nest (fungibles/rft parts).
211 // Check NFT's balance of nested assets and NFT's children.
212 await nested.transfer(charlie, targetNft.nestingAccount(), totalAmount);
213 await checkNestedAmountState({
214 expectedBalance: totalAmount,
215 childrenShouldPresent: true,
216 nested,
217 targetNft,
218 });
219
220 // Transfer/burn only a part of nested assets.
221 // Check that NFT's balance of the nested assets correctly decreased and NFT's children are not changed.
222 await doOperationAndCheck({
223 amount: firstUnnestAmount,
224 shouldBeNestedAfterOp: true,
225 });
226
227 // Transfer/burn all remaining nested assets.
228 // Check that NFT's balance of the nested assets is 0 and NFT has no more children.
229 await doOperationAndCheck({
230 amount: restUnnestAmount,
231 shouldBeNestedAfterOp: false,
232 });
233 }));
234
235 ownerOrAdminUnnestCases(['nft']).map(testCase =>
236 itSub(`[nft]: allows a collection ${testCase.sender} to ${testCase.op} nested token`, async ({helper}) => {
237 const owner = alice;
238 const admin = bob;
239
240 const unnester = (testCase.sender === 'owner')
241 ? owner
242 : admin;
243
244 const collectionNFT = await helper.nft.mintCollection(owner);
245 await collectionNFT.setPermissions(owner, {nesting: {tokenOwner: true}});
246
247 const collectionNested = await helper.nft.mintCollection(owner, {
248 limits: {
249 ownerCanTransfer: true,
250 },
251 });
252 await collectionNested.addAdmin(owner, {Substrate: admin.address});
253
254 const targetNft = await collectionNFT.mintToken(owner, {Substrate: charlie.address});
255 const nested = await collectionNested.mintToken(owner, {Substrate: charlie.address});
256
257 await nested.transfer(charlie, targetNft.nestingAccount());
258 expect(await targetNft.getChildren()).to.be.deep.equal([{
259 collectionId: nested.collectionId,
260 tokenId: nested.tokenId,
261 }]);
262
263 if (testCase.op === 'transfer') {
264 await nested.transferFrom(unnester, targetNft.nestingAccount(), {Substrate: bob.address});
265 } else {
266 await nested.burnFrom(unnester, targetNft.nestingAccount());
267 }
268
269 expect((await targetNft.getChildren()).length).to.be.equal(0);
270 }));
86});271});
87272
88describe('Negative Test: Unnesting', () => {273describe('Negative Test: Unnesting', () => {