Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Nfts: Multiple approvals #12178

Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
bf7d59b
multiple approvals
Szegoo Sep 3, 2022
cf08222
clear
Szegoo Sep 3, 2022
b296ba3
tests & clean up
Szegoo Sep 3, 2022
a8c71d4
fix in logic & fmt
Szegoo Sep 3, 2022
3cfe0cc
fix benchmarks
Szegoo Sep 3, 2022
7a8391a
deadline
Szegoo Sep 3, 2022
21d5b81
test deadline
Szegoo Sep 3, 2022
62e6371
current_block + deadline
Szegoo Sep 4, 2022
8067c3b
update ApprovedTransfer event
Szegoo Sep 4, 2022
d4d1fe2
benchmark
Szegoo Sep 5, 2022
2d06e76
docs
Szegoo Sep 5, 2022
111adbb
Update frame/nfts/src/lib.rs
Szegoo Sep 6, 2022
3e7101d
fmt fix
Szegoo Sep 6, 2022
2247ee5
Update frame/nfts/src/lib.rs
Szegoo Sep 7, 2022
0a30732
update tests
Szegoo Sep 7, 2022
7bf0260
anyone can cancel
Szegoo Sep 8, 2022
b7a50f3
Update frame/nfts/src/tests.rs
Szegoo Sep 8, 2022
9c57d52
fmt
Szegoo Sep 8, 2022
a8371b3
fix logic
Szegoo Sep 8, 2022
b552b2e
unnecessary line
Szegoo Sep 8, 2022
00a271c
".git/.scripts/bench-bot.sh" pallet dev pallet_nfts
Sep 8, 2022
2a4775c
Update frame/nfts/src/lib.rs
jsidorenko Sep 8, 2022
bf7d701
Merge remote-tracking branch 'js/uniques-v2-main-branch/js/uniques-v2…
Szegoo Sep 8, 2022
5c8d01a
Update lib.rs
Szegoo Sep 8, 2022
0629062
fmt
Szegoo Sep 8, 2022
e8eec8b
Update frame/nfts/src/lib.rs
Szegoo Sep 9, 2022
5ec3892
Update frame/nfts/src/lib.rs
Szegoo Sep 9, 2022
f496fa7
fmt
Szegoo Sep 9, 2022
5697cbb
Update frame/nfts/src/lib.rs
Szegoo Sep 9, 2022
5be3e14
suggestion
Szegoo Sep 9, 2022
f8319df
new line
Szegoo Sep 9, 2022
74c439d
Merge branch 'js/uniques-v2-main-branch' into uniques-multiple-approvals
jsidorenko Sep 9, 2022
550f934
".git/.scripts/bench-bot.sh" pallet dev pallet_nfts
Sep 9, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1453,6 +1453,7 @@ parameter_types! {
pub const ItemDeposit: Balance = 1 * DOLLARS;
pub const KeyLimit: u32 = 32;
pub const ValueLimit: u32 = 256;
pub const ApprovalsLimit: u32 = 20;
}

impl pallet_uniques::Config for Runtime {
Expand Down Expand Up @@ -1490,6 +1491,7 @@ impl pallet_nfts::Config for Runtime {
type StringLimit = StringLimit;
type KeyLimit = KeyLimit;
type ValueLimit = ValueLimit;
type ApprovalsLimit = ApprovalsLimit;
type WeightInfo = pallet_nfts::weights::SubstrateWeight<Runtime>;
#[cfg(feature = "runtime-benchmarks")]
type Helper = ();
Expand Down
23 changes: 19 additions & 4 deletions frame/nfts/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,9 +368,10 @@ benchmarks_instance_pallet! {
let (item, ..) = mint_item::<T, I>(0);
let delegate: T::AccountId = account("delegate", 0, SEED);
let delegate_lookup = T::Lookup::unlookup(delegate.clone());
}: _(SystemOrigin::Signed(caller.clone()), collection, item, delegate_lookup)
let deadline = T::BlockNumber::max_value();
}: _(SystemOrigin::Signed(caller.clone()), collection, item, delegate_lookup, Some(deadline))
verify {
assert_last_event::<T, I>(Event::ApprovedTransfer { collection, item, owner: caller, delegate }.into());
assert_last_event::<T, I>(Event::ApprovedTransfer { collection, item, owner: caller, delegate, deadline: Some(deadline) }.into());
}

cancel_approval {
Expand All @@ -379,12 +380,26 @@ benchmarks_instance_pallet! {
let delegate: T::AccountId = account("delegate", 0, SEED);
let delegate_lookup = T::Lookup::unlookup(delegate.clone());
let origin = SystemOrigin::Signed(caller.clone()).into();
Nfts::<T, I>::approve_transfer(origin, collection, item, delegate_lookup.clone())?;
}: _(SystemOrigin::Signed(caller.clone()), collection, item, Some(delegate_lookup))
let deadline = T::BlockNumber::max_value();
Nfts::<T, I>::approve_transfer(origin, collection, item, delegate_lookup.clone(), Some(deadline))?;
}: _(SystemOrigin::Signed(caller.clone()), collection, item, delegate_lookup)
verify {
assert_last_event::<T, I>(Event::ApprovalCancelled { collection, item, owner: caller, delegate }.into());
}

clear_all_transfer_approvals {
let (collection, caller, _) = create_collection::<T, I>();
let (item, ..) = mint_item::<T, I>(0);
let delegate: T::AccountId = account("delegate", 0, SEED);
let delegate_lookup = T::Lookup::unlookup(delegate.clone());
let origin = SystemOrigin::Signed(caller.clone()).into();
let deadline = T::BlockNumber::max_value();
Nfts::<T, I>::approve_transfer(origin, collection, item, delegate_lookup.clone(), Some(deadline))?;
}: _(SystemOrigin::Signed(caller.clone()), collection, item)
verify {
assert_last_event::<T, I>(Event::AllApprovalsCancelled {collection, item, owner: caller}.into());
}

set_accept_ownership {
let caller: T::AccountId = whitelisted_caller();
T::Currency::make_free_balance_be(&caller, DepositBalanceOf::<T, I>::max_value());
Expand Down
13 changes: 12 additions & 1 deletion frame/nfts/src/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
Account::<T, I>::insert((&dest, &collection, &item), ());
let origin = details.owner;
details.owner = dest;

// The approved accounts have to be reset to None, because otherwise pre-approve attack
// would be possible, where the owner can approve his second account before making the
// transaction and then claiming the item back.
details.approvals.clear();

Item::<T, I>::insert(&collection, &item, &details);
ItemPriceOf::<T, I>::remove(&collection, &item);

Expand Down Expand Up @@ -168,7 +174,12 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {

let owner = owner.clone();
Account::<T, I>::insert((&owner, &collection, &item), ());
let details = ItemDetails { owner, approved: None, is_frozen: false, deposit };
let details = ItemDetails {
owner,
approvals: ApprovalsOf::<T, I>::default(),
is_frozen: false,
deposit,
};
Item::<T, I>::insert(&collection, &item, details);
Ok(())
},
Expand Down
115 changes: 96 additions & 19 deletions frame/nfts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ use frame_support::{
traits::{
tokens::Locker, BalanceStatus::Reserved, Currency, EnsureOriginWithArg, ReservableCurrency,
},
transactional,
transactional, BoundedBTreeMap,
};
use frame_system::Config as SystemConfig;
use sp_runtime::{
Expand All @@ -59,7 +59,7 @@ pub use pallet::*;
pub use types::*;
pub use weights::WeightInfo;

type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
type AccountIdLookupOf<T> = <<T as SystemConfig>::Lookup as StaticLookup>::Source;

#[frame_support::pallet]
pub mod pallet {
Expand Down Expand Up @@ -149,6 +149,10 @@ pub mod pallet {
#[pallet::constant]
type ValueLimit: Get<u32>;

/// The maximum approvals an item could have.
#[pallet::constant]
type ApprovalsLimit: Get<u32>;

#[cfg(feature = "runtime-benchmarks")]
/// A set of helper functions for benchmarking.
type Helper: BenchmarkHelper<Self::CollectionId, Self::ItemId>;
Expand All @@ -157,6 +161,12 @@ pub mod pallet {
type WeightInfo: WeightInfo;
}

pub type ApprovalsOf<T, I = ()> = BoundedBTreeMap<
<T as SystemConfig>::AccountId,
Option<<T as SystemConfig>::BlockNumber>,
<T as Config<I>>::ApprovalsLimit,
>;

#[pallet::storage]
#[pallet::storage_prefix = "Class"]
/// Details of a collection.
Expand Down Expand Up @@ -209,7 +219,7 @@ pub mod pallet {
T::CollectionId,
Blake2_128Concat,
T::ItemId,
ItemDetails<T::AccountId, DepositBalanceOf<T, I>>,
ItemDetails<T::AccountId, DepositBalanceOf<T, I>, ApprovalsOf<T, I>>,
OptionQuery,
>;

Expand Down Expand Up @@ -311,6 +321,7 @@ pub mod pallet {
item: T::ItemId,
owner: T::AccountId,
delegate: T::AccountId,
deadline: Option<<T as SystemConfig>::BlockNumber>,
},
/// An approval for a `delegate` account to transfer the `item` of an item
/// `collection` was cancelled by its `owner`.
Expand All @@ -320,6 +331,8 @@ pub mod pallet {
owner: T::AccountId,
delegate: T::AccountId,
},
/// All approvals of an item got cancelled.
AllApprovalsCancelled { collection: T::CollectionId, item: T::ItemId, owner: T::AccountId },
/// A `collection` has had its attributes changed by the `Force` origin.
ItemStatusChanged { collection: T::CollectionId },
/// New metadata has been set for a `collection`.
Expand Down Expand Up @@ -385,6 +398,8 @@ pub mod pallet {
UnknownCollection,
/// The item ID has already been used for an item.
AlreadyExists,
/// The approval had a deadline that expired, so the approval isn't valid anymore.
ApprovalExpired,
/// The owner turned out to be different to what was expected.
WrongOwner,
/// Invalid witness data given.
Expand All @@ -393,10 +408,10 @@ pub mod pallet {
InUse,
/// The item or collection is frozen.
Frozen,
/// The provided account is not a delegate.
NotDelegate,
/// The delegate turned out to be different to what was expected.
WrongDelegate,
/// There is no delegate approved.
NoDelegate,
/// No approval exists that would allow the transfer.
Unapproved,
/// The named owner has not signed ownership of the collection is acceptable.
Expand All @@ -415,6 +430,8 @@ pub mod pallet {
NotForSale,
/// The provided bid is too low.
BidTooLow,
/// The item has reached its approval limit.
ReachedApprovalLimit,
}

impl<T: Config<I>, I: 'static> Pallet<T, I> {
Expand Down Expand Up @@ -630,8 +647,15 @@ pub mod pallet {

Self::do_transfer(collection, item, dest, |collection_details, details| {
if details.owner != origin && collection_details.admin != origin {
let approved = details.approved.take().map_or(false, |i| i == origin);
let approved = details.approvals.contains_key(&origin);
ensure!(approved, Error::<T, I>::NoPermission);
Szegoo marked this conversation as resolved.
Show resolved Hide resolved

let deadline =
details.approvals.get(&origin).ok_or(Error::<T, I>::NoPermission)?;
if let Some(d) = deadline {
let block_number = frame_system::Pallet::<T>::block_number();
ensure!(*d >= block_number, Error::<T, I>::ApprovalExpired);
Szegoo marked this conversation as resolved.
Show resolved Hide resolved
}
}
Ok(())
})
Expand Down Expand Up @@ -912,6 +936,7 @@ pub mod pallet {
/// - `collection`: The collection of the item to be approved for delegated transfer.
/// - `item`: The item of the item to be approved for delegated transfer.
/// - `delegate`: The account to delegate permission to transfer the item.
/// - `maybe_deadline`: Optional deadline for the approval.
///
/// Emits `ApprovedTransfer` on success.
///
Expand All @@ -922,6 +947,7 @@ pub mod pallet {
collection: T::CollectionId,
item: T::ItemId,
delegate: AccountIdLookupOf<T>,
maybe_deadline: Option<<T as SystemConfig>::BlockNumber>,
) -> DispatchResult {
let maybe_check: Option<T::AccountId> = T::ForceOrigin::try_origin(origin)
.map(|_| None)
Expand All @@ -939,21 +965,29 @@ pub mod pallet {
ensure!(permitted, Error::<T, I>::NoPermission);
}

details.approved = Some(delegate);
let deadline = match maybe_deadline {
Some(d) => Some(d.saturating_add(frame_system::Pallet::<T>::block_number())),
None => None,
};
Szegoo marked this conversation as resolved.
Show resolved Hide resolved

details
.approvals
.try_insert(delegate.clone(), deadline.clone())
jsidorenko marked this conversation as resolved.
Show resolved Hide resolved
.map_err(|_| Error::<T, I>::ReachedApprovalLimit)?;
Item::<T, I>::insert(&collection, &item, &details);

let delegate = details.approved.expect("set as Some above; qed");
Self::deposit_event(Event::ApprovedTransfer {
collection,
item,
owner: details.owner,
delegate,
deadline,
});

Ok(())
}

/// Cancel the prior approval for the transfer of an item by a delegate.
/// Cancel one of the transfer approvals for a specific item.
///
/// Origin must be either:
/// - the `Force` origin;
Expand All @@ -962,9 +996,8 @@ pub mod pallet {
///
/// Arguments:
/// - `collection`: The collection of the item of whose approval will be cancelled.
/// - `item`: The item of the item of whose approval will be cancelled.
/// - `maybe_check_delegate`: If `Some` will ensure that the given account is the one to
/// which permission of transfer is delegated.
/// - `item`: The item of the collection of whose approval will be cancelled.
/// - `delegate`: The account that is going to loose their approval.
///
/// Emits `ApprovalCancelled` on success.
///
Expand All @@ -974,12 +1007,14 @@ pub mod pallet {
origin: OriginFor<T>,
collection: T::CollectionId,
item: T::ItemId,
maybe_check_delegate: Option<AccountIdLookupOf<T>>,
delegate: AccountIdLookupOf<T>,
) -> DispatchResult {
let maybe_check: Option<T::AccountId> = T::ForceOrigin::try_origin(origin)
.map(|_| None)
.or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?;

let delegate = T::Lookup::lookup(delegate)?;

let collection_details =
Collection::<T, I>::get(&collection).ok_or(Error::<T, I>::UnknownCollection)?;
let mut details =
Expand All @@ -988,18 +1023,60 @@ pub mod pallet {
let permitted = check == collection_details.admin || check == details.owner;
Szegoo marked this conversation as resolved.
Show resolved Hide resolved
ensure!(permitted, Error::<T, I>::NoPermission);
}
let maybe_check_delegate = maybe_check_delegate.map(T::Lookup::lookup).transpose()?;
let old = details.approved.take().ok_or(Error::<T, I>::NoDelegate)?;
if let Some(check_delegate) = maybe_check_delegate {
ensure!(check_delegate == old, Error::<T, I>::WrongDelegate);
}

ensure!(details.approvals.contains_key(&delegate), Error::<T, I>::NotDelegate);

details.approvals.remove(&delegate);
Item::<T, I>::insert(&collection, &item, &details);
Self::deposit_event(Event::ApprovalCancelled {
collection,
item,
owner: details.owner,
delegate: old,
delegate,
});

Ok(())
}

/// Cancel all the approvals of a specific item.
///
/// Origin must be either:
/// - the `Force` origin;
/// - `Signed` with the signer being the Admin of the `collection`;
/// - `Signed` with the signer being the Owner of the `item`;
///
/// Arguments:
/// - `collection`: The collection of the item of whose approvals will be cleared.
/// - `item`: The item of the collection of whose approvals will be cleared.
///
/// Emits `AllApprovalsCancelled` on success.
///
/// Weight: `O(1)`
#[pallet::weight(T::WeightInfo::clear_all_transfer_approvals())]
pub fn clear_all_transfer_approvals(
origin: OriginFor<T>,
collection: T::CollectionId,
item: T::ItemId,
) -> DispatchResult {
let maybe_check: Option<T::AccountId> = T::ForceOrigin::try_origin(origin)
.map(|_| None)
.or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?;

let collection_details =
Collection::<T, I>::get(&collection).ok_or(Error::<T, I>::UnknownCollection)?;
let mut details =
Item::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::UnknownCollection)?;
if let Some(check) = maybe_check {
let permitted = check == collection_details.admin || check == details.owner;
ensure!(permitted, Error::<T, I>::NoPermission);
}

details.approvals.clear();
Item::<T, I>::insert(&collection, &item, &details);
Self::deposit_event(Event::AllApprovalsCancelled {
collection,
item,
owner: details.owner,
});

Ok(())
Expand Down
1 change: 1 addition & 0 deletions frame/nfts/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ impl Config for Test {
type StringLimit = ConstU32<50>;
type KeyLimit = ConstU32<50>;
type ValueLimit = ConstU32<50>;
type ApprovalsLimit = ConstU32<10>;
type WeightInfo = ();
#[cfg(feature = "runtime-benchmarks")]
type Helper = ();
Expand Down
Loading