Skip to content

Commit 8fa699d

Browse files
authored
feat: explicit reverser for reversible accounts (#8)
* Add explicit reverser for reversible transfers * Make reverser get reserved funds, update benchmarks * Add more tests
1 parent 9ab7282 commit 8fa699d

6 files changed

Lines changed: 347 additions & 100 deletions

File tree

pallets/reversible-transfers/src/benchmarking.rs

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,16 @@ fn setup_reversible_account<T: Config>(
4040
who: T::AccountId,
4141
delay: BlockNumberFor<T>,
4242
policy: DelayPolicy,
43+
reverser: Option<T::AccountId>,
4344
) {
44-
ReversibleAccounts::<T>::insert(who, (delay, policy));
45+
ReversibleAccounts::<T>::insert(
46+
who,
47+
ReversibleAccountData {
48+
delay,
49+
policy,
50+
explicit_reverser: reverser,
51+
},
52+
);
4553
}
4654

4755
// Helper to fund an account (requires Balances pallet in mock runtime)
@@ -77,6 +85,7 @@ mod benchmarks {
7785
#[benchmark]
7886
fn set_reversibility() -> Result<(), BenchmarkError> {
7987
let caller: T::AccountId = whitelisted_caller();
88+
let explicit_reverser: T::AccountId = benchmark_account("explicit_reverser", 0, SEED);
8089
let delay: BlockNumberFor<T> = T::DefaultDelay::get();
8190
let policy = DelayPolicy::Explicit;
8291

@@ -85,9 +94,17 @@ mod benchmarks {
8594
RawOrigin::Signed(caller.clone()),
8695
Some(delay),
8796
policy.clone(),
97+
Some(explicit_reverser.clone()),
8898
);
8999

90-
assert_eq!(ReversibleAccounts::<T>::get(&caller), Some((delay, policy)));
100+
assert_eq!(
101+
ReversibleAccounts::<T>::get(&caller),
102+
Some(ReversibleAccountData {
103+
delay,
104+
policy,
105+
explicit_reverser: Some(explicit_reverser),
106+
})
107+
);
91108

92109
Ok(())
93110
}
@@ -102,7 +119,7 @@ mod benchmarks {
102119

103120
// Setup caller as reversible
104121
let delay = T::DefaultDelay::get();
105-
setup_reversible_account::<T>(caller.clone(), delay, DelayPolicy::Explicit);
122+
setup_reversible_account::<T>(caller.clone(), delay, DelayPolicy::Explicit, None);
106123

107124
let call = make_transfer_call::<T>(recipient.clone(), transfer_amount)?;
108125
let tx_id = T::Hashing::hash_of(&(caller.clone(), call).encode());
@@ -129,13 +146,22 @@ mod benchmarks {
129146
#[benchmark]
130147
fn cancel() -> Result<(), BenchmarkError> {
131148
let caller: T::AccountId = whitelisted_caller();
149+
let reverser: T::AccountId = benchmark_account("reverser", 1, SEED);
150+
132151
fund_account::<T>(&caller, BalanceOf::<T>::from(1000u128));
152+
fund_account::<T>(&reverser, BalanceOf::<T>::from(1000u128));
133153
let recipient: T::AccountId = benchmark_account("recipient", 0, SEED);
134154
let transfer_amount = 100u128;
135155

136156
// Setup caller as reversible and schedule a task in setup
137157
let delay = T::DefaultDelay::get();
138-
setup_reversible_account::<T>(caller.clone(), delay, DelayPolicy::Explicit);
158+
// Worst case scenario: reverser is explicit
159+
setup_reversible_account::<T>(
160+
caller.clone(),
161+
delay,
162+
DelayPolicy::Explicit,
163+
Some(reverser.clone()),
164+
);
139165
let call = make_transfer_call::<T>(recipient.clone(), transfer_amount)?;
140166

141167
// Use internal function directly in setup - requires RuntimeOrigin from Config
@@ -152,7 +178,7 @@ mod benchmarks {
152178

153179
// Benchmark the cancel extrinsic
154180
#[extrinsic_call]
155-
_(RawOrigin::Signed(caller.clone()), tx_id);
181+
_(RawOrigin::Signed(reverser), tx_id);
156182

157183
assert_eq!(AccountPendingIndex::<T>::get(&caller), 0);
158184
assert!(!PendingTransfers::<T>::contains_key(&tx_id));
@@ -173,7 +199,7 @@ mod benchmarks {
173199

174200
// Setup owner as reversible and schedule a task in setup
175201
let delay = T::DefaultDelay::get();
176-
setup_reversible_account::<T>(owner.clone(), delay, DelayPolicy::Explicit);
202+
setup_reversible_account::<T>(owner.clone(), delay, DelayPolicy::Explicit, None);
177203
let call = make_transfer_call::<T>(recipient.clone(), transfer_amount)?;
178204

179205
let owner_origin = RawOrigin::Signed(owner.clone()).into();

pallets/reversible-transfers/src/lib.rs

Lines changed: 106 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,21 @@ pub mod weights;
2222
pub use weights::WeightInfo;
2323

2424
use alloc::vec::Vec;
25+
use frame_support::traits::tokens::{Fortitude, Restriction};
2526
use frame_support::{pallet_prelude::*, traits::schedule::DispatchTime};
2627
use frame_system::pallet_prelude::*;
2728
use sp_runtime::traits::StaticLookup;
2829

2930
/// How to delay transactions
30-
/// - `Explicit`: Only delay transactions explicitly using `schedule_dispatch`.
31+
/// - `Explicit`: Only delay transactions explicitly using `schedule_transfer`.
3132
/// - `Intercept`: Intercept and delay transactions at the `TransactionExtension` level.
3233
///
3334
/// For example, for a reversible account with `DelayPolicy::Intercept`, the transaction
34-
/// will be delayed even if the user doesn't explicitly call `schedule_dispatch`. And for `DelayPolicy::Explicit`,
35-
/// the transaction will be delayed only if the user explicitly calls this pallet's `schedule_dispatch` extrinsic.
35+
/// will be delayed even if the user doesn't explicitly call `schedule_transfer`. And for `DelayPolicy::Explicit`,
36+
/// the transaction will be delayed only if the user explicitly calls this pallet's `schedule_transfer` extrinsic.
3637
#[derive(Encode, Decode, MaxEncodedLen, Clone, Default, TypeInfo, Debug, PartialEq, Eq)]
3738
pub enum DelayPolicy {
38-
/// Only explicitly delay transactions using `schedule_dispatch` call
39+
/// Only explicitly delay transactions using `schedule_transfer` call
3940
#[default]
4041
Explicit,
4142
/// Intercept and delay transactions at `TransactionExtension` level. This is not UX friendly
@@ -46,6 +47,17 @@ pub enum DelayPolicy {
4647
Intercept,
4748
}
4849

50+
/// Reversible account details
51+
#[derive(Encode, Decode, MaxEncodedLen, Clone, Default, TypeInfo, Debug, PartialEq, Eq)]
52+
pub struct ReversibleAccountData<AccountId, BlockNumber> {
53+
/// The account that can reverse the transaction. `None` means the account itself.
54+
pub explicit_reverser: Option<AccountId>,
55+
/// The delay period for the account
56+
pub delay: BlockNumber,
57+
/// The policy for the account
58+
pub policy: DelayPolicy,
59+
}
60+
4961
/// Pending transfer details
5062
#[derive(Encode, Decode, MaxEncodedLen, Clone, Default, TypeInfo, Debug, PartialEq, Eq)]
5163
pub struct PendingTransfer<AccountId, Balance, Call> {
@@ -142,7 +154,7 @@ pub mod pallet {
142154
_,
143155
Blake2_128Concat,
144156
T::AccountId,
145-
(BlockNumberFor<T>, DelayPolicy),
157+
ReversibleAccountData<T::AccountId, BlockNumberFor<T>>,
146158
OptionQuery,
147159
>;
148160

@@ -172,8 +184,7 @@ pub mod pallet {
172184
/// [who, maybe_delay: None means disabled]
173185
ReversibilitySet {
174186
who: T::AccountId,
175-
delay: BlockNumberFor<T>,
176-
policy: DelayPolicy,
187+
data: ReversibleAccountData<T::AccountId, BlockNumberFor<T>>,
177188
},
178189
/// A transaction has been intercepted and scheduled for delayed execution.
179190
/// [who, tx_id, execute_at_moment]
@@ -199,6 +210,8 @@ pub mod pallet {
199210
AccountAlreadyReversible,
200211
/// The account attempting the action is not marked as reversible.
201212
AccountNotReversible,
213+
/// Reverser can not be the account itself, because it is redundant.
214+
ExplicitReverserCanNotBeSelf,
202215
/// The specified pending transaction ID was not found.
203216
PendingTxNotFound,
204217
/// The caller is not the original submitter of the transaction they are trying to cancel.
@@ -217,6 +230,8 @@ pub mod pallet {
217230
InvalidCall,
218231
/// Invalid scheduler origin
219232
InvalidSchedulerOrigin,
233+
/// Reverser is invalid
234+
InvalidReverser,
220235
}
221236

222237
#[pallet::call]
@@ -235,19 +250,34 @@ pub mod pallet {
235250
origin: OriginFor<T>,
236251
delay: Option<BlockNumberFor<T>>,
237252
policy: DelayPolicy,
253+
reverser: Option<T::AccountId>,
238254
) -> DispatchResult {
239255
let who = ensure_signed(origin)?;
240256

241257
ensure!(
242258
!ReversibleAccounts::<T>::contains_key(&who),
243259
Error::<T>::AccountAlreadyReversible
244260
);
261+
ensure!(
262+
reverser != Some(who.clone()),
263+
Error::<T>::ExplicitReverserCanNotBeSelf
264+
);
265+
245266
let delay = delay.unwrap_or(T::DefaultDelay::get());
246267

247268
ensure!(delay >= T::MinDelayPeriod::get(), Error::<T>::DelayTooShort);
248269

249-
ReversibleAccounts::<T>::insert(&who, (delay, policy.clone()));
250-
Self::deposit_event(Event::ReversibilitySet { who, delay, policy });
270+
let reversible_account_data = ReversibleAccountData {
271+
explicit_reverser: reverser,
272+
delay,
273+
policy: policy.clone(),
274+
};
275+
276+
ReversibleAccounts::<T>::insert(&who, reversible_account_data.clone());
277+
Self::deposit_event(Event::ReversibilitySet {
278+
who,
279+
data: reversible_account_data,
280+
});
251281

252282
Ok(())
253283
}
@@ -320,7 +350,9 @@ pub mod pallet {
320350
T: pallet_balances::Config<RuntimeHoldReason = <T as Config>::RuntimeHoldReason>,
321351
{
322352
/// Check if an account has reversibility enabled and return its delay.
323-
pub fn is_reversible(who: &T::AccountId) -> Option<(BlockNumberFor<T>, DelayPolicy)> {
353+
pub fn is_reversible(
354+
who: &T::AccountId,
355+
) -> Option<ReversibleAccountData<T::AccountId, BlockNumberFor<T>>> {
324356
ReversibleAccounts::<T>::get(who)
325357
}
326358

@@ -398,8 +430,18 @@ pub mod pallet {
398430
amount: BalanceOf<T>,
399431
) -> DispatchResult {
400432
let who = ensure_signed(origin)?;
401-
let (delay, _) =
402-
Self::reversible_accounts(&who).ok_or(Error::<T>::AccountNotReversible)?;
433+
let ReversibleAccountData {
434+
delay,
435+
explicit_reverser,
436+
policy: _,
437+
} = Self::reversible_accounts(&who).ok_or(Error::<T>::AccountNotReversible)?;
438+
439+
match explicit_reverser {
440+
Some(reverser) => {
441+
ensure!(who != reverser, Error::<T>::InvalidReverser);
442+
}
443+
None => {}
444+
};
403445

404446
let transfer_call: T::RuntimeCall = pallet_balances::Call::<T>::transfer_keep_alive {
405447
dest: dest.clone(),
@@ -480,23 +522,33 @@ pub mod pallet {
480522
/// Cancels a previously scheduled transaction. Internal logic used by `cancel` extrinsic.
481523
fn cancel_transfer(who: &T::AccountId, tx_id: T::Hash) -> DispatchResult {
482524
// Retrieve owner from storage to verify ownership
483-
let pending =
484-
PendingTransfers::<T>::get(tx_id).ok_or(Error::<T>::PendingTxNotFound)?;
525+
let pending = PendingTransfers::<T>::get(tx_id).ok_or(Error::<T>::PendingTxNotFound)?;
485526

486-
ensure!(&pending.who == who, Error::<T>::NotOwner);
527+
let reversible_account_data = ReversibleAccounts::<T>::get(&pending.who)
528+
.ok_or(Error::<T>::AccountNotReversible)?;
487529

488-
// Remove from main storage
489-
PendingTransfers::<T>::mutate(tx_id, |pending_opt| {
490-
if let Some(pending) = pending_opt {
491-
if pending.count > 1 {
492-
// If there are more than one identical transactions, decrement the count
493-
pending.count = pending.count.saturating_sub(1);
494-
} else {
495-
// Otherwise, remove the transaction from storage
496-
*pending_opt = None;
497-
}
498-
}
499-
});
530+
if let Some(explicit_reverser) = &reversible_account_data.explicit_reverser {
531+
// If the reverser is set, ensure the caller is the reverser
532+
ensure!(who == explicit_reverser, Error::<T>::InvalidReverser);
533+
} else {
534+
ensure!(&pending.who == who, Error::<T>::NotOwner);
535+
}
536+
537+
if pending.count > 1 {
538+
// If there are more than one identical transactions, decrement the count
539+
PendingTransfers::<T>::insert(
540+
&tx_id,
541+
PendingTransfer {
542+
who: pending.who.clone(),
543+
call: pending.call,
544+
amount: pending.amount,
545+
count: pending.count.saturating_sub(1),
546+
},
547+
);
548+
} else {
549+
// Otherwise, remove the transaction from storage
550+
PendingTransfers::<T>::remove(&tx_id);
551+
}
500552

501553
// Decrement account index
502554
AccountPendingIndex::<T>::mutate(&pending.who, |current_count| {
@@ -509,14 +561,25 @@ pub mod pallet {
509561
// Cancel the scheduled task
510562
T::Scheduler::cancel_named(schedule_id).map_err(|_| Error::<T>::CancellationFailed)?;
511563

512-
// Release the funds
513-
pallet_balances::Pallet::<T>::release(
514-
&HoldReason::ScheduledTransfer.into(),
515-
&pending.who,
516-
pending.amount,
517-
Precision::Exact,
518-
)?;
519-
564+
if let Some(reverser) = &reversible_account_data.explicit_reverser {
565+
pallet_balances::Pallet::<T>::transfer_on_hold(
566+
&HoldReason::ScheduledTransfer.into(),
567+
&pending.who,
568+
reverser,
569+
pending.amount,
570+
Precision::Exact,
571+
Restriction::Free,
572+
Fortitude::Polite,
573+
)?;
574+
} else {
575+
// Release the funds
576+
pallet_balances::Pallet::<T>::release(
577+
&HoldReason::ScheduledTransfer.into(),
578+
&pending.who,
579+
pending.amount,
580+
Precision::Exact,
581+
)?;
582+
}
520583
Self::deposit_event(Event::TransactionCancelled {
521584
who: who.clone(),
522585
tx_id,
@@ -538,7 +601,14 @@ pub mod pallet {
538601
for (who, delay) in &self.initial_reversible_accounts {
539602
// Basic validation, ensure delay is reasonable if needed
540603
if *delay >= T::MinDelayPeriod::get() {
541-
ReversibleAccounts::<T>::insert(who, (delay, DelayPolicy::Explicit));
604+
ReversibleAccounts::<T>::insert(
605+
who,
606+
ReversibleAccountData {
607+
explicit_reverser: None,
608+
delay: *delay,
609+
policy: DelayPolicy::Explicit,
610+
},
611+
);
542612
} else {
543613
// Optionally log a warning during genesis build
544614
log::warn!(

pallets/reversible-transfers/src/mock.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ pub fn new_test_ext() -> sp_io::TestExternalities {
122122
.unwrap();
123123

124124
pallet_balances::GenesisConfig::<Test> {
125-
balances: vec![(1, 1_000_000_000_000_000), (2, 2)],
125+
balances: vec![(1, 1_000_000_000_000_000), (2, 2), (255, 100_000_000_000)],
126126
}
127127
.assimilate_storage(&mut t)
128128
.unwrap();

0 commit comments

Comments
 (0)