sRFC 00009: Sign-In with Programmable (Smart) Wallets using Off-Chain Delegates

Summary

This SRFC proposes an application standard for authenticating users of dapps, specifically those using Programmable Wallets (a.k.a Smart Wallets or Smart Contract Wallets), through the use of off-chain delegates. This standard addresses the challenge of authenticating users who interact with an app using a Programmable Wallet, which does not have a private key for signing messages. By enabling Programmable Wallets to appoint a delegate with a private key to sign messages on their behalf, a secure and verifiable way to authenticate users is achieved.

Introduction

A lot of dapps use the authentication process called off-chain message signing. The mechanism takes advantage of the fact that user keypair can be used to cryptographically sign any arbitrary messages such that anyone can verify that a message is signed by the party controlling the private key for the corresponding public key. The algorithm for this process is as follows:

  • The app produces a unique message and gives it to the user (wallet) to sign.
  • The user signs it and returns the signature back to the app.
  • The app verifies that the signature is indeed valid and is for the message it provided originally.
  • If true, the app issues an auth token that the client can use for future interactions with the app’s API.

The Problem of Programmable Wallets

Not every Solana address has a corresponding private key. A good example is Programmable Wallets - accounts owned by a Solana program that is typically controlled by one or many regular (keypair) wallets and interacts with the chain following the rules of the program.

For example, a multisig program owns a vault account shared by multiple owners of the multisig, tracks the status of voting on proposals, and allows executing the proposals once they are approved by the quorum. The vault account has a public key but lacks the private key; the program programmatically “signs” on its behalf.

How can we authenticate a user who uses a Programmable Wallet address to interact with a dapp?

Proposed Solution

Introducing Off-chain Delegates.

So Programmable Wallet accounts cannot sign a message off-chain as they lack a private key. However, they can appoint a delegate - a regular account that has a private key - authorized to sign messages on behalf of the Programmable Wallet account. This delegation can be fully registered on-chain, making it verifiable by anyone.

Let’s take a look at how the sign-in flow would work when modified to support Programmable Wallets:

  • The app produces a unique message and gives it to the user (Programmable Wallet) to sign.
  • The wallet knows which address controls the Programmable Wallet account and signs the message with the delegate keypair. It then returns the signature along with information about the Programmable Wallet account and delegate addresses to the app.
  • The app verifies that the signature is produced by the delegate.
  • The app verifies that there’s a delegation record (DelegateToken account) for the given delegate created by the account on-chain and that it has not expired.
  • If true, the app issues an auth token that the client can use for future interactions with the app’s API.

Implementation on the Programmable Wallet programs’ side can vary between Programmable Wallets. Each program can decide and implement the mechanism of creating delegate records in whatever way they want. They just need to make a CPI (cross-program invocation) into the Off-chain Delegate Program that manages those records and sign that with the PDA seeds of the Programmable Wallet account they control.

Drawbacks

This mechanism introduces a slight overhead compared to off-chain signing for regular wallets. The overhead is the DelegateToken account that must be created and stored on-chain for each delegate. The current reference implementation, Off-chain Delegate Program, uses 77 bytes, which is about 0.0014268 SOL per account. The rent can be reclaimed when the delegate is removed, but it’s still worth highlighting.

Another problem is additional logic that needs to be implemented on the dapp side. Unfortunately, I don’t see how the mechanism can be implemented entirely in the wallet apps/extensions without explicit support from the dapps. If someone has ideas, please feel free to comment.

Conclusion

The proposed SRFC standardizes the process of off-chain authentication for users with Programmable Wallets through the use of off-chain delegates. Although it introduces some overhead due to the on-chain storage of DelegateToken accounts, this mechanism provides a secure and verifiable way to authenticate users interacting with an app using a Programmable Wallet.

Implementation: Off-chain Delegate Program

Here’s a flow diagram that illustrates the proposed process of authentication.

mentioned this on twitter but i’ll paste my comment here for proper discussion:

What if instead of having a DelegateToken account, there was a standard by which a key could be checked for membership (as a controller, or possibly for a particular role) of a programmable wallet?

This still wouldn’t solve the problem on the dapp side, as a check would still have to be issued, but having a programmable wallet membership verification mechanism could have a broader set of applications aside from delegated login.

@silo Thanks for your comment. Do you have any specific application in mind. Just curious whether DelegateToken isn’t able to cover that.

The thing is, DelegateToken is such a simple concept that it can be used in many cases. It might make sense to change the name of the account and the program if it isn’t universal enough to fit the use-cases, but in order to do so, we need to understand what those use-cases are.

Sure. So in this proposal, the programmable wallet is asking a member wallet to act as a delegate on behalf of the pw. It knows who the delegates are and provides a delegate to the dapp when requested. And it needs to do this every time a delegate is used. I’m saying maybe we can flip that, and basically have a “registry” where anyone can look up the member wallets of pw. Now the dapp can easily verify membership without having to make the request to the pw every time. This also allows dapps to automatically bootstrap member wallets and link them to a pw account and they can observe changes over time and use that information accordingly vs making the request every time and essentially using a “snapshot” of data (just the single delegate at that instant). Could even use a reverse-lookup sort of mechanism, whereby given a member wallet, the pw information can be looked up.

First of all, with the current design it is still possible to fetch all the Programmable Wallets (PWs) where the user is a delegate of, it’s a relatively simple getProgramAddresses call with filtering by the delegate field.

vs making the request every time and essentially using a “snapshot” of data

In my view, this is a feature actually. As a dapp, you want to check the current snapshot always, because PWs might remove a delegate (e.g. a compromised key) at any moment.

allows dapps to automatically bootstrap member wallets and link them to a pw account

But what if a user account has multiple PWs associated with it. How would the dapp choose which one to link to? How I see the typical use case is that your wallet should tell the dapp which address to use (cuz that’s your interface with the web3 world), hence the PW → Delegate relationship rather than the other way around.

Maybe this is because I’m having s specific scenario in mind (signing in with a multisig wallet) but I’m having difficulty with understanding why the reverse relationship (Delegate → PW) would be easier to use.

Gotcha. So a registry would never have stale data. It would always reflect current membership. The reason for having a reverse lookup (or registry), would be so that the dapp could have a “unified view” of the client. While a user might be using a specific delegate (member wallet) to access a dapp, the dapp may use information in the other wallets. For example, think of a something that has token-gated access. I might have a specific NFT that grants me access to something sitting safely in my hardware wallet, while I use my mobile wallet as a delegate to gain access. (This is actually the specific use case that we have with our gaming NFTs. Kinda like a delegated-login-with-NFT.)

The fact that the member wallet might have multiple PWs associated with it is a good point. For us, we actually don’t allow it and instead use a domain concept (a member can only be linked to a singe PW within a domain).

Thank you for sharing your viewpoint. I now completely understand your perspective. It seems that the use-case you mentioned is already being addressed through “Wallet Linking” by several existing dApps, such as Tensor, Magic Eden, and Dialect.

While I agree that this use-case might warrant its own standard, I suggest separating it from the current proposal. This is because, firstly, it doesn’t necessarily require support from the Wallet Standard side, as dApps can independently maintain a list of “Linked Wallets.” Secondly, this feature is already functional for many dApps, in contrast to the present proposal which aims to address a need that is not yet fulfilled by any dApp due to the absence of a standardized approach.

Yea, if this is just for a delegated sign-in mechanism then a separate standard would make sense.

Right now the wallet linking most dapps use isn’t done on-chain and usually requires the wallets to be in the same browser as normal browser sessions are used for the linking. So there’s no way to link, say, a mobile wallet to an existing desktop wallet.

Hey @vovacodes I’m wondering what you might think of the work we’re doing on sRFC 00007 and how that approach might compare to what you’ve suggested here.

Take a look at my response here and let me know what you think of the spec I added.

In short, I think we have some overlapping approaches to delegating signatures. In my spec for sRFC 00007, I’m obviously targeting encryption keys, but we might be able to accomplish something very similar for delegated wallets using the same functionality but adapted for on-chain registered delegation.

The idea would simply be to use this protocol spec (possibly even the same program?) to store on-chain the delegate keypair so anyone can verify.

I suggest leveraging the same program only because with your particular use case(s), we need only store a Solana address in the PDA, rather than the complex data layout I’ve designed for the Keying program. However, since our use cases are somewhat related (verifying some key/keypair is in fact owned by you and authorized by you), it might make sense to share the same program namespace.

Let me know!

1 Like

@joec, I believe your program addresses this use-case perfectly. I suppose we can use a unique discriminator for the “message signing delegates” and use additional configuration section for adding parameters like expiration, etc

Actually I’m curious how important you think the concept of “expiration” is? One potential issue is that’s really only an “honest man’s config”. What I mean is, sure the on-chain program can specify that they key is expired, but anyone querying that information must make the decision at their implementation (client) level to honor the expiration date or ignore it

Yeah it’s a fair concern. A pro-expiration argument would be that the proposed process already relies upon the “decision at their implementation (client) level to honor” the account stored on-chain, so expanding it slightly to include expiration doesn’t change the trust model much but adds a bit more flexibility.

I would personally like to hear what other folks in the ecosystem think about it, especially the app developers. We could also start without expiration and see if we really need it.