Skip to content

Commit

Permalink
Support subsidised batches. (#1747)
Browse files Browse the repository at this point in the history
* Support subsidised batches.

* Fix tests.

* Add unit tests for subsidised batche.

* Support batch_all and force_batch.

* Add more unit tests.
  • Loading branch information
Neopallium authored Oct 28, 2024
1 parent dfcb80d commit a1e591b
Show file tree
Hide file tree
Showing 7 changed files with 243 additions and 43 deletions.
13 changes: 8 additions & 5 deletions pallets/common/src/traits/relayer.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{traits::identity, CommonConfig};
use crate::traits::identity;
use frame_support::{decl_event, weights::Weight};
use polymesh_primitives::{Balance, EventDid};
use sp_runtime::transaction_validity::InvalidTransaction;
Expand All @@ -12,25 +12,28 @@ pub trait WeightInfo {
fn decrease_polyx_limit() -> Weight;
}

pub trait SubsidiserTrait<AccountId> {
pub trait SubsidiserTrait<AccountId, RuntimeCall> {
/// Check if a `user_key` has a subsidiser and that the subsidy can pay the `fee`.
fn check_subsidy(
user_key: &AccountId,
fee: Balance,
pallet: Option<&[u8]>,
call: Option<&RuntimeCall>,
) -> Result<Option<AccountId>, InvalidTransaction>;

/// Debit `fee` from the remaining balance of the subsidy for `user_key`.
fn debit_subsidy(
user_key: &AccountId,
fee: Balance,
) -> Result<Option<AccountId>, InvalidTransaction>;
}

pub trait Config: CommonConfig + identity::Config {
pub trait Config: frame_system::Config + identity::Config {
/// The overarching event type.
type RuntimeEvent: From<Event<Self>> + Into<<Self as frame_system::Config>::RuntimeEvent>;

/// Subsidy pallet weights.
type WeightInfo: WeightInfo;
/// Subsidy call filter.
type SubsidyCallFilter: frame_support::traits::Contains<Self::RuntimeCall>;
}

decl_event! {
Expand Down
2 changes: 1 addition & 1 deletion pallets/protocol-fee/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ pub trait Config: frame_system::Config + IdentityConfig {
type WeightInfo: WeightInfo;
/// Connection to the `Relayer` pallet.
/// Used to charge protocol fees to a subsidiser, if any, instead of the payer.
type Subsidiser: SubsidiserTrait<Self::AccountId>;
type Subsidiser: SubsidiserTrait<Self::AccountId, Self::RuntimeCall>;
}

decl_error! {
Expand Down
60 changes: 39 additions & 21 deletions pallets/relayer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ use frame_support::{
decl_error, decl_module, decl_storage,
dispatch::{DispatchError, DispatchResult},
ensure, fail,
traits::{Contains, GetCallMetadata},
};
use frame_system::ensure_signed;
use pallet_identity::PermissionedCallOriginData;
Expand Down Expand Up @@ -452,21 +453,6 @@ impl<T: Config> Module<T> {
Ok(())
}

fn ensure_pallet_is_subsidised(pallet: &[u8]) -> Result<Option<()>, InvalidTransaction> {
match pallet {
// These pallets are subsidised by the paying key.
b"Asset" | b"ComplianceManager" | b"CorporateAction" | b"ExternalAgents"
| b"Permissions" | b"Portfolio" | b"Settlement" | b"Statistics" | b"Sto"
| b"Balances" | b"Identity" => Ok(Some(())),
// The user key needs to pay for `remove_paying_key` call.
b"Relayer" => Ok(None),
// Reject all other pallets.
_ => fail!(InvalidTransaction::Custom(
TransactionError::PalletNotSubsidised as u8
)),
}
}

fn get_subsidy(
user_key: &T::AccountId,
fee: Balance,
Expand All @@ -483,16 +469,48 @@ impl<T: Config> Module<T> {
}
}

impl<T: Config> SubsidiserTrait<T::AccountId> for Module<T> {
impl<T: Config> Module<T>
where
<T as frame_system::Config>::RuntimeCall: GetCallMetadata,
{
fn ensure_subsidy_call(
call: &<T as frame_system::Config>::RuntimeCall,
) -> Result<bool, InvalidTransaction> {
// Check if the call is allowed to be subsidised.
if T::SubsidyCallFilter::contains(call) {
Ok(true)
} else {
let metadata = call.get_call_metadata();
if metadata.pallet_name == "Relayer" {
// The user key needs to pay for `remove_paying_key` call.
Ok(false)
} else {
Err(InvalidTransaction::Custom(
TransactionError::PalletNotSubsidised as u8,
))
}
}
}
}

impl<T: Config> SubsidiserTrait<T::AccountId, <T as frame_system::Config>::RuntimeCall>
for Module<T>
where
<T as frame_system::Config>::RuntimeCall: GetCallMetadata,
{
fn check_subsidy(
user_key: &T::AccountId,
fee: Balance,
pallet: Option<&[u8]>,
call: Option<&<T as frame_system::Config>::RuntimeCall>,
) -> Result<Option<T::AccountId>, InvalidTransaction> {
match (Self::get_subsidy(user_key, fee)?, pallet) {
(Some(s), Some(pallet)) => {
// Ensure that the current pallet can be subsidised.
Ok(Self::ensure_pallet_is_subsidised(pallet)?.map(|_| s.paying_key))
match (Self::get_subsidy(user_key, fee)?, call) {
(Some(s), Some(call)) => {
// Ensure the call can be subsidised.
if !Self::ensure_subsidy_call(call)? {
// The caller must pay for the transaction themselves.
return Ok(None);
}
Ok(Some(s.paying_key))
}
(Some(s), None) => {
// No pallet restriction applied (protocol fees).
Expand Down
55 changes: 55 additions & 0 deletions pallets/runtime/common/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,9 +303,64 @@ macro_rules! misc_pallet_impls {
type WeightInfo = polymesh_weights::pallet_external_agents::SubstrateWeight;
}

pub struct SubsidyFilter;

impl SubsidyFilter {
fn allowed_batch(calls: &[RuntimeCall]) -> bool {
// Limit batch size to 5.
if calls.len() > 5 {
return false;
}
for call in calls {
// Check if the call is allowed inside a batch.
if !Self::allowed(call, true) {
return false;
}
}
true
}

fn allowed(call: &RuntimeCall, nested: bool) -> bool {
match call {
RuntimeCall::Asset(_) => true,
RuntimeCall::ComplianceManager(_) => true,
RuntimeCall::CorporateAction(_) => true,
RuntimeCall::ExternalAgents(_) => true,
RuntimeCall::Portfolio(_) => true,
RuntimeCall::Settlement(_) => true,
RuntimeCall::Statistics(_) => true,
RuntimeCall::Sto(_) => true,
RuntimeCall::Balances(_) => true,
RuntimeCall::Identity(_) => true,
// Allow non-nested batch calls.
RuntimeCall::Utility(call) if nested == false => match call {
// Limit batch size to 5.
pallet_utility::Call::batch { calls } => {
Self::allowed_batch(&calls)
}
pallet_utility::Call::batch_all { calls } => {
Self::allowed_batch(&calls)
}
pallet_utility::Call::force_batch { calls } => {
Self::allowed_batch(&calls)
}
_ => false,
},
_ => false,
}
}
}

impl frame_support::traits::Contains<RuntimeCall> for SubsidyFilter {
fn contains(call: &RuntimeCall) -> bool {
Self::allowed(call, false)
}
}

impl pallet_relayer::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type WeightInfo = polymesh_weights::pallet_relayer::SubstrateWeight;
type SubsidyCallFilter = SubsidyFilter;
}

impl pallet_asset::Config for Runtime {
Expand Down
143 changes: 136 additions & 7 deletions pallets/runtime/tests/src/relayer_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,33 @@ type Error = pallet_relayer::Error<TestStorage>;
// Relayer Test Helper functions
// =======================================

fn call_balance_transfer(val: Balance) -> <TestStorage as frame_system::Config>::RuntimeCall {
fn call_balance_transfer(val: Balance) -> RuntimeCall {
RuntimeCall::Balances(pallet_balances::Call::transfer {
dest: MultiAddress::Id(AccountKeyring::Alice.to_account_id()),
value: val,
})
}

fn call_system_remark(size: usize) -> <TestStorage as frame_system::Config>::RuntimeCall {
fn call_system_remark(size: usize) -> RuntimeCall {
RuntimeCall::System(frame_system::Call::remark {
remark: vec![0; size],
})
}

fn call_asset_register_ticker(name: &[u8]) -> <TestStorage as frame_system::Config>::RuntimeCall {
fn call_utility_batch(calls: Vec<RuntimeCall>) -> RuntimeCall {
RuntimeCall::Utility(pallet_utility::Call::batch { calls })
}

fn call_utility_batch_all(calls: Vec<RuntimeCall>) -> RuntimeCall {
RuntimeCall::Utility(pallet_utility::Call::batch_all { calls })
}

fn call_asset_register_ticker(name: &[u8]) -> RuntimeCall {
let ticker = Ticker::from_slice_truncated(name);
RuntimeCall::Asset(pallet_asset::Call::register_unique_ticker { ticker })
}

fn call_relayer_remove_paying_key(
user_key: AccountId,
paying_key: AccountId,
) -> <TestStorage as frame_system::Config>::RuntimeCall {
fn call_relayer_remove_paying_key(user_key: AccountId, paying_key: AccountId) -> RuntimeCall {
RuntimeCall::Relayer(pallet_relayer::Call::remove_paying_key {
user_key,
paying_key,
Expand Down Expand Up @@ -95,6 +100,27 @@ fn assert_subsidy(user: User, subsidy: Option<(User, Balance)>) {
);
}

fn assert_invalid_subsidy_call(caller: &AccountId, call: &RuntimeCall) {
let len = 10;
let expected_err = TransactionValidityError::Invalid(InvalidTransaction::Custom(
TransactionError::PalletNotSubsidised as u8,
));

// test `validate`
let pre_err = ChargeTransactionPayment::from(0)
.validate(caller, call, &info_from_weight(5), len)
.map(|_| ())
.unwrap_err();
assert_eq!(pre_err, expected_err);

// test `pre_dispatch`
let pre_err = ChargeTransactionPayment::from(0)
.pre_dispatch(caller, call, &info_from_weight(5), len)
.map(|_| ())
.unwrap_err();
assert_eq!(pre_err, expected_err);
}

/// Setup a subsidy with the `payer` paying for the `user`.
#[track_caller]
fn setup_subsidy(user: User, payer: User, limit: Balance) {
Expand Down Expand Up @@ -530,6 +556,109 @@ fn do_relayer_transaction_and_protocol_fees_test() {
assert_eq!(diff_balance(), (total_fee, total_fee));
}

#[test]
fn relayer_batched_subsidy_calls_test() {
ExtBuilder::default()
.monied(true)
.transaction_fees(5, 1, 1)
.build()
.execute_with(&do_relayer_batched_subsidy_calls_test);
}
fn do_relayer_batched_subsidy_calls_test() {
let bob = User::new(AccountKeyring::Bob);
let alice = User::new(AccountKeyring::Alice);

let prev_balance = Balances::free_balance(&alice.acc());
let remaining = 2_000 * POLY;

setup_subsidy(bob, alice, remaining);

let diff_balance = || {
let curr_balance = Balances::free_balance(&alice.acc());
let curr_remaining = get_subsidy(bob).map(|s| s.remaining).unwrap();
(prev_balance - curr_balance, remaining - curr_remaining)
};

let len = 10;

// Pallet System is not subsidised.
let call = call_utility_batch(vec![call_system_remark(42)]);
assert_invalid_subsidy_call(&bob.acc(), &call);

// No charge to subsidiser balance or subsidy remaining POLYX.
assert_eq!(diff_balance(), (0, 0));

// Large batches are not allowed.
let call = call_utility_batch(vec![
call_asset_register_ticker(b"A"),
call_asset_register_ticker(b"B"),
call_asset_register_ticker(b"C"),
call_asset_register_ticker(b"D"),
call_asset_register_ticker(b"E"),
// Too many calls.
call_asset_register_ticker(b"F"),
]);
assert_invalid_subsidy_call(&bob.acc(), &call);

// No charge to subsidiser balance or subsidy remaining POLYX.
assert_eq!(diff_balance(), (0, 0));

// Nested batches are not allowed.
let call = call_utility_batch(vec![call_utility_batch(vec![call_asset_register_ticker(
b"A",
)])]);
assert_invalid_subsidy_call(&bob.acc(), &call);

// No charge to subsidiser balance or subsidy remaining POLYX.
assert_eq!(diff_balance(), (0, 0));

//
// Bob registers a some tickers with the transaction and protocol fees paid by subsidiser.
//
let call = call_utility_batch_all(vec![
call_asset_register_ticker(b"A"),
call_asset_register_ticker(b"B"),
call_asset_register_ticker(b"C"),
call_asset_register_ticker(b"D"),
call_asset_register_ticker(b"E"),
]);
let call_info = info_from_weight(100);
// 0. Calculate fees for registering an asset ticker.
let transaction_fee = TransactionPayment::compute_fee(len as u32, &call_info, 0);
assert!(transaction_fee > 0);
let protocol_fee = ProtocolFee::compute_fee(&[
ProtocolOp::AssetRegisterTicker,
ProtocolOp::AssetRegisterTicker,
ProtocolOp::AssetRegisterTicker,
ProtocolOp::AssetRegisterTicker,
ProtocolOp::AssetRegisterTicker,
]);
assert!(protocol_fee > 0);
let total_fee = transaction_fee + protocol_fee;

// 1. Call `pre_dispatch`.
let pre = ChargeTransactionPayment::from(0)
.pre_dispatch(&bob.acc(), &call, &call_info, len)
.unwrap();

// 2. Execute extrinsic.
assert_ok!(call.dispatch(bob.origin()));

// 3. Call `post_dispatch`.
assert!(ChargeTransactionPayment::post_dispatch(
Some(pre),
&call_info,
&post_info_from_weight(50),
len,
&Ok(())
)
.is_ok());

// Verify that the correct fee was deducted from alice's balance
// and Bob's subsidy's remaining POLYX.
assert_eq!(diff_balance(), (total_fee, total_fee));
}

#[test]
fn relayer_accept_cdd_and_fees_test() {
ExtBuilder::default()
Expand Down
4 changes: 2 additions & 2 deletions pallets/runtime/tests/src/staking/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,11 +367,11 @@ impl polymesh_common_utilities::transaction_payment::CddAndFeeDetails<AccountId,
}
}

impl SubsidiserTrait<AccountId> for Test {
impl SubsidiserTrait<AccountId, RuntimeCall> for Test {
fn check_subsidy(
_: &AccountId,
_: Balance,
_: Option<&[u8]>,
_: Option<&RuntimeCall>,
) -> Result<Option<AccountId>, InvalidTransaction> {
Ok(None)
}
Expand Down
Loading

0 comments on commit a1e591b

Please sign in to comment.