Skip to content
Open
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: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion pallets/proxy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,33 @@ codec = { workspace = true, features = ["max-encoded-len"] }
frame = { workspace = true, features = ["runtime"] }
scale-info = { workspace = true, features = ["derive"] }
subtensor-macros.workspace = true
frame-system.workspace = true
frame-support.workspace = true

[dev-dependencies]
pallet-balances = { default-features = true, workspace = true }
pallet-subtensor-utility = { default-features = true, workspace = true }

[features]
default = ["std"]
std = ["codec/std", "frame/std", "scale-info/std"]
std = [
"codec/std",
"frame/std",
"scale-info/std",
"frame-support/std",
"frame-system/std",
]
runtime-benchmarks = [
"frame/runtime-benchmarks",
"pallet-balances/runtime-benchmarks",
"pallet-subtensor-utility/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
]
try-runtime = [
"frame/try-runtime",
"pallet-balances/try-runtime",
"pallet-subtensor-utility/try-runtime",
"frame-support/try-runtime",
"frame-system/try-runtime",
]
23 changes: 23 additions & 0 deletions pallets/proxy/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -491,5 +491,28 @@ mod benchmarks {
Ok(())
}

#[benchmark]
fn set_real_pays_fee(p: Linear<1, { T::MaxProxies::get() - 1 }>) -> Result<(), BenchmarkError> {
add_proxies::<T>(p, None)?;
let caller: T::AccountId = whitelisted_caller();
let delegate: T::AccountId = account("target", 0, SEED);
let delegate_lookup = T::Lookup::unlookup(delegate.clone());

#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()), delegate_lookup, true);

assert!(RealPaysFee::<T>::contains_key(&caller, &delegate));
assert_last_event::<T>(
Event::RealPaysFeeSet {
real: caller,
delegate,
pays_fee: true,
}
.into(),
);

Ok(())
}

impl_benchmark_test_suite!(Proxy, crate::tests::new_test_ext(), crate::tests::Test);
}
69 changes: 69 additions & 0 deletions pallets/proxy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,44 @@ pub mod pallet {
Pays::Yes.into()
})
}

/// Set whether the real account pays transaction fees for proxy calls made by a
/// specific delegate.
///
/// The dispatch origin for this call must be _Signed_ and must be the real (delegator)
/// account that has an existing proxy relationship with the delegate.
///
/// Parameters:
/// - `delegate`: The proxy account for which to set the fee payment preference.
/// - `pays_fee`: If `true`, the real account will pay fees for proxy calls made by
/// this delegate. If `false`, the delegate pays (default behavior).
#[pallet::call_index(11)]
#[pallet::weight(T::WeightInfo::set_real_pays_fee(T::MaxProxies::get()))]
pub fn set_real_pays_fee(
origin: OriginFor<T>,
delegate: AccountIdLookupOf<T>,
pays_fee: bool,
) -> DispatchResult {
let real = ensure_signed(origin)?;
let delegate = T::Lookup::lookup(delegate)?;

// Verify proxy relationship exists
Self::find_proxy(&real, &delegate, None)?;

if pays_fee {
RealPaysFee::<T>::insert(&real, &delegate, ());
} else {
RealPaysFee::<T>::remove(&real, &delegate);
}

Self::deposit_event(Event::RealPaysFeeSet {
real,
delegate,
pays_fee,
});

Ok(())
}
}

#[pallet::event]
Expand Down Expand Up @@ -727,6 +765,12 @@ pub mod pallet {
old_deposit: BalanceOf<T>,
new_deposit: BalanceOf<T>,
},
/// The real-pays-fee setting was updated for a proxy relationship.
RealPaysFeeSet {
real: T::AccountId,
delegate: T::AccountId,
pays_fee: bool,
},
}

#[pallet::error]
Expand Down Expand Up @@ -796,6 +840,21 @@ pub mod pallet {
pub type LastCallResult<T: Config> =
StorageMap<_, Twox64Concat, T::AccountId, DispatchResult, OptionQuery>;

/// Tracks which (real, delegate) pairs have opted in to the real account paying
/// transaction fees for proxy calls made by the delegate.
/// Existence of an entry means the real account pays; absence means the delegate pays
/// (default).
#[pallet::storage]
pub type RealPaysFee<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
T::AccountId, // real
Twox64Concat,
T::AccountId, // delegate
(),
OptionQuery,
>;

#[pallet::view_functions]
impl<T: Config> Pallet<T> {
/// Check if a `RuntimeCall` is allowed for a given `ProxyType`.
Expand Down Expand Up @@ -951,6 +1010,9 @@ impl<T: Config> Pallet<T> {
if !proxies.is_empty() {
*x = Some((proxies, new_deposit))
}
// Clean up real-pays-fee flag for this specific proxy relationship
RealPaysFee::<T>::remove(delegator, &delegatee);

Self::deposit_event(Event::<T>::ProxyRemoved {
delegator: delegator.clone(),
delegatee,
Expand Down Expand Up @@ -1081,5 +1143,12 @@ impl<T: Config> Pallet<T> {
pub fn remove_all_proxy_delegates(delegator: &T::AccountId) {
let (_, old_deposit) = Proxies::<T>::take(delegator);
T::Currency::unreserve(delegator, old_deposit);
// Clean up all real-pays-fee flags for this delegator
let _ = RealPaysFee::<T>::clear_prefix(delegator, u32::MAX, None);
}

/// Check if the real account has opted in to paying fees for a specific delegate.
pub fn is_real_pays_fee(real: &T::AccountId, delegate: &T::AccountId) -> bool {
RealPaysFee::<T>::contains_key(real, delegate)
}
}
122 changes: 122 additions & 0 deletions pallets/proxy/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1251,3 +1251,125 @@ fn poke_deposit_fails_for_unsigned_origin() {
);
});
}

#[test]
fn set_real_pays_fee_works() {
new_test_ext().execute_with(|| {
// Account 1 adds account 3 as proxy
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(1),
3,
ProxyType::Any,
0
));

// Account 1 (real) enables real-pays-fee for delegate 3
assert_ok!(Proxy::set_real_pays_fee(RuntimeOrigin::signed(1), 3, true));
assert!(Proxy::is_real_pays_fee(&1, &3));
System::assert_last_event(
ProxyEvent::RealPaysFeeSet {
real: 1,
delegate: 3,
pays_fee: true,
}
.into(),
);

// Disable it
assert_ok!(Proxy::set_real_pays_fee(RuntimeOrigin::signed(1), 3, false));
assert!(!Proxy::is_real_pays_fee(&1, &3));
System::assert_last_event(
ProxyEvent::RealPaysFeeSet {
real: 1,
delegate: 3,
pays_fee: false,
}
.into(),
);
});
}

#[test]
fn set_real_pays_fee_fails_without_proxy() {
new_test_ext().execute_with(|| {
// No proxy relationship between 1 and 3
assert_noop!(
Proxy::set_real_pays_fee(RuntimeOrigin::signed(1), 3, true),
Error::<Test>::NotProxy,
);
});
}

#[test]
fn set_real_pays_fee_fails_unsigned() {
new_test_ext().execute_with(|| {
assert_noop!(
Proxy::set_real_pays_fee(RuntimeOrigin::none(), 3, true),
DispatchError::BadOrigin,
);
});
}

#[test]
fn set_real_pays_fee_fails_root() {
new_test_ext().execute_with(|| {
assert_noop!(
Proxy::set_real_pays_fee(RuntimeOrigin::root(), 3, true),
DispatchError::BadOrigin,
);
});
}

#[test]
fn real_pays_fee_cleaned_on_remove_proxy() {
new_test_ext().execute_with(|| {
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(1),
3,
ProxyType::Any,
0
));
assert_ok!(Proxy::set_real_pays_fee(RuntimeOrigin::signed(1), 3, true));
assert!(Proxy::is_real_pays_fee(&1, &3));

// Remove the proxy
assert_ok!(Proxy::remove_proxy(
RuntimeOrigin::signed(1),
3,
ProxyType::Any,
0
));

// Flag should be cleaned up
assert!(!Proxy::is_real_pays_fee(&1, &3));
});
}

#[test]
fn real_pays_fee_cleaned_on_remove_proxies() {
new_test_ext().execute_with(|| {
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(1),
2,
ProxyType::Any,
0
));
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(1),
3,
ProxyType::Any,
0
));
assert_ok!(Proxy::set_real_pays_fee(RuntimeOrigin::signed(1), 2, true));
assert_ok!(Proxy::set_real_pays_fee(RuntimeOrigin::signed(1), 3, true));
assert!(Proxy::is_real_pays_fee(&1, &2));
assert!(Proxy::is_real_pays_fee(&1, &3));

// Remove all proxies
assert_ok!(Proxy::remove_proxies(RuntimeOrigin::signed(1)));

// Both flags should be cleaned up
assert!(!Proxy::is_real_pays_fee(&1, &2));
assert!(!Proxy::is_real_pays_fee(&1, &3));
});
}
Loading
Loading