Conversation
| spender: T::AccountId, | ||
| amount: Balance, | ||
| ) -> DispatchResult { | ||
| let caller_data = IdentityPallet::<T>::ensure_origin_call_permissions(origin)?; |
There was a problem hiding this comment.
I don't think we should be doing call permissions here. When account keys hold the assets only that key has the authority over those assets.
There was a problem hiding this comment.
Based on our discussion we will keep using call permissions here. Which means we should change Asset.transfer_asset to also use call permissions.
pallets/asset/src/lib.rs
Outdated
| /// Maps (owner, spender, asset_id) to the approved allowance amount. | ||
| /// | ||
| /// A non-existent entry returns 0 (via `ValueQuery`), matching ERC-20 behavior. | ||
| /// When an allowance is revoked (set to 0), the entry is removed to bound storage growth. | ||
| #[pallet::storage] | ||
| pub type Allowances<T: Config> = | ||
| StorageMap<_, Blake2_128Concat, (T::AccountId, T::AccountId, AssetId), Balance, ValueQuery>; |
There was a problem hiding this comment.
This should be a double map, so that the owner can easily query all current allowances.
| let from = Some(AssetHolder::try_from(alice.account().encode()).unwrap()); | ||
| let to = AssetHolder::try_from(bob.account().encode()).unwrap(); |
There was a problem hiding this comment.
Using accounts instead of portfolios will be cheaper (no custodianship checks).
pallets/nft/src/lib.rs
Outdated
| let _caller_data = IdentityPallet::<T>::ensure_origin_call_permissions(origin.clone())?; | ||
|
|
||
| let fund = Fund { | ||
| description: FundDescription::NonFungible(nfts), | ||
| memo, | ||
| }; | ||
|
|
||
| let (_instruction_id, consumed) = | ||
| <T as pallet_asset::Config>::SettlementFn::transfer_funds( | ||
| origin, |
There was a problem hiding this comment.
The call permission check here is a duplicate, since it is checked in transfer_funds we don't need it here.
The pallet/extrinsic names are set before the extrinsic is called, the permissions checks are not based on the fn name.
| let _caller_data = IdentityPallet::<T>::ensure_origin_call_permissions(origin.clone())?; | |
| let fund = Fund { | |
| description: FundDescription::NonFungible(nfts), | |
| memo, | |
| }; | |
| let (_instruction_id, consumed) = | |
| <T as pallet_asset::Config>::SettlementFn::transfer_funds( | |
| origin, | |
| let fund = Fund { | |
| description: FundDescription::NonFungible(nfts), | |
| memo, | |
| }; | |
| let (_instruction_id, consumed) = | |
| <T as pallet_asset::Config>::SettlementFn::transfer_funds( | |
| origin, |
| let mut weight_meter = <T as Config>::SettlementFn::transfer_and_try_execute_weight_meter( | ||
| <T as Config>::WeightInfo::transfer_asset_base_weight(), | ||
| true, | ||
| ); | ||
|
|
||
| // Create the transfer instruction via the settlement engine and affirm it as the sender. | ||
| let instruction_id = T::SettlementFn::transfer_asset_and_try_execute( |
There was a problem hiding this comment.
Since this method emits an event, we should still create the weight meter here. Also since this code path only uses account holder (no portfolios), it's worse-case weight should be lower.
| // In spender mode (sender != caller), pass None to skip the caller's secondary key | ||
| // check and instead verify the sender's account via their DID's primary key. | ||
| let sender_sk = if from_did == origin_did { | ||
| origin_data.secondary_key.as_ref() | ||
| } else { | ||
| None | ||
| }; | ||
|
|
||
| // Affirm the instruction on behalf of the sender. | ||
| Self::unsafe_affirm_instruction( | ||
| origin_data.primary_did, | ||
| instruction_id, | ||
| [from].into(), | ||
| origin_data.secondary_key.as_ref(), | ||
| None, | ||
| )?; | ||
| Self::unsafe_affirm_instruction(from_did, instruction_id, [from].into(), sender_sk, None)?; |
There was a problem hiding this comment.
This is very confusing. The spend_allowance check is outside of this function and it is not clear how that check matches up with the logic here.
Please move the spender/portfolio check into a function that can be called here for the "different DIDs, use settlement" case.
pallets/settlement/src/lib.rs
Outdated
| @@ -3888,9 +4069,17 @@ impl<T: Config> SettlementFnTrait<T> for Pallet<T> { | |||
| weight_meter: &mut WeightMeter, | |||
| #[cfg(feature = "runtime-benchmarks")] bench_base_weight: bool, | |||
| ) -> Result<Option<InstructionId>, DispatchErrorWithPostInfo> { | |||
There was a problem hiding this comment.
I think this is dead code now.
pallets/settlement/src/lib.rs
Outdated
| Self::base_transfer_and_try_execute( | ||
| origin, | ||
| Some(resolved_from), | ||
| Some(from_did), | ||
| to, | ||
| Some(to_did), |
There was a problem hiding this comment.
I think this is the only call to that function, so don't need optional parameters.
pallets/settlement/src/lib.rs
Outdated
| AssetHolder::Portfolio(ref portfolio_id) => { | ||
| T::PortfolioFn::ensure_portfolio_custody(portfolio_id, caller_did)?; | ||
| } | ||
| } |
There was a problem hiding this comment.
Please move this spender/custody check into a function, so it can be called in base_transfer_and_try_execute or inside the "Same-identity: transfer directly" block below.
Also I think we only need to do the spender check if from.is_some().
| // TODO: Replace with generated weights after benchmark run. | ||
| // Based on base_transfer (21r/5w) + origin check (1r) + allowance (1r/1w) + DID resolution (2r). | ||
| fn transfer_funds() -> Weight { |
There was a problem hiding this comment.
Please run new benchmarks locally (use a placeholder weight function during the first build/run), since the benchmarks will record storage read/writes.
| let alice = UserBuilder::<T>::default().generate_did().build("Alice"); | ||
| }: _(alice.origin, AffirmationRequirement::Required) | ||
|
|
||
| transfer_funds { |
There was a problem hiding this comment.
I think we will need to split this benchmark/weight function into multiple functions.
Benchmarks for the main code paths (with receiver affirmation, no execution):
- sender or receiver is a portfolio, same DID
- sender or receiver is a portfolio, different DID
- sender and receiver accounts, same DID
- sender and receiver accounts, different DID
Also a benchmark to cover (
There was a problem hiding this comment.
My last comment got cut off.
Also a benchmark to cover the try execution logic.
Implements spender approval primitives for the asset pallet and adds a
transfer_fundsextrinsic to the settlement pallet for spending approved allowances and same-identity direct transfers.changelog
new features
transfer_fundsto the settlement pallet — supports same-identity direct transfers and spender-approved transfers without going through the settlement enginetransfer_assettransfer_nftto the NFT pallet — transfer NFTs between accounts/portfolios with same routing logic astransfer_fundsmodified external API
transfer_assetnow usesensure_origin_call_permissionsand routes same-DID transfers directly instead of always creating a settlement instructionnew external API
approveextrinsic to the asset pallet — set, replace, or revoke a spender allowance for a given assettransfer_fundsextrinsic to the settlement pallet — transfer assets with optional spender approval, defaults source to caller's account whenfromisNoneallowance(owner, spender, asset_id)Runtime API — query remaining spender allowance without a transactionnew events
Approval { owner, spender, asset_id, amount }event — emitted onapprovecalls, not on spendother
Allowancesstorage map to the asset pallet — maps(owner, spender, asset_id)to approved balanceInsufficientAllowanceerror to the asset palletSenderSameAsReceiverandAllowancesNotSupportedForNFTserrors to the settlement palletspend_allowancepublic helper to the asset pallet — used by the settlement pallet to check and decrement allowancesBalance::MAX) is never decremented on spend; allowance depleted to zero removes the storage entryapprove,allowanceRuntime API, andtransfer_fundscovering spender mode, same-identity transfers, cross-identity settlement, portfolio custody, NFT transfers, atomicity, and edge casesPortfolioFnTrait/PortfolioFnConfigtransfer_funds/transfer_funds_weighttoSettlementFnTrait— exposes routing logic for cross-pallet callerssimplified_nft_transfernow acceptsOption<InstructionId>(was required)transfer_asset_base_weightbenchmark,transfer_asset_and_try_execute,transfer_nft_and_try_execute