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 15 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
10 changes: 7 additions & 3 deletions pallets/payment/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This pallet allows users to create secure reversible payments that keep funds lo
- Resolver Account: A resolver account is assigned to every payment created, this account has the privilege to cancel/release a payment that has been disputed.
- Remark: The pallet allows to create payments by optionally providing some extra(limited) amount of bytes, this is reffered to as Remark. This can be used by a marketplace to seperate/tag payments.
- CancelBufferBlockLength: This is the time window where the recipient can dispute a cancellation request from the payment creator.
- MaxTasksPerBlock: The max count of tasks that can be added to a block, this number should be small so that the entire block is not consumed by the on_initialize function.

## Interface

Expand All @@ -31,7 +32,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 +50,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 +73,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
25 changes: 0 additions & 25 deletions pallets/payment/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ use super::*;

use crate::{Pallet as Payment, Payment as PaymentStore};
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 virto_primitives::Asset;

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
129 changes: 82 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 @@ -36,6 +39,7 @@ pub mod pallet {
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 All @@ -53,6 +57,9 @@ pub mod pallet {
/// Maximum permitted size of `Remark`
#[pallet::constant]
type MaxRemarkLength: Get<u32>;
/// Maximum scheduled tasks per block - this number should be small enough to ensure entire block is not consumed by on_init hook
#[pallet::constant]
type MaxTasksPerBlock: Get<u32>;
stanly-johnson marked this conversation as resolved.
Show resolved Hide resolved
/// Buffer period - number of blocks to wait before user can claim canceled payment
#[pallet::constant]
type CancelBufferBlockLength: Get<Self::BlockNumber>;
Expand All @@ -65,7 +72,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 +87,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 +145,47 @@ 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 {
while remaining_weight > T::WeightInfo::cancel() {
let task = ScheduledTasks::<T>::iter().next();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if every call to next is a new read to the storage? if that's the case we probably need to substract it from the remaining weight. I wonder if there's a better way to go through the list of tasks since we could be potentially spending quite some time every block just skipping tasks 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for being indecisive, I'm thinking maybe we should mix on_initialize and on_idle? in a not so good case imagine merchants have a week or a month to react to the refund and suppose there's a bunch of releases queued but only the few that can actually be executed are at the end of the iteration meaning we can often run out weight before we get to do any refunds.

We could instead have "two queues", first a map that indexes by "future block number" -> [(from, to), ...(MAX)] where MAX is the bound representing the maximum refunds that can be requested in a block that will likely be a small number, if it gets full we can fail in the request_refund with a specific error, it will be up to the front-end to try again and make it nice to the user. This queue is processed in the on_initialize for each block where we keep work to a minimum basically just moving the everything in the bounded vec to the second "ready queue" that is the double map you have here. That way when we iterate here we don't need to check the block number because every task is ready to be processed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the issue the cancel_block timing? Could you take a look at 4d2e0cc, so I've reduced it to one DB read, then make a Vec sorted by the cancel_block, so if the first one cannot be cancelled we can return all, also helps when we lag behind if the blocks are full. I've benchmarked the weights for reading and writing the Attributes storage, will add that to the expected weights if this looks ok

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the cost to read/write the scheduled tasks in 75ee30e wdyt?


match task {
Some((from, to, ScheduledTask { task, when })) => {
// early return if the expiry block is in future
if when > now {
return remaining_weight
}

// process the task
match task {
Task::Cancel => {
remaining_weight -= T::WeightInfo::cancel();
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),
);
Self::deposit_event(Event::PaymentCancelled { from, to });
},
}
},
_ => return remaining_weight,
}
}
remaining_weight
}
}

#[pallet::call]
impl<T: Config> Pallet<T> {
Expand All @@ -144,7 +200,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 +232,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 +348,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 +373,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 +393,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 +428,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 +567,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
32 changes: 25 additions & 7 deletions pallets/payment/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use crate as payment;
use crate::PaymentDetail;
use frame_support::{
parameter_types,
traits::{Contains, Everything, GenesisBuild, OnFinalize, OnInitialize},
traits::{Contains, Everything, GenesisBuild, Hooks, OnFinalize},
weights::DispatchClass,
};
use frame_system as system;
use orml_traits::parameter_type_with_key;
Expand All @@ -21,6 +22,8 @@ pub type Balance = u128;
pub type AccountId = u8;
pub const PAYMENT_CREATOR: AccountId = 10;
pub const PAYMENT_RECIPENT: AccountId = 11;
pub const PAYMENT_CREATOR_TWO: AccountId = 30;
pub const PAYMENT_RECIPENT_TWO: AccountId = 31;
pub const CURRENCY_ID: Asset = Asset::Network(NetworkAsset::KSM);
pub const RESOLVER_ACCOUNT: AccountId = 12;
pub const FEE_RECIPIENT_ACCOUNT: AccountId = 20;
Expand Down Expand Up @@ -124,6 +127,7 @@ parameter_types! {
pub const IncentivePercentage: Percent = Percent::from_percent(10);
pub const MaxRemarkLength: u32 = 50;
pub const CancelBufferBlockLength: u64 = 600;
pub const MaxTasksPerBlock: u32 = 2;
}

impl payment::Config for Test {
Expand All @@ -134,16 +138,22 @@ impl payment::Config for Test {
type FeeHandler = MockFeeHandler;
type MaxRemarkLength = MaxRemarkLength;
type CancelBufferBlockLength = CancelBufferBlockLength;
type MaxTasksPerBlock = MaxTasksPerBlock;
type WeightInfo = ();
}

// Build genesis storage according to the mock runtime.
pub fn new_test_ext() -> sp_io::TestExternalities {
let mut t = system::GenesisConfig::default().build_storage::<Test>().unwrap();

orml_tokens::GenesisConfig::<Test> { balances: vec![(PAYMENT_CREATOR, CURRENCY_ID, 100)] }
.assimilate_storage(&mut t)
.unwrap();
orml_tokens::GenesisConfig::<Test> {
balances: vec![
(PAYMENT_CREATOR, CURRENCY_ID, 100),
(PAYMENT_CREATOR_TWO, CURRENCY_ID, 100),
],
}
.assimilate_storage(&mut t)
.unwrap();

let mut ext: sp_io::TestExternalities = t.into();
// need to set block number to 1 to test events
Expand All @@ -153,8 +163,16 @@ pub fn new_test_ext() -> sp_io::TestExternalities {

pub fn run_to_block(n: u64) {
while System::block_number() < n {
System::on_finalize(System::block_number());
System::set_block_number(System::block_number() + 1);
System::on_initialize(System::block_number());
let block_number = System::block_number();

// ensure the on_idle is executed
<frame_system::Pallet<Test>>::register_extra_weight_unchecked(
Payment::on_idle(block_number, 1_000_000_000),
DispatchClass::Mandatory,
);

<frame_system::Pallet<Test> as OnFinalize<u64>>::on_finalize(block_number);

System::set_block_number(block_number + 1);
}
}
Loading