Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add approve and transfer_from #5

Open
wants to merge 2 commits into
base: release-polkadot-stable2407
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 33 additions & 0 deletions tokens/src/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use frame_support::traits::{
Contains, Get,
};
use sp_arithmetic::{traits::Bounded, ArithmeticError};
use sp_runtime::traits::Zero;
use sp_runtime::DispatchError;
use sp_runtime::DispatchResult;

pub struct Combiner<AccountId, TestKey, A, B>(sp_std::marker::PhantomData<(AccountId, TestKey, A, B)>);

Expand Down Expand Up @@ -350,3 +352,34 @@ where
T::set_total_issuance(GetCurrencyId::get(), amount)
}
}

impl<T: crate::Config> fungibles::approvals::Inspect<T::AccountId> for crate::Pallet<T> {
// Check the amount approved to be spent by an owner to a delegate
fn allowance(asset: T::CurrencyId, owner: &T::AccountId, delegate: &T::AccountId) -> T::Balance {
crate::Approvals::<T>::get((asset, &owner, &delegate))
.map(|x| x)
.unwrap_or_else(Zero::zero)
}
}

impl<T: crate::Config> fungibles::approvals::Mutate<T::AccountId> for crate::Pallet<T> {
// Approve spending tokens from a given account
fn approve(
asset: T::CurrencyId,
owner: &T::AccountId,
delegate: &T::AccountId,
amount: T::Balance,
) -> DispatchResult {
Self::do_approve(asset, owner, delegate, amount)
}

fn transfer_from(
asset: T::CurrencyId,
owner: &T::AccountId,
delegate: &T::AccountId,
dest: &T::AccountId,
amount: T::Balance,
) -> DispatchResult {
Self::do_transfer_from(asset, owner, delegate, dest, amount)
}
}
158 changes: 158 additions & 0 deletions tokens/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ pub mod module {
DeadAccount,
// Number of named reserves exceed `T::MaxReserves`
TooManyReserves,
Unapproved,
}

#[pallet::event]
Expand Down Expand Up @@ -365,6 +366,23 @@ pub mod module {
currency_id: T::CurrencyId,
amount: T::Balance,
},
/// (Additional) funds have been approved for transfer to a destination
/// account.
ApprovedTransfer {
currency_id: T::CurrencyId,
source: T::AccountId,
delegate: T::AccountId,
amount: T::Balance,
},
/// An `amount` was transferred in its entirety from `owner` to
/// `destination` by the approved `delegate`.
TransferredApproved {
currency_id: T::CurrencyId,
owner: T::AccountId,
delegate: T::AccountId,
destination: T::AccountId,
amount: T::Balance,
},
}

/// The total issuance of a token type.
Expand Down Expand Up @@ -417,6 +435,22 @@ pub mod module {
ValueQuery,
>;

#[pallet::storage]
/// Approved balance transfers. First balance is the amount approved for
/// transfer. Second is the amount of `T::Currency` reserved for storing
/// this. First key is the asset ID, second key is the owner and third key
/// is the delegate.
pub(super) type Approvals<T: Config> = StorageNMap<
_,
(
NMapKey<Blake2_128Concat, T::CurrencyId>,
NMapKey<Blake2_128Concat, T::AccountId>, // owner
NMapKey<Blake2_128Concat, T::AccountId>, // delegate
),
T::Balance,
OptionQuery,
>;

#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub balances: Vec<(T::AccountId, T::CurrencyId, T::Balance)>,
Expand Down Expand Up @@ -650,6 +684,74 @@ pub mod module {

Ok(())
}

/// Approve an amount of asset for transfer by a delegated third-party
/// account.
///
/// Origin must be Signed.
///
/// Ensures that `ApprovalDeposit` worth of `Currency` is reserved from
/// signing account for the purpose of holding the approval. If some
/// non-zero amount of assets is already approved from signing account
/// to `delegate`, then it is topped up or unreserved to
/// meet the right value.
///
/// NOTE: The signing account does not need to own `amount` of assets at
/// the point of making this call.
///
/// - `id`: The identifier of the asset.
/// - `delegate`: The account to delegate permission to transfer asset.
/// - `amount`: The amount of asset that may be transferred by
/// `delegate`. If there is
/// already an approval in place, then this acts additively.
///
/// Emits `ApprovedTransfer` on success.
///
/// Weight: `O(1)`
#[pallet::call_index(5)]
#[pallet::weight(T::WeightInfo::approve())]
pub fn approve(
origin: OriginFor<T>,
currency_id: T::CurrencyId,
delegate: T::AccountId,
#[pallet::compact] amount: T::Balance,
) -> DispatchResult {
let owner = ensure_signed(origin)?;
Self::do_approve(currency_id, &owner, &delegate, amount)
}

/// Transfer some asset balance from a previously delegated account to
/// some third-party account.
///
/// Origin must be Signed and there must be an approval in place by the
/// `owner` to the signer.
///
/// If the entire amount approved for transfer is transferred, then any
/// deposit previously reserved by `approve` is unreserved.
///
/// - `id`: The identifier of the asset.
/// - `owner`: The account which previously approved for a transfer of
/// at least `amount` and
/// from which the asset balance will be withdrawn.
/// - `destination`: The account to which the asset balance of `amount`
/// will be transferred.
/// - `amount`: The amount of assets to transfer.
///
/// Emits `TransferredApproved` on success.
///
/// Weight: `O(1)`
#[pallet::call_index(6)]
#[pallet::weight(T::WeightInfo::transfer_from())]
pub fn transfer_from(
origin: OriginFor<T>,
currency_id: T::CurrencyId,
owner: T::AccountId,
destination: T::AccountId,
#[pallet::compact] amount: T::Balance,
) -> DispatchResult {
let delegate = ensure_signed(origin)?;
Self::do_transfer_from(currency_id, &owner, &delegate, &destination, amount)
}
}
}

Expand Down Expand Up @@ -1130,6 +1232,62 @@ impl<T: Config> Pallet<T> {
});
Ok(amount)
}

/// Creates an approval from `owner` to spend `amount` of asset `id` tokens
/// by 'delegate' while reserving `T::ApprovalDeposit` from owner
///
/// If an approval already exists, the new amount is added to such existing
/// approval
pub(crate) fn do_approve(
id: T::CurrencyId,
owner: &T::AccountId,
delegate: &T::AccountId,
amount: T::Balance,
) -> DispatchResult {
if amount == Default::default() {
Approvals::<T>::remove((id.clone(), &owner, &delegate));
} else {
Approvals::<T>::set((id.clone(), &owner, &delegate), Some(amount));
}
Self::deposit_event(Event::ApprovedTransfer {
currency_id: id,
source: owner.clone(),
delegate: delegate.clone(),
amount,
});

Ok(())
}

/// Reduces the asset `id` balance of `owner` by some `amount` and increases
/// the balance of `dest` by (similar) amount, checking that 'delegate' has
/// an existing approval from `owner` to spend`amount`.
///
/// Will fail if `amount` is greater than the approval from `owner` to
/// 'delegate' Will unreserve the deposit from `owner` if the entire
/// approved `amount` is spent by 'delegate'
pub(crate) fn do_transfer_from(
id: T::CurrencyId,
owner: &T::AccountId,
delegate: &T::AccountId,
destination: &T::AccountId,
amount: T::Balance,
) -> DispatchResult {
Approvals::<T>::try_mutate_exists((id.clone(), &owner, delegate), |maybe_approved| -> DispatchResult {
let approved = maybe_approved.take().ok_or(Error::<T>::Unapproved)?;
let remaining = approved.checked_sub(&amount).ok_or(Error::<T>::Unapproved)?;

Self::do_transfer(id, &owner, &destination, amount, ExistenceRequirement::AllowDeath)?;

if remaining.is_zero() {
Approvals::<T>::remove((id.clone(), &owner, &delegate));
} else {
*maybe_approved = Some(remaining);
}
Ok(())
})?;
Ok(())
}
}

impl<T: Config> MultiCurrency<T::AccountId> for Pallet<T> {
Expand Down
80 changes: 80 additions & 0 deletions tokens/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#![cfg(test)]

use super::*;
use frame_support::traits::fungibles::approvals::Inspect;
use frame_support::{assert_noop, assert_ok};
use frame_system::RawOrigin;
use mock::*;
Expand Down Expand Up @@ -1269,3 +1270,82 @@ fn post_transfer_can_use_new_balance() {
));
});
}

#[test]
fn approval_lifecycle_works() {
ExtBuilder::default()
.balances(vec![(ALICE, DOT, 100)])
.build()
.execute_with(|| {
assert_ok!(Tokens::approve(Some(ALICE).into(), DOT, BOB, 50));
assert_eq!(Approvals::<Runtime>::get((DOT, ALICE, BOB)).unwrap(), 50);
assert_ok!(Tokens::approve(Some(ALICE).into(), DOT, BOB, 20));
assert_eq!(Approvals::<Runtime>::get((DOT, ALICE, BOB)).unwrap(), 20);

assert_ok!(Tokens::transfer_from(Some(BOB).into(), DOT, ALICE, CHARLIE, 10));
assert_eq!(Tokens::free_balance(DOT, &ALICE), 90);
assert_eq!(Tokens::free_balance(DOT, &BOB), 0);
assert_eq!(Tokens::free_balance(DOT, &CHARLIE), 10);
assert_eq!(Approvals::<Runtime>::get((DOT, ALICE, BOB)).unwrap(), 10);

assert_ok!(Tokens::approve(Some(ALICE).into(), DOT, BOB, 0));
assert_eq!(Approvals::<Runtime>::get((DOT, ALICE, BOB)), None);

assert_ok!(Tokens::approve(Some(ALICE).into(), DOT, BOB, 0));
assert_eq!(Approvals::<Runtime>::get((DOT, ALICE, BOB)), None);

assert_noop!(
Tokens::transfer_from(Some(BOB).into(), DOT, ALICE, CHARLIE, 10),
Error::<Runtime>::Unapproved
);
});
}

#[test]
fn transfer_from_all_funds() {
ExtBuilder::default()
.balances(vec![(ALICE, DOT, 100)])
.build()
.execute_with(|| {
assert_ok!(Tokens::approve(Some(ALICE).into(), DOT, BOB, 50));
assert_eq!(Approvals::<Runtime>::get((DOT, ALICE, BOB)).unwrap(), 50);
assert_eq!(Tokens::allowance(DOT, &ALICE, &BOB), 50);

// transfer the full amount, which should trigger auto-cleanup
assert_ok!(Tokens::transfer_from(Some(BOB).into(), DOT, ALICE, BOB, 50));
assert_eq!(Approvals::<Runtime>::get((DOT, ALICE, BOB)), None);

assert_eq!(Tokens::free_balance(DOT, &ALICE), 50);
assert_eq!(Tokens::free_balance(DOT, &BOB), 50);
});
}

#[test]
fn cannot_transfer_more_than_approved() {
ExtBuilder::default()
.balances(vec![(ALICE, DOT, 100)])
.build()
.execute_with(|| {
assert_ok!(Tokens::approve(Some(ALICE).into(), DOT, BOB, 50));
assert_eq!(Approvals::<Runtime>::get((DOT, ALICE, BOB)).unwrap(), 50);
assert_noop!(
Tokens::transfer_from(Some(BOB).into(), DOT, ALICE, BOB, 51),
Error::<Runtime>::Unapproved
);
});
}

#[test]
fn cannot_transfer_more_than_exists() {
ExtBuilder::default()
.balances(vec![(ALICE, DOT, 100)])
.build()
.execute_with(|| {
assert_ok!(Tokens::approve(Some(ALICE).into(), DOT, BOB, 101));
assert_eq!(Approvals::<Runtime>::get((DOT, ALICE, BOB)).unwrap(), 101);
assert_noop!(
Tokens::transfer_from(Some(BOB).into(), DOT, ALICE, BOB, 101),
Error::<Runtime>::BalanceTooLow
);
});
}
12 changes: 12 additions & 0 deletions tokens/src/weights.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ pub trait WeightInfo {
fn transfer_keep_alive() -> Weight;
fn force_transfer() -> Weight;
fn set_balance() -> Weight;
fn approve() -> Weight;
fn transfer_from() -> Weight;
}

/// Default weights.
Expand Down Expand Up @@ -63,4 +65,14 @@ impl WeightInfo for () {
.saturating_add(RocksDbWeight::get().reads(3 as u64))
.saturating_add(RocksDbWeight::get().writes(3 as u64))
}
fn approve() -> Weight {
Weight::from_parts(38_000_000, 0)
.saturating_add(RocksDbWeight::get().reads(5 as u64))
.saturating_add(RocksDbWeight::get().writes(4 as u64))
}
fn transfer_from() -> Weight {
Weight::from_parts(69_000_000, 0)
.saturating_add(RocksDbWeight::get().reads(5 as u64))
.saturating_add(RocksDbWeight::get().writes(4 as u64))
}
}