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

Cancel RefundRequested transactions #198

Merged
merged 17 commits into from
Mar 10, 2022
Merged
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,4 @@ stop-parachain:

.PHONY: benchmark
benchmark:
cargo run --release --features=runtime-benchmarks -- benchmark --chain dev --execution=wasm --wasm-execution compiled --extrinsic="*" --pallet=$(pallet) --steps=20 --repeat=10 --raw --heap-pages=4096 --output ./pallets/payment/src/weights.rs --template ./.maintain/frame-weight-template.hbs
cargo run --release --features=runtime-benchmarks -- benchmark --chain dev --execution=wasm --wasm-execution compiled --extrinsic="*" --pallet=$(pallet) --steps=20 --repeat=10 --heap-pages=4096 --output ./pallets/payment/src/weights.rs --template ./.maintain/frame-weight-template.hbs
9 changes: 6 additions & 3 deletions pallets/payment/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ This pallet allows users to create secure reversible payments that keep funds lo
- `resolve_release_payment` - Allows assigned judge to release a payment
- `resolve_cancel_payment` - Allows assigned judge to cancel a payment
- `request_refund` - Allows the creator of the payment to trigger cancel with a buffer time.
- `claim_refund` - Allows the creator to claim payment refund after buffer time
- `dispute_refund` - Allows the recipient to dispute the payment request of sender
- `request_payment` - Create a payment that can be completed by the sender using the `accept_and_pay` extrinsic.
- `accept_and_pay` - Allows the sender to fulfill a payment request created by a recipient
Expand All @@ -50,10 +49,12 @@ pub struct PaymentDetail<T: pallet::Config> {
/// type of asset used for payment
pub asset: AssetIdOf<T>,
/// amount of asset used for payment
#[codec(compact)]
pub amount: BalanceOf<T>,
/// incentive amount that is credited to creator for resolving
#[codec(compact)]
pub incentive_amount: BalanceOf<T>,
/// enum to track payment lifecycle [Created, NeedsReview]
/// enum to track payment lifecycle [Created, NeedsReview, RefundRequested, Requested]
pub state: PaymentState<T::BlockNumber>,
/// account that can settle any disputes created in the payment
pub resolver_account: T::AccountId,
Expand All @@ -71,7 +72,9 @@ pub enum PaymentState<BlockNumber> {
/// A judge needs to review and release manually
NeedsReview,
/// The user has requested refund and will be processed by `BlockNumber`
RefundRequested(BlockNumber),
RefundRequested { cancel_block: BlockNumber },
/// The recipient of this transaction has created a request
PaymentRequested,
}
```

Expand Down
53 changes: 26 additions & 27 deletions pallets/payment/src/benchmarking.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
use super::*;

use crate::{Pallet as Payment, Payment as PaymentStore};
use crate::{Pallet as Payment, Payment as PaymentStore, ScheduledTasks};
use frame_benchmarking::{account, benchmarks, impl_benchmark_test_suite, whitelisted_caller};
use frame_support::traits::{OnFinalize, OnInitialize};
use frame_system::RawOrigin;
use orml_traits::MultiCurrency;
use sp_runtime::traits::One;
use sp_std::vec;
use sp_std::{vec, vec::Vec};
use virto_primitives::Asset;

const SEED: u32 = 0;
Expand All @@ -26,16 +24,6 @@ fn assert_last_event<T: Config>(generic_event: <T as Config>::Event) {
assert_eq!(event, &system_event);
}

pub fn run_to_block<T: Config>(n: T::BlockNumber) {
while frame_system::Pallet::<T>::block_number() < n {
frame_system::Pallet::<T>::on_finalize(frame_system::Pallet::<T>::block_number());
frame_system::Pallet::<T>::set_block_number(
frame_system::Pallet::<T>::block_number() + One::one(),
);
frame_system::Pallet::<T>::on_initialize(frame_system::Pallet::<T>::block_number());
}
}

benchmarks! {
where_clause { where T::Asset: MultiCurrency<
<T as frame_system::Config>::AccountId,
Expand Down Expand Up @@ -112,19 +100,6 @@ benchmarks! {
assert_last_event::<T>(Event::<T>::PaymentCreatorRequestedRefund { from: caller, to: recipent, expiry: 601u32.into() }.into());
}

// creator of payment can claim a refund
claim_refund {
let caller = whitelisted_caller();
let _ = T::Asset::deposit(get_currency_id(), &caller, INITIAL_AMOUNT);
let recipent : T::AccountId = account("recipient", 0, SEED);
Payment::<T>::pay(RawOrigin::Signed(caller.clone()).into(), recipent.clone(), get_currency_id(), SOME_AMOUNT, None)?;
Payment::<T>::request_refund(RawOrigin::Signed(caller.clone()).into(), recipent.clone())?;
run_to_block::<T>(700u32.into());
}: _(RawOrigin::Signed(caller.clone()), recipent.clone())
verify {
assert_last_event::<T>(Event::<T>::PaymentCancelled { from: caller, to: recipent}.into());
}

// recipient of a payment can dispute a refund request
dispute_refund {
let caller = whitelisted_caller();
Expand Down Expand Up @@ -156,6 +131,30 @@ benchmarks! {
verify {
assert_last_event::<T>(Event::<T>::PaymentRequestCompleted { from: sender, to: receiver}.into());
}

// the weight to read the next scheduled task
read_task {
let sender : T::AccountId = whitelisted_caller();
let receiver : T::AccountId = account("recipient", 0, SEED);
ScheduledTasks::<T>::insert(sender, receiver, ScheduledTask { task: Task::Cancel, when: 1u32.into() });
}: {
let task : Vec<(T::AccountId, T::AccountId, ScheduledTaskOf<T>)> = ScheduledTasks::<T>::iter().collect();
} verify {
let task : Vec<(T::AccountId, T::AccountId, ScheduledTaskOf<T>)> = ScheduledTasks::<T>::iter().collect();
assert_eq!(task.len(), 1);
}

// the weight to remove a scheduled task
remove_task {
let sender : T::AccountId = whitelisted_caller();
let receiver : T::AccountId = account("recipient", 0, SEED);
ScheduledTasks::<T>::insert(sender.clone(), receiver.clone(), ScheduledTask { task: Task::Cancel, when: 1u32.into() });
}: {
ScheduledTasks::<T>::remove(sender, receiver);
} verify {

}

}

impl_benchmark_test_suite!(Payment, crate::mock::new_test_ext(), crate::mock::Test,);
143 changes: 96 additions & 47 deletions pallets/payment/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ pub mod weights;
#[frame_support::pallet]
pub mod pallet {
pub use crate::{
types::{DisputeResolver, FeeHandler, PaymentDetail, PaymentHandler, PaymentState},
types::{
DisputeResolver, FeeHandler, PaymentDetail, PaymentHandler, PaymentState,
ScheduledTask, Task,
},
weights::WeightInfo,
};
use frame_support::{
Expand All @@ -30,12 +33,14 @@ pub mod pallet {
traits::{CheckedAdd, Saturating},
Percent,
};
use sp_std::vec::Vec;

pub type BalanceOf<T> =
<<T as Config>::Asset as MultiCurrency<<T as frame_system::Config>::AccountId>>::Balance;
pub type AssetIdOf<T> =
<<T as Config>::Asset as MultiCurrency<<T as frame_system::Config>::AccountId>>::CurrencyId;
pub type BoundedDataOf<T> = BoundedVec<u8, <T as Config>::MaxRemarkLength>;
pub type ScheduledTaskOf<T> = ScheduledTask<<T as frame_system::Config>::BlockNumber>;

#[pallet::config]
pub trait Config: frame_system::Config {
Expand Down Expand Up @@ -65,7 +70,7 @@ pub mod pallet {
pub struct Pallet<T>(_);

#[pallet::storage]
#[pallet::getter(fn rates)]
#[pallet::getter(fn payment)]
/// Payments created by a user, this method of storageDoubleMap is chosen since there is no usecase for
/// listing payments by provider/currency. The payment will only be referenced by the creator in
/// any transaction of interest.
Expand All @@ -80,6 +85,18 @@ pub mod pallet {
PaymentDetail<T>,
>;

#[pallet::storage]
#[pallet::getter(fn tasks)]
/// Store the list of tasks to be executed in the on_idle function
pub(super) type ScheduledTasks<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
T::AccountId, // payment creator
Blake2_128Concat,
T::AccountId, // payment recipient
ScheduledTaskOf<T>,
>;

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
Expand Down Expand Up @@ -126,10 +143,63 @@ pub mod pallet {
RefundNotRequested,
/// Dispute period has not passed
DisputePeriodNotPassed,
/// The automatic cancelation queue cannot accept
RefundQueueFull,
}

#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
/// Hook that execute when there is leftover space in a block
/// This function will look for any pending scheduled tasks that can
/// be executed and will process them.
fn on_idle(now: T::BlockNumber, mut remaining_weight: Weight) -> Weight {
let mut task_list: Vec<(T::AccountId, T::AccountId, ScheduledTaskOf<T>)> =
ScheduledTasks::<T>::iter().collect();
// sort the task list by the cancel_block
task_list.sort_by(|(_, _, t), (_, _, x)| t.when.partial_cmp(&x.when).unwrap());

while remaining_weight > T::WeightInfo::cancel() {
// get the next task to execute
let task = task_list.iter().next();
match task {
Some((from, to, ScheduledTask { task, when })) => {
// early return if the expiry block is in future
// since the task list is sorted by cancel block
// if the task cannot be cancelled we can return the whole set
if when > &now {
return remaining_weight
}

// process the task
match task {
Task::Cancel => {
// the remaining weight is the weight - cancel - remove from scheduled tasks
remaining_weight = remaining_weight.saturating_sub(
T::WeightInfo::cancel()
.saturating_add(T::WeightInfo::remove_task()),
);
ScheduledTasks::<T>::remove(from.clone(), to.clone());
// process the cancel payment
let _ = <Self as PaymentHandler<T>>::settle_payment(
from.clone(),
to.clone(),
Percent::from_percent(0),
);
// emit the cancel event
Self::deposit_event(Event::PaymentCancelled {
from: from.clone(),
to: to.clone(),
});
},
}
},
_ => return remaining_weight,
}
}

remaining_weight
}
}

#[pallet::call]
impl<T: Config> Pallet<T> {
Expand All @@ -144,7 +214,7 @@ pub mod pallet {
origin: OriginFor<T>,
recipient: T::AccountId,
asset: AssetIdOf<T>,
amount: BalanceOf<T>,
#[pallet::compact] amount: BalanceOf<T>,
remark: Option<BoundedDataOf<T>>,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
Expand Down Expand Up @@ -176,6 +246,8 @@ pub mod pallet {
// ensure the payment is in Created state
if let Some(payment) = Payment::<T>::get(&from, &to) {
ensure!(payment.state == PaymentState::Created, Error::<T>::InvalidAction)
} else {
fail!(Error::<T>::InvalidPayment);
}

// release is a settle_payment with 100% recipient_share
Expand Down Expand Up @@ -290,15 +362,22 @@ pub mod pallet {

// set the payment to requested refund
let current_block = frame_system::Pallet::<T>::block_number();
let can_cancel_block = current_block
let cancel_block = current_block
.checked_add(&T::CancelBufferBlockLength::get())
.ok_or(Error::<T>::MathError)?;
payment.state = PaymentState::RefundRequested(can_cancel_block);

ScheduledTasks::<T>::insert(
who.clone(),
recipient.clone(),
ScheduledTask { task: Task::Cancel, when: cancel_block },
);

payment.state = PaymentState::RefundRequested { cancel_block };

Self::deposit_event(Event::PaymentCreatorRequestedRefund {
from: who,
to: recipient,
expiry: can_cancel_block,
expiry: cancel_block,
});

Ok(())
Expand All @@ -308,43 +387,6 @@ pub mod pallet {
Ok(().into())
}

/// Allow payment creator to claim the refund if the payment recipent has not disputed
/// After the payment creator has `request_refund` can then call this extrinsic to
/// cancel the payment and receive the reserved amount to the account if the dispute period
/// has passed.
#[transactional]
#[pallet::weight(T::WeightInfo::claim_refund())]
pub fn claim_refund(
origin: OriginFor<T>,
recipient: T::AccountId,
) -> DispatchResultWithPostInfo {
use PaymentState::*;
let who = ensure_signed(origin)?;

if let Some(payment) = Payment::<T>::get(&who, &recipient) {
match payment.state {
NeedsReview => fail!(Error::<T>::PaymentNeedsReview),
Created | PaymentRequested => fail!(Error::<T>::RefundNotRequested),
RefundRequested(cancel_block) => {
let current_block = frame_system::Pallet::<T>::block_number();
// ensure the dispute period has passed
ensure!(current_block > cancel_block, Error::<T>::DisputePeriodNotPassed);
// cancel the payment and refund the creator
<Self as PaymentHandler<T>>::settle_payment(
who.clone(),
recipient.clone(),
Percent::from_percent(0),
)?;
Self::deposit_event(Event::PaymentCancelled { from: who, to: recipient });
},
}
} else {
fail!(Error::<T>::InvalidPayment);
}

Ok(().into())
}

/// Allow payment recipient to dispute the refund request from the payment creator
/// This does not cancel the request, instead sends the payment to a NeedsReview state
/// The assigned resolver account can then change the state of the payment after review.
Expand All @@ -365,9 +407,17 @@ pub mod pallet {
let payment = maybe_payment.as_mut().ok_or(Error::<T>::InvalidPayment)?;
// ensure the payment is in Requested Refund state
match payment.state {
RefundRequested(_) => {
RefundRequested { cancel_block } => {
ensure!(
cancel_block > frame_system::Pallet::<T>::block_number(),
Error::<T>::InvalidAction
);

payment.state = PaymentState::NeedsReview;

// remove the payment from scheduled tasks
ScheduledTasks::<T>::remove(creator.clone(), who.clone());

Self::deposit_event(Event::PaymentRefundDisputed {
from: creator,
to: who,
Expand All @@ -392,7 +442,7 @@ pub mod pallet {
origin: OriginFor<T>,
from: T::AccountId,
asset: AssetIdOf<T>,
amount: BalanceOf<T>,
#[pallet::compact] amount: BalanceOf<T>,
) -> DispatchResultWithPostInfo {
let to = ensure_signed(origin)?;

Expand Down Expand Up @@ -531,7 +581,6 @@ pub mod pallet {
/// For cancelling a payment, recipient_share = 0
/// For releasing a payment, recipient_share = 100
/// In other cases, the custom recipient_share can be specified
#[require_transactional]
fn settle_payment(
from: T::AccountId,
to: T::AccountId,
Expand Down
Loading