diff --git a/tokens/src/lib.rs b/tokens/src/lib.rs index 4735c5fcb..a0d247f6b 100644 --- a/tokens/src/lib.rs +++ b/tokens/src/lib.rs @@ -137,7 +137,7 @@ pub struct ReserveData { pub amount: Balance, } -/// balance information for an account. +/// Balance information for an account. #[derive(Encode, Decode, Clone, PartialEq, Eq, Default, MaxEncodedLen, RuntimeDebug, TypeInfo)] pub struct AccountData { /// Non-reserved part of the balance. There may still be restrictions on @@ -256,6 +256,8 @@ pub mod module { DeadAccount, // Number of named reserves exceed `T::MaxReserves` TooManyReserves, + /// The allowance is too low + AllowanceTooLow, } #[pallet::event] @@ -366,6 +368,13 @@ pub mod module { currency_id: T::CurrencyId, amount: T::Balance, }, + /// Some allowance was updated + AllowanceSet { + currency_id: T::CurrencyId, + owner: T::AccountId, + spender: T::AccountId, + amount: T::Balance, + }, } /// The total issuance of a token type. @@ -418,6 +427,19 @@ pub mod module { ValueQuery, >; + /// Allowances for accounts to spend approved funds. + #[pallet::storage] + pub(super) type Allowances = StorageNMap< + _, + ( + NMapKey, // currency + NMapKey, // owner + NMapKey, // spender + ), + T::Balance, // amount + ValueQuery, + >; + #[pallet::genesis_config] pub struct GenesisConfig { pub balances: Vec<(T::AccountId, T::CurrencyId, T::Balance)>, @@ -649,6 +671,84 @@ pub mod module { Ok(()) } + + /// Approve the `spender` to spend an `amount` from the free balance of `owner`. + /// + /// The dispatch origin for this call must be `Signed` by the owner. + /// + /// - `spender`: The account to approve spending. + /// - `currency_id`: currency type. + /// - `amount`: free balance amount to allow. + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::set_allowance())] + pub fn set_allowance( + origin: OriginFor, + spender: ::Source, + currency_id: T::CurrencyId, + #[pallet::compact] amount: T::Balance, + ) -> DispatchResult { + let owner = ensure_signed(origin)?; + let spender = T::Lookup::lookup(spender)?; + + Allowances::::insert((¤cy_id, &owner, &spender), amount); + + Self::deposit_event(Event::AllowanceSet { + currency_id, + owner, + spender, + amount, + }); + + Ok(()) + } + + /// Spender can transfer some free balance from the approved account. + /// + /// The dispatch origin for this call must be `Signed` by the spender. + /// + /// - `from`: The account which approved spending. + /// - `from`: The recipient of the transfer. + /// - `currency_id`: currency type. + /// - `amount`: free balance amount to allow. + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::set_balance())] + pub fn transfer_allowance( + origin: OriginFor, + from: ::Source, + to: ::Source, + currency_id: T::CurrencyId, + #[pallet::compact] amount: T::Balance, + ) -> DispatchResult { + let spender = ensure_signed(origin)?; + let owner = T::Lookup::lookup(from)?; + let destination = T::Lookup::lookup(to)?; + + let remaining = Allowances::::try_mutate( + (¤cy_id, &owner, &spender), + |allowance| -> Result { + let remaining = allowance.checked_sub(&amount).ok_or(Error::::AllowanceTooLow)?; + *allowance = remaining; + Ok(remaining) + }, + )?; + + Self::deposit_event(Event::AllowanceSet { + currency_id, + owner: owner.clone(), + spender, + amount: remaining, + }); + + Self::do_transfer( + currency_id, + &owner, + &destination, + amount, + ExistenceRequirement::KeepAlive, + )?; + + Ok(()) + } } } diff --git a/tokens/src/tests.rs b/tokens/src/tests.rs index 504ca06ab..8a68de190 100644 --- a/tokens/src/tests.rs +++ b/tokens/src/tests.rs @@ -264,6 +264,59 @@ fn set_balance_should_work() { }); } +#[test] +fn set_allowance_should_work() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100)]) + .build() + .execute_with(|| { + assert_eq!(Allowances::::get((DOT, ALICE, BOB)), 0); + assert_ok!(Tokens::set_allowance(Some(ALICE).into(), BOB, DOT, 100)); + assert_eq!(Allowances::::get((DOT, ALICE, BOB)), 100); + + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::AllowanceSet { + currency_id: DOT, + owner: ALICE, + spender: BOB, + amount: 100, + })); + }); +} + +#[test] +fn transfer_allowance_should_work() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 200)]) + .build() + .execute_with(|| { + assert_eq!(Tokens::free_balance(DOT, &ALICE), 200); + + assert_ok!(Tokens::set_allowance(Some(ALICE).into(), BOB, DOT, 200)); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::AllowanceSet { + currency_id: DOT, + owner: ALICE, + spender: BOB, + amount: 200, + })); + + assert_ok!(Tokens::transfer_allowance(Some(BOB).into(), ALICE, CHARLIE, DOT, 100)); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::AllowanceSet { + currency_id: DOT, + owner: ALICE, + spender: BOB, + amount: 100, + })); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Transfer { + currency_id: DOT, + from: ALICE, + to: CHARLIE, + amount: 100, + })); + + assert_eq!(Allowances::::get((DOT, ALICE, BOB)), 100); + }); +} + // ************************************************* // tests for inline impl // ************************************************* diff --git a/tokens/src/weights.rs b/tokens/src/weights.rs index 8f5637160..85e06d7d2 100644 --- a/tokens/src/weights.rs +++ b/tokens/src/weights.rs @@ -34,6 +34,8 @@ pub trait WeightInfo { fn transfer_keep_alive() -> Weight; fn force_transfer() -> Weight; fn set_balance() -> Weight; + fn set_allowance() -> Weight; + fn transfer_allowance() -> Weight; } /// Default weights. @@ -63,4 +65,14 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(3 as u64)) .saturating_add(RocksDbWeight::get().writes(3 as u64)) } + fn set_allowance() -> Weight { + Weight::from_parts(34_000_000, 0) + .saturating_add(RocksDbWeight::get().reads(3 as u64)) + .saturating_add(RocksDbWeight::get().writes(3 as u64)) + } + fn transfer_allowance() -> Weight { + Weight::from_parts(34_000_000, 0) + .saturating_add(RocksDbWeight::get().reads(3 as u64)) + .saturating_add(RocksDbWeight::get().writes(3 as u64)) + } }