Staking: deprecating controller for proxies strategy & implementation

The idea of deprecating controllers for proxies has been around for a while, with the issue being posted 2 years ago now.

The benefits of doing this include:

  • Removing duplicated logic in the codebase (controllers do a lot of what proxies can do)
  • No longer need to call Bonded storage to get the controller/stash mapping
  • Simplify the staking pallet and further streamline it in prep for refactoring

Calls from proxies however do require an additional storage read.

From an end user perspective, less emphasis will need to be put on the stash account. Beyond the initial bond() transaction, proxies can manage the position (including unbond) alone. Where we currently try to give equal footing to stash and controller in UIs, this will no longer be necessary. The proxy will be the sole account of interest.

The main issue of not implementing proxies now (please post more if missing):

  • All nominators need a proxy, and proxies require a ~20 DOT deposit.
  • Migration strategy from controllers → proxies needs to be finalised.

Current solution of interest: Provide free proxies to nominators.

ProxyProvider

@kianenigma has posted a solution of having a ProxyProvider for the staking pallet, where a proxy implementation can be used to create / manage staking proxies.

trait ProxyProvider {
  type AccountId;
  
  // get the proxy of stash, if any. 
  fn proxy_for(stash: &Self::AccountId) -> Option<Self::AccountId>;
  fn create_proxy(...) -> Result<_, _>;
  fn remove_proxy(...) -> Result<_, _>;
}

// As a first step, staking's inner controller management system will be the `ProxyProvider`

impl ProxyProvider for pallet_staking::Pallet<_> { ... }

EligibleFreeProxy hook

Another option would be the other way round, letting proxy pallet know about the concept of free deposits, rather than the staking pallet know about proxies.

A hook can provide accounts that are eligible to bypass proxy deposits, with a function like eligible_for_staking_proxy(who) -> bool would be implemented in the staking pallet. An already bonded staker could then bypass the deposit, and wouldn’t require tight / loose coupling.

trait EligibleFreeProxy {
   type AccountId;

   fn get_eligible_staking(who: AccountId) -> bool;
   fn get_eligible_pure(who: AccountId) -> bool;
   ...
}

// and implement in staking
impl EligibleFreeProxy {
   fn get_eligible_staking(who: AccountId) -> {
      // storage read x 2
      bonded  && !hasProxy ? true : false
   }
}

In the interest of future staking refactors, I feel now is a good time to address this in our attempt to further streamline staking.

The goal of this thread would be to get validation of a solid strategy we can take forward and implement.

5 Likes

This seems like a trivial cost increase, for significantly more simple code.

Why is it a requirement? Should be a choice by the user whether they want to use a proxy or not.

The 20 DOT deposit for proxies can be adjusted, but to be honest, if you are managing a small amount of DOT, you probably do not gain much by having the controller / stash abstraction.


My two cents is that neither of these solutions are quite there, and you are over-emphasizing the need for having a free proxy versus just letting the proxy pallet handle making itself affordable to use.

I would say we should:

  • migrate all controller accounts to proxies, and do this for free as a one time deal.
  • then simply remove all references to any kind of two layer abstraction from the staking pallet, and simply let it be known that if you hold a bunch of funds, it is “best practice” to use a proxy, and you just need to pay the appropriate fees.
  • if needed, we can revisit the cost of creating a proxy
2 Likes

Why is it a requirement? Should be a choice by the user whether they want to use a proxy or not.

With the mindset that we will be maintaining the 2 layer abstraction, the proxy needs to slot into place of the controller, this is what sprouted the idea that a proxy would be required as the alternative to the controller.

If on the other hand we simply remove the controller logic altogether as suggested, then proxies become optional, and the deposit does not become a hindrance to start staking to the average user.

Just letting the proxy pallet handle making itself affordable to use

The cost of proxies is definitely the thing that is making us consider the further integrations. I agree that the simplest solution would be to not have to do them at all, if possible.

  • migrate all controller accounts to proxies, and do this for free as a one time deal.
  • then simply remove all references to any kind of two layer abstraction from the staking pallet, and simply let it be known that if you hold a bunch of funds, it is “best practice” to use a proxy, and you just need to pay the appropriate fees.

I guess this migration would need a small adjustment in the proxy pallet to support a deposit-less proxy creation, and this upgrade would need to happen before the migration takes place.

One of the best practices in pallet development is to always keep track of the deposit explicitly in storage.

For proxies, you can see the deposit amount is registered in the storage item here: substrate/lib.rs at 06a9f0a5da9681287f8a1c7b53497921238ece81 · paritytech/substrate · GitHub

The reason we do this is to ensure if the deposits ever change, we dont over or under return the deposit.

With this in mind, the pallet should be designed to perfectly handle proxies where we manually set the deposit to 0.

Cool, understood. In that case minimum changes would need to be made to proxy, from what I’ve seen we are not accounting for the manual deposit use case yet.

So if all controller logic is removed, the stash has access to all staking functions by default. If a proxy is introduced, does it make sense to disable the stash from making further calls that the proxy could do on its behalf?

Getting to an implementation strategy, i’ve got the following steps down so far leading to test network deployment

  • Finalise implementation to be made.
  • Okay changes with W3F researchers / any security implications (low probability?)
  • Source all UIs that will be affected and prepare them to deprecate controller & support proxies out of the box
    • JS Apps.
    • Staking dashboard.
    • Polkadot JS Plus extension.
    • Any wallets that support Staking

PRs:

  • Open Substrate PR and implement.
  • Update Wiki documentation with W3F.
  • Update polkadot.network/staking landing page.

Before merge:

  • Ensure UIs are prepared.
  • Roll out on Westend.

So if all controller logic is removed, the stash has access to all staking functions by default. If a proxy is introduced, does it make sense to disable the stash from making further calls that the proxy could do on its behalf?

If i read this correctly, then no, i don’t think this is the right way to look at it.

So you have the staking pallet, where all user functions should be exposed as callable through a stash account. If you directly interact with the staking pallet as a stash account, you would have no issues, and that is totally okay.

However, some of these calls are more sensitive than others. For example, requesting a payout is very harmless. Changing your session keys or even withdrawing your stake is potentially sensitive. This is why we suggest the stash account mostly stays as a cold wallet, which only comes out once in a while for big tasks.

To create a “hot” wallet, you would create a second account, which has proxy access to the stash, but the proxy only limits you to the “safe” functions in staking.

Then you can call into the staking pallet’s safe functions using: controller → proxy(controller to stash) → staking (as stash)

And this would only work for a certain set of functions we want.

So from the perspective of the staking pallet, there is just one account, and it knows nothing better in the entire pallet logic.

Then from the proxy perspective, we simply create a new proxy type which allows us to call the functions we would want a controller to be able to call.

1 Like

As for steps, I think this makes sense in my head:

  • create the new proxy type for staking controller, this can be merged on its own, super simple
  • write a migration which gives every controller proxy access to the stash via this new proxy, can also be done without change any existing pallets, now controllers will have two ways to access stash
  • notify the ecosystem to switch to use proxy instead of controller functions to access staking
  • then remove all controller accounts from the staking pallet, while keeping all the logic there still, see if there is anyone who really breaks from this
  • then remove all the dead code from the staking pallet

Agree with the general feedback from @shawntabrizi, I think both of the initial ideas are a bit over-complicated. That being said, given that the wallet integrations of proxy accounts is not perfect yet, we could have a transition period where all of 1. bond(controller, 10) 2. bond(stash, 10) and
3. proxy(controller, bond(10)) are acceptable. This was more or less what I had in mind by creating a ProxyProvider. So, we’d have something like:

fn bond(origin, amount: _) {
  // if signed by controller directly 
  T::ProxyProvider::is_proxy(origin);
  // or if signed by stash directly. 
  let stash = ensure_signed(origin);
}

My only minor disagreement is that the new Controller proxy type is also most likely an overkill. All controller should just move to be Staking proxy. The differences between the two are negligible and I have nowhere seen that it was an intentional choice. I think the real solution here is configurable proxies, as discussed in Proxy Pallet On Steriods.


All that said, the first action item ,before we touch any Rust code, is to ensure that in the staking dashboard (and PJS apps, and other wallets that provide staking experience) using a proxy account is seamless and supported. Afterwards, we can proceed as Shawn explained.

2 Likes