Hybrid Custody & Restrictions on Linked Accounts

Context

With the introduction of account linking on mainnet, several concerns have been raised by the community around shared access to accounts with non-custodial wallets. While the currently proposed linked account standard technically enables Hybrid Custody by enabling and maintaining shared access on accounts, the proposal does not take into account that regulatory obligations are vastly different between users and custodial parties.

The crux of the issue this proposal seeks to address is the risk exposure custodial parties incur by sharing unrestricted access with secondary parties.

The question then is, how do we enable users to have real ownership over their hybrid custody accounts while empowering builders to prevent regulatory obligations that may come with that shared access?

Given the importance of a Hybrid Custody solution that works for everyone, we’ve developed an initial technical implementation that we’re opening up for consideration and feedback early in the development loop.

Problem

First, what’s the problem? Simply, the issue is that many projects have gone through great pains to prevent access to assets (e.g. FungibleTokens) in app-custodied accounts. Opening up unrestricted access to users by linking their accounts allows for another party to circumvent those preventative measures, potentially exposing app developers to the liabilities involved with custody of those assets in linked app accounts. For developers to adopt Hybrid Custody, the ideal approach is to provide a solution which avoids exposure to any regulatory risk at all or, at minimum, empowers developers to build solutions that obviate those risks themselves.

Proposed Solution

Overview

We’re suggesting a technical solution that empowers dapps to maintain self-defined restrictions on their access to linked accounts. This allows dapps to continue interacting with linked app accounts on behalf of a user while eliminating the possibility that they will inadvertently have access to undesirable assets so long as those restrictions are defined where necessary. Again this is a potential technical solution early in development, and there’s plenty of flexibility in the level of restriction - you might adopt this and inadvertently allow Capabilities that still leave you open to regulatory risk if you don’t properly restrict them, so how you implement that restriction is still for you to discern.

TL;DR: Enable dapp developers to restrict their own access on linked accounts

High-Level Design

On the left, you can see that the user has unrestricted access to a linked child account. This link is enabled by an AuthAccount Capability wrapped and stored in the parent account (more details can be found in FLIP#72). Wallet providers would be able to query for a user’s linked accounts as well as the contents of those accounts, and dapps like marketplaces can build seamless experiences such as NFT listings, sales, etc. leveraging those linked accounts.

On the other side, you see the dapp’s backend account maintains access on that child account as well. However, the dapp’s access on that account is restricted. This restriction is self-imposed at the time of account linking. Also of note is that the dapp does not have key access on the child account. To categorically avoid any risk exposure, it’s critical that the dapp must revoke their key on the child account in the same transaction in which they link the parent account if they custodied a key on the account to begin with.

Low-Level Design

As noted above, there will now be two sides to an account link - the user-managed and dapp-managed accounts. We’ll focus on each side of the link before putting them together.

Let’s not dive too deep here since this side of the link is already covered in FLIP#72 with implementation in this PR.

Essentially, the unrestricted AuthAccount Capability on the child account is wrapped in an NFT and placed in the user’s LinkedAccounts.Collection. The NFT also maintains a private Capability on the child account’s Handler resource which identifies the child account, its parent, and resolves metadata about the purpose of the account.

On the dapp side of things, we have the restricted AuthAccount Capability. This restricted Capability is wrapped in an AccessPoint along with allowed Capability types, corresponding paths, and a dapp developer-defined CapabilityValidator. This validator ensures that any requests for Capabilities from the child account via the AccessPoint only ever return types that the developer intended to retrieve.

Why are the type guarantees granted by a validator important? Well, if a user were to alter the underlying Capability at a path to say a FungibleToken.Vault, that would be a problem for a developer attempting to ensure their FungibleToken access was restricted. Since the validator is developer-defined, it enables a) a generic solution and b) customization based on use-case, jurisdiction, and risk tolerances.

The dapp managed account (shown above as custodied by backend KMS) stores an Accessor on the child account’s AccessPoint, providing an easy way to retrieve allowed Capabilities from the child account.

As mentioned previously, if the dapp originally custodied a key on the child account, they should ensure that key is revoked in the same transaction as when parent-child account linking occurs. Otherwise, the restrictions pictured above are useless.

An additional note about the developer-defined CapabilityValidator - any dapp utilizing a solution where the dapp controls the restriction contract (in this case the CapabilityValidator implementation) and retains the ability to make updates to it in the future should independently evaluate whether any legal and regulatory issues will arise from the retention of such ability.

Putting it all together, the user has a Collection enabling the one-to-many unrestricted access on linked accounts while the dapp has a single accessor enabling restricted access on a single child account.

In the end, the idea is that this will enable users to have real ownership over their Hybrid Custody accounts, and developers have a mechanism to ensure their control and ownership over those accounts is limited and in compliance with their local regulations. All this, of course, while still facilitating the seamless UX we’re all after.

Process

Let’s take a look at this amended account linking & capability retrieval process in practice:

  1. CapabilityValidator Definition & Deployment - Before enabling account linking, the developer would have defined their own CapabilityValidator implementation and deployed the defining contract. This definition enforces the desired restrictions that will be placed on the dapp’s access to any future linked child accounts.

  2. Account Linking - Providing the user unrestricted access to the child account while setting restrictions on the dapp’s access

    1. Create an AuthAccount Capability for both the parent & dapp accounts
    2. Link the app-created & custodied account to the user’s main account, configuring a LinkedAccounts.Collection if needed
    3. Revoke the app-custodied key on the now user-linked child account
    4. Create an AccessPoint in the child account, passing the child account AuthAccount Capability & previously defined CapabilityValidator, before linking a public & private AccessPoint Capabilities
    5. Publish the private AccessPoint Capability for the dapp’s backend account to claim
  3. Claim Access Point Capability - Claiming the restricted access provided in the previous transaction

    1. Claim the published AccessPoint Capability to maintain the dapp’s restricted access on the child account
    2. Wrap the claimed Capability in an Accessor and saving the resource in storage
  4. Retrieve Allowed Capabilities on the Child Account - Getting a Capability required by the dapp act on behalf of the user with guarantees on the type of Capability retrieved

    1. Reference the Accessor in storage, and retrieve a reference to the child account’s AccessPoint
    2. Call for the requested Capability
      1. If the Capability is not allowed or the CapabilityValidator in the child account’s AccessPoint determines the Capability Type at the corresponding path is not the Type intended, nothing is returned
      2. Otherwise, the AccessPoint successfully returns the requested Capability to the calling dapp account

Resources & Example

Take a look at this branch for an example implementation of the constructs in this diagram. Of note are the following:

As always, all feedback is welcome and appreciated. Thanks all!

2 Likes

High-level notes from my first read through (I’m sure I will need to look again)

  • This proposal adds an intermediary account which is the child of two accounts (a non-custodial wallet, and an application’s wallet)
  • This setup is mostly around how to enforce what the application wallet can access on the child account. The other account (non-custodial one) is unchanged.
  • Apps store the rules of restrictions they need on the child account
  • Apps need their own implementation of these rules because they might differ by jurisdiction
  • The non-custodial account gets full access to the child

Is all of the above correct? If so, really great start overall, I think there are a lot of moving parts, so apologies if I’ve missed the mark here a bit and need to be corrected. A few questions/points to get started:

  1. Isn’t this approach risky for the app wallet? If I store the rules for what I’m permitted to retrieve on the child, then I can’t be sure that no one else who has access to that account will alter those rules and cause me to now break whatever restrictions I have to follow. It seems to me that apps would need to store those rules on their own account, and give a capability to it (or some other means of sharing it) to the AccessPoint resource.

  2. It looks like you’re following the patterns used in ScopedNFTProvider and ScopedFTProvider resources for checking the validity of an action. In this case, whether a capability can be obtained or not. Some considerations we ran into that that you might want to think about as well, I don’t think they’re necessarily applicable and can probably be offloaded on the implementation itself:

    • Should the validator type and its details be discoverable? I don’t see a way to do this currently, that might be something that folks want to be able to see for display purposes and for validation on the app side that nothing has been tampered with on the child account
    • Guaranteed callback methods. We have one for when an allowance is taken on the FT flavor, for instance. Might not be as relevant but I figured it was worth surfacing. Depends on what access patterns are being encouraged.
  3. Do we need this intermediary account? Would it make more sense to instead share subsections of an account to the non-custodial wallet, and then wrap a save method on the ScopedAccount resource to only let you save certain types of objects. For instance, you are not allowed to save anything of type FungibleToken.Vault, or you cannot link or unlink things, only retrieve capabilities.

  4. How would this work with Dapper Wallet and its issues with withdraw limits? It seems like we’d be missing some other form of controls that they’ll need to granularly tell if an nft is past its holding period, or if it’s even able to be withdrawn at all. If we are instead splitting up what nfts are in the app wallet and which are in the child, we should probably call that out and think through how apps are even expected to manage that, it would be a heavier lift than I’m anticipating they’ll be willing to do.

2 Likes

I echo austin’s concerns, especially those in question 1.

Some more initial thoughts:

  1. It seems like it’d be useful to impose general restrictions on the child account. i.e., the dapp could also define a CapabilityValidator for what capabilities the user can access in the child account. This would ensure that the account never interacts with restricted functionality.
    • Semantically, this child account would represent an agreement between the user and the dapp, and it would make sense that they share limited control of it. In the current implementation I don’t really see the point of an intermediary account either.
  2. The CapabilityValidator interface seems useful when you have a good idea of the specific contracts you want/don’t want your child accounts to interact with. But it seems hard to make it generic enough for many use cases. For example, if is it possible to create a general-purpose child account that can do anything, but can’t hold any fungible tokens?
    • Would love to see more examples of what’s possible here, or alternative solutions that could allow this sort of scenario. austin’s idea of wrapping a save method in validators sounds promising.
2 Likes

Thank you both for your thoughtful feedback. I’ll respond to each point-by-point, but want to make sure I understand the concerns voiced so far:

  • Given the shared access on a child account with an unrestricted party, can’t they just alter the restrictions set by the app?
  • Displaying the level of access an app has, primarily for the app but secondarily for the user sharing access
  • Potential redundancy of the intermediary account which I interpret to reference child account - please correct me if I misunderstood.
  • Potential incompatibility of this proposal with Dapper’s withdrawal limits
  • Lack of generalizability with the CapabilitiyValidator's requirement to statically enumerate desired types

@austin your high-level notes are all on point.

  1. Re: Risk of altered restrictions
    With regard to the risk associated with storing the AccessPoint in the child account, the dapp can be assured that their restrictions won’t change due to the encapsulation of the contained CapabilityValidator. Taking a look at the interface below, you can see that there is not a way to change the restrictions given to the AccessPoint on initialization. The worst someone could do is break the app account’s access to the AccessPoint by either unlinking the Capability or altering either the Capabilities or underlying Capability types at pre-defined paths. The contract defining the app-specific CapabilityValidator implementation would be deployed to some other developer-associated account, meaning only the developer would be able to alter the CapabilityValidator definitions contained in the AccessPoint.
pub resource AccessPoint : AccessPointPublic {
        access(self) let id: UInt64
        access(self) let authAccountCapability: Capability<&AuthAccount>
        access(self) let allowedCapabilities: {Type: CapabilityPath}
        access(self) let validator: AnyStruct{CapabilityValidator}

        pub fun getID(): UInt64
        pub fun getAllowedCapabilities(): {Type: CapabilityPath}
        pub fun getScopedAccountAddress(): Address
        pub fun getCapabilityByType(_ type: Type): Capability?
        access(self) fun borrowAuthAccount(): &AuthAccount
    }

  1. Re: Discoverability
    We can definitely have AccessPoint implement MetadataViews.Resolver and resolve a sort of view containing the allowedCapabilities. My concern here is wallets presenting that view as a source of truth on the complete permissions an app has on their child account. So long as the app had access to the account before the user linked their account, the user should consider that a secondary party has full access regardless of what the scope defined in the resolved view and consider anything inside it as shared access. There’s ultimately no way to limit this fact for walletless onboarding IMHO.
    Re: callbacks
    Curious where y’all think this would be useful. It’s an interesting pattern for sure. Come to think of it maybe the CapabilityValidator fits that pattern with its validate() method, though it’s not mutating anything like the referenced AllowanceFilter.

  2. Re: Intermediary account & User-side restrictions on accounts (Also covers @artur point 1/)
    You’re right that if we adopt restrictions on user accounts, the intermediate account is technically unnecessary.
    I touched on this a bit under discoverability - I think attempting to restrict the user’s access on the child account leads to significantly more work on the part of wallet providers, and could potentially fragment UX.
    We ran into this when thinking through the basis for Hybrid Custody - by relying on Capabilities to delegate access to users, we’re putting the burden of implementation on the contract devs to properly define and parse out the Capabilities for both hybrid custody and standard custody experiences, and additionally asking wallet providers to properly represent the limited access a user has. @dete might have something to add here as well.
    Unrestricted user access is much clearer to all parties - a user has shared access on this linked account, anything in there incurs custodial risk, and they can access anything inventoried in that account. Considering restricted user access from the marketplace perspective when iterating over a user’s linked accounts, they’ll not only need to know the inventory of those accounts, but also whether the user has the appropriate permissions to access those NFTs, etc.
    The absolute case - unrestricted access feels much cleaner and truer to the real ownership we’re after with Hybrid Custody
    Re: Gating save() on type
    On preventing the user from saving certain types, there’s really no way to guarantee this. Consider this case - I could define my own VaultWrapper resource, wrap my Vault in the resource, then save it in the child account. That would avoid any FT related event emissions and subvert a filter like thingToStore.isSubtype(of: Type<@FungibleToken.Vault>()) , thus making the restriction on the parent account useless or at least incomplete. Given that fact, dapp developers will still need to restrict their access to these sorts of edge cases, which this proposal attempts to address.
    Additionally, by restricting the AuthAcccount Capability & gating save() the user wouldn’t be able to link Capabilities on types they store due to the static typing requirements when linking. That’s part of the reason why the AccessPoint is limited to retrieval from existing paths and doesn’t enable storing or linking new resources or Capabilities.

  3. On this point I think @rrrkren is the person to ask - any insights here? My initial thoughts are that Dapper could define its own CapabilityValidators but I’m not aware of the full context here.

@artur

  1. Thanks for expanding on this concern. See 3/ above as I tried to cover it with Austin’s point.
  2. For sure! A validator could do something like allow any Capability except FungibleToken types to be retrieved, though big caveat that this could be bypassed by wrapping a Vault in some containing resource, so enumerating allowed types is significantly more precise in this construction given the need to statically declare borrowed types.
pub struct NotFungibleToken : CapabilityValidator {
  pub fun validate(expectedType: Type, capability: Capability): Bool {
    // Return false if Capability links Vault, Provider, or Receiver
    if let vaultRef = capability.borrow<&FungibleToken.Vault>() {
      return false
    }
    if let providerRef = capability.borrow<&{FungibleToken.Provider}>() {
      return false
    }
    if let receiverRef = capability.borrow<&{FungibleToken.Receiver}>() {
      return false
    }
    return expectedType != Type<@FungibleToken.Vault>() ||
      expectedType != Type<@{FungibleToken.Provider}>() ||
      expectedType != Type<@{FungibleToken.Receiver}>()
  }
}

Also of note, this validator doesn’t confirm that the given Capability links expectedType, but rather that it doesn’t link to a FungibleToken interface implementation. I’ve struggled with generalizing implementations due to the need to statically declare types in Cadence.

Ultimately, the Hybrid Custody we’re after gives the user real ownership of the assets in their linked accounts and I think that should mean they have unrestricted access to those accounts and the things inside of them. That said, HC doesn’t have to be the solution for everyone. It sounds like there are concerns around broadly giving users full access to linked accounts, but I think the alternative - while my perspective might be a bit black-and-white - is rebuilding walled gardens under a new custodial paradigm mediated by wrapped AuthAccount Capabilities. IMHO, giving users restricted access significantly complicates dealing with those linked accounts given the number of various stakeholders involved in the implementation of Hybrid Custody.

Hope these points helped clarified my thinking, and am interested to know if the concerns remain and if there are alternative models you all think we should consider.

3 Likes

What are the overall goals with Hybrid Custody? For us, the key question here is, does this really hold up to the goals the app developer is trying to accomplish? For them, they want to be able to:

  1. Allow someone to onboard without a wallet, in a seamless way. Yes.

  2. Allow them to link a parent account to take content off platform. Yes.

  3. Limit my risk on the child account once it’s linked. TBD(?).

  4. Allow the user to keep using my app with their child account seamlessly (no signing, etc). TBD(?)

  5. Do all of this with as little burden on the app developer as possible. No

On 3 - Based on my read of everything above, this child can now pretty much be used for anything. However, the app can only do certain things. Are we just saying that since the app has limited access to the child, it’s okay? Will that hold up in a regulatory way if the child now has currency on it? That’s the key question in terms of limiting any risk - I can see the argument here.

On 4 - Can’t I link or unlink capabilities as the parent and now this child account essentially can’t be used by the app? Or more so, revoke the app’s keys? As an app developer, this becomes a pretty painful scenario to keep track of, since I won’t really realize it happened.

On 5 - A critical part of account linking (as I understand it) was to get this unlock with very little burden on the app itself. That is, you can link your account to a blocto wallet and then they can just go to flowty/gaia/etc and it’s all good to go. This current approach puts a lot of the burden on the app - I can’t imagine many apps wanting to do all the work to properly control for all of this. I can already imagine someone linking, going to a third party app that has them do something unexpected, and then complaining to the app developer that their account isn’t working as expected, things are missing, etc.

Broadly, I get what you’re thinking - I like the perspective of opening this account up to be used more broadly as a general purpose account and less like a walled garden. But, we should think about how to balance that with developer friendliness as well.

1 Like

I see your points @anir-niftory, and we definitely want this to be simple enough for app developers to implement. Of course, as developers, there will need to be some weight pulled on the backend to abstract complexities on the user end. There’s only so much that can be done at the language and contract standard layers, and, while we want to provide primitives that are as useful as possible to as many builders as possible, we want to strike a balance between ease of implementation for app developers, enhanced user ownership, and ease of implementation for wallet providers.

In terms of the open questions you listed (skipping 1/ and 2/ since those are settled):

  1. Limit my risk on the child account once it’s linked
    While we aren’t lawyers and can’t give legal advice, our understanding is that the primary question here is whether an app has control of an account with access to funds. Under the proposal in this post, an app restricting its access to certain types, so long as those types are not deemed as financial instruments, would satisfy the primary question as the app would not have control of said account and would not have access to those funds even though they reside in the same account. Again, big caveat that regulatory obligations vary over time and jurisdiction, so developers should seek independent legal counsel.
  2. Allow the user to keep using my app with their child account seamlessly (no signing, etc).
    Under the proposed construction, the app could continue to act on behalf of the user via Capabilities. See this example transaction retrieving a Capability from an AccessPoint. Also, this proposal recommends revoking the app’s key on the child account upon linking so its access is truly restricted. I realize this may be where misunderstandings about the need for an additional account come in - the app needs an account in which to store an AccessPoint Capability on the child account restricting its access since it can no longer maintain a key on the account.
    The question I have is whether this is manageable. What difficulties do you see here that lead to your doubt on this point?
    I also acknowledge that sharing unrestricted access with a user creates some complexity here in the event that the user, or another app for that matter, alters the linked account’s storage in a way the app doesn’t expect. And your point that the burden of responsibility, in that case, could unfairly fall back on the app is valid.
    That said, I don’t think this possibility is anything new. It’s not clear to me that the answer to this is preventing access on the child account considering the cost is the loss of real ownership we’re after with Hybrid Custody. After all, can’t I sign a transaction that would modify Capability paths and reconfigure resources in unexpected ways today? IMHO this parallels the case you’re referring to, except it’s on my singular account as opposed to my linked account.
    If you have specific examples about how this concern is unique to Hybrid Custody, I’m interested in hearing them.
  3. Do all of this with as little burden on the app developer as possible
    I would say this is a goal, though balanced with the burden placed on other stakeholders (i.e. wallet providers, marketplaces, and others that leverage a user’s linked accounts) as well as the ecosystem-wide UX unlocked by whatever solution we arrive at. I’d hate for it to be easy to implement by apps, but unusable to facilitate a viable wallet experience, for example.

The way I see it, there are a number of paths forward at a high level. The ultimate solution will be a a matter of determining which balances the interest of all stakeholders best, and all of these will involve negotiations along the axes of ecosystem-wide UX, burden on wallet providers vs app devs, and user ownership.

  • Give both the user and app unrestricted access
    This is what was proposed up until now, and is unworkable as it exposes developers to too much risk.
  • Give the user unrestricted access and restrict app access
    This is addressed in the original post, so I’ll leave this one be.
  • Restrict both user and app access
    Also proposed by @artur, I have trouble understanding how dual these restrictions aren’t redundant. IMO we might as well just give the user restricted access on an app-controlled account, as @austin pointed out above.
  • Restrict user access and give the app unrestricted access
    From my understanding, this is the setup it seems you all are proposing and is effectively reversing the relationship in the first bullet point.

This leaves us with bullets 2/ and 4/. I can definitely see your point on how 4/ would be easier to manage from the app’s perspective.

Before I list my concerns with restricting user access, we should collectively clarify the product requirements we’re working to satisfy with our Hybrid Custody implementation.

  • Grants real ownership to users → assets inside accounts are accessible to users via main account
  • Access to linked accounts is portable → Implies a generalized implementation
  • Unlocks composability on app assets → follows from the first & second points
  • Easily interpretable by both apps and wallets
  • Must empower compliance w/ regulatory obligations → implies guaranteed restrictions on shared accounts by either party

Bearing those in mind, I’ll point to the original motivation for Hybrid Custody - to make onboarding easier while providing users paths to real ownership, unlocking large scale composability, and mobile applications on Flow. It’s a game-changing vision, but the ultimate solution will not be a fit for every use case. That openness is not without its risks, but the belief is that building permissionlessly is worth it for a lot more applications than can currently capitalize on it because of the barriers to user onboarding.

With that said, here are my concerns with user access restrictions:

  1. Restricting accounts prevents real ownership & composability
    Compromises 1 & 3
    This is one of the primary motivators for Hybrid Custody to begin with. Limiting access severely limits composability and prevents one of the biggest advantages of chain-powered applications - ownership. Hybrid Custody as a term needs to mean something for it to be a differentiator for Flow and applications on it. If it means a user might have some degree of “ownership”, it means nothing. It’s like seeing “natural” on a food label…what does that mean, exactly? In reality, it has no substance.
  2. Accurately representing user’s access on linked accounts
    Compromises 4
    Capabilities vary in shape and scope by use case and implementation. The current landscape implies most allowed Capabilities will be NFT-related, with Capability implementations that are largely similar. However, future use cases might differ heavily - gaming is one example. Even among NFT-related Capabilities, small details can drastically impact core functionality - i.e. panic on withdraw() prevents withdrawal, allow listing deposit() on NFT.id limits public deposits, etc.
    Given that we want users to easily identify their linked accounts and their access on them, restricting their access on those accounts makes the task of providing that overview significantly gnarlier. Tasking a wallet provider with parsing out all of the details is a much bigger lift than the app devs simply providing controlled accounts with the Capabilities specific to their use case and which they likely defined.

To drive this home, I’ll provide the following example:

  • App A provides user unrestricted access
  • App B provides user NonFungibleToken.Receiver for Florider Collection
  • App C provides user NonFungibleToken.Receiver & Provider for Florider Collection

Consider how the user’s wallet is supposed to reasonably display the user’s access to their inventory. Moreover, consider how a marketplace is supposed to facilitate an NFT sale from App B given withdrawals aren’t allowed. Now consider this over an arbitrary number linked accounts. To me, this world feels incredibly fragmented as a user.

Now the alternative:

  • App A, App B, App C all provide user unrestricted access

Now consider how much simpler it is for a wallet provider to showcase their inventory and facilitate transfers between linked accounts - they’re all fair game. Additionally, a marketplace can be assured that anything in each account is accessible per each Collection’s NonFungibleToken implementation. The lift on the app side then is giving themselves the Capabilities the first example would’ve given the user, which seems like a significantly smaller lift. I can appreciate it’s not insignificant but the alternative feels unworkable not to mention a non-starter given the “real ownership” product requirement.

Thanks so much for engaging through all of this, I know it’s lengthy and dense. I hope this doesn’t read as too defensive as I’m really just trying to suss out the best path forward among everyone here. Your feedback and concerns are incredibly valuable and appreciated.

Lots to reply to! Thanks @gio_on_flow for putting so much effort into your answers, it’s very appreciated.

With regard to the risk associated with storing the AccessPoint in the child account, the dapp can be assured that their restrictions won’t change due to the encapsulation of the contained CapabilityValidator

Could a malicious parent fully replace the AccessPoint resource with modifications as they please? Apps could protect against it by recording the original’s uuid, but I’m just trying to get a sense of the attack vectors in play with the approach being proposed. I’ll make sure to read the contract(s) again (and probably a few more times after that) just to get my head around this more completely.

Re: Discoverability

Totally hear you on these points. I think that metadata views are always good to have and we already have to live with the fact that they aren’t really verifiable. A collection could easily mock the NFTCollectionData view, for instance, to make themselves look like some other collection depending on how marketplaces pick them up.

Consumers of views always have to proceed with some caution about what they’re given here, but at the same time if an app is controlling the creation of this resource, shouldn’t they be able to reasonably guarantee what it yields? We could always have a struct or resource that the app supplies handle view resolution, and delegate to it from the AccessPoint resource.

Re: Intermediary account & User-side restrictions on accounts

This piece touches on my main concern, in general, with this approach, which is that apps are actually the bedrock to hybrid custody being useful. If we make it hard to:

  1. Enable hybrid custody
  2. Maintain their app after it’s enabled
  3. Protect their users who enable HC

Then how do we expect to sell it as a feature they should support or use? We’re basically touching on who is the priority when we consider this functionality and its road to adoption. Is it the user, app, decentralized platforms, or wallets that we’re selling this to? Obviously it’s a bit of all of them (though I’m not sure that wallets actually need to care here very much.) Platforms like Flowty are the ones who will bear the burden of understanding how to talk to these hybrid models, but again they’re only useful if we have products that make it available to us.

Can we make sure to cover a walkthrough of how an app which adopts this model has to maintain it? We’ve covered the basics in terms of setup and access, but that doesn’t include risk mitigation and general maintenance to make sure that users on their side still have the seamless experience they already have on the app. If our implementation takes away from that experience, I’m not sure any of them will accept this model.

Re: Gating save() on type

I think there’s more to explore on this piece, because we could prevent all mutating functions (link, unlink, save, and load) and just allow you to get a capability as an alternative model to sharing an account when an app needs to ensure that things safe on their end.

It hurts platforms and wallets, but gives assurances to the app that they’re safe which imo is going to be the main concern.

In this model, all I’d be able to do as a platform using this hybrid custody model is withdraw and deposit nfts (or whatever other types we want to make sure are supported.) The lower the barrier for apps, the better. Folks on the niftory team (@anir-niftory and @artur) can probably speak more to that, though.

Ultimately, the Hybrid Custody we’re after gives the user real ownership of the assets in their linked accounts and I think that should mean they have unrestricted access to those accounts and the things inside of them. That said, HC doesn’t have to be the solution for everyone. It sounds like there are concerns around broadly giving users full access to linked accounts, but I think the alternative - while my perspective might be a bit black-and-white - is rebuilding walled gardens under a new custodial paradigm mediated by wrapped AuthAccount Capabilities. IMHO, giving users restricted access significantly complicates dealing with those linked accounts given the number of various stakeholders involved in the implementation of Hybrid Custody.

Totally hear this, I think that’s what our upcoming call will help us understand a little better. This will be a constant battle, but we will have a bootstrapping problem to convince apps and platforms to use this and I am anticipating that we won’t be able to convince apps to fully relinquish control simply because of the added complexity they will take on from users being able to manipulate their system.

Allow the user to keep using my app with their child account seamlessly (no signing, etc).
…
It’s not clear to me that the answer to this is preventing access on the child account considering the cost is the loss of real ownership we’re after with Hybrid Custody. After all, can’t I sign a transaction that would modify Capability paths and reconfigure resources in unexpected ways today?

The thing is, right now it isn’t a consideration apps have to make because they fully control their accounts. It’s going to look like we’re asking them to take on a lot of complexity so that users on their app can leave to other systems which might even be competitors to the things they offer (like a marketplace, for instance)

What’s the road to convincing them that it’s not only worth it to allow, but that they should bear the burden of keeping their systems safe in this complete a way? As of now, apps generally don’t have to handle unknown user behavior like this, it’s a whole new paradigm and we might be a bit optimistic with what they’re willing to do for their users.

On concerns for access restrictions

Maybe I am off-base, but are we encouraging wallets to basically use these child accounts fully like they would other wallets? Because in my head I model this more like how I can login with google on lots of products. They’re all different accounts, but have a single access point (my main wallet in this case).

I don’t expect that when I’m logged in on Spotify that I can somehow use its systems when I later login to Door Dash, those two accounts are built and maintained for specific purposes, even if I only have a single account I login with.

In that same light, do we actually want to encourage more than access to the things that the app generates? I think that’s my main disconnect, here. I don’t see much value in allowing someone to pollute Ticket Master’s wallets with junk (thus bricking it and forcing TM to give it more flow) – all that I really need as a platform hooking into it is the NFT collection that it exposes.

If we adopt the current model/proposal, how do we think that an app will warn users who might link their account to another wallet? If we allow complete unfettered access, imagine the giant warning label we’ll see that will inevitably scare users just like when an app asks for more access than it needs on your phone. Instead, if we inverse the flow of this proposal so that it isn’t unrestricted access, you can actually follow a model more similar to what users see now with traditional apps where a prompt says:

This app would like to access your:

  • Contacts
  • Photos
  • Location

Something like:

Linking this account will give 0x1234 access to your:

  • Top Shot Collection
  • All Day Collection
  • Flovatar Collection

In some contexts, that access is fine. But if a calculator app asks for my location, I’m probably going to uninstall it and never consider it again. I anticipate users are going to think in this context. Have we made any considerations with that viewpoint in mind? For instance, users might get concerned when linking their dapper wallet because they may think it means access to their bank account or Dapper Balance. They won’t know that it’s separate, in their mind it’s all the same thing (their account)

Lastly, can we can get Dapper Wallet folks into Monday’s call just to make sure that all of this is considered with their needs? Ideally other non-dapper apps should be engaged as well so we aren’t just talking amongst ourselves as cadence/flow evangelists while forgetting that folks in traditional spaces will be hesitant to adopt it.

1 Like

Coming back to this with an alternative initial solution at something more friendly to apps:

This approach maintains apps as the owner of an account, and gives access to NFT collections over to the new parent account. Additionally, the child can share any other capabilities they’d like via a Proxy resource which I can use to obtain any additional functionality that products existing in the same space as the parent account might need.

After the meeting, considering account will be used as storage, I think we should have solved the problem without AuthAccount linking:

Considering shared account ( puppet ) is 0x1, my main account is 0x2. Dapp account is 0x3

  • Dapp creates 0x1, stores NFT collection
  • Dapp links private cap to access to collection to 0x3 ( which dapp will make all interactions with, the one mentioned that will be used after custody transfer )
  • When user links; dapp simply continues to use the same capability, gives same capability to user.

for little bit advanced use case:

  • we add to cadence following capability ability.
  • I save capability to/storage/myTopShotCap
  • when I link to this storage address, it actually links to the target of the capability.

No AuthAccount linked in the process.

After thinking and discussing with Austin a bit; I think we need multiple custody stages. ( instead of 2 )

  • App Custody ( when app creates the account )

Here the App will be both Owner and App

  • Shared Custody ( app and user has shared custody )

Here Owner is User and App is App

  • Full User Custody ( where user has the account AuthAccount access )

Here App is App, and Owner has full account access.

App has : right to put stuff to storage ( write only, cannot delete ) and link while putting the storage. So App can later put user’s account new resources for feature updates. Each capability is hold in some resource in child account, default all capabilities are also granted to App

Owner can: Change grants ( add / remove capabilities ) to dapp, but only capabilities created by dapp.
Owner also can promote their account by destroying the Owner resource by sending to the contract, and get AuthAccount capability for full custody.

App can check account custody status.

made a small code (except last propomotion) step in https://github.com/Flowtyio/restricted-child-account/blob/792419b803ccc6891c4b1989824460d70bf8a854/contracts/CapabilityGrants.cdc

1 Like

To catch others up on some offline discussion @bluesign and I have had, here are some other requirements we will need to consider:

  1. Integration with the CapabilityFactory so we can have a unified getCapability method to retrieve typed Capabilities from the child account. This is essential. Naked capabilities are not enough for marketplaces and other platforms to seamlessly use child accounts.
  2. A filter on-top of Capability retrieval to give apps control of what to share (this is a Dapper Wallet requirement). Right now, I have a contract called CapabilityFilter to do this but it might need some work, still
  3. CapabilityProxy so that the managing account is permitted to share specific types with the restricted account

I’m going to come back here once I have a first-pass at this three-phase approach. What we have so far is a great start, but these other requirements might change its structure considerably so some other attempts at implementation will be helpful as we discuss further

1 Like

Had some time to break down these thoughts more thoroughly, I figure it’s best to keep this forum up to date in case anyone has better ideas, and so that progression isn’t too sudden or we lose context along the way

@bluesign and I are hoping to define a standard interface which we can use to extend additional use cases on-top of. This could actually lead to a world where the contract defined by @gio_on_flow and the one I’ve put forth here could both exist and people can pick whatever “flavor” fits their needs similar to how we have an NFT standard but lots of implementations of them.

Working backwards from there, a first pass at a standard interface for this might look something like this:

pub resource interface Account {
  pub fun borrowAccount(): &AuthAccount
  pub fun getCapability(_ t: Type): Capability
  pub fun getAddress(): Address
  pub fun resolveView(_ t: Type): AnyStruct?
  pub fun getViews(): [Type]
}

I’m sure there are others that are needed. A good next step might be to put on our hats for each persona using HC and see what kinds of actions they are likely to universally need, and then ensure that we have all of that functionality exposed in some way for them to use. I’ll continue to think about that and come back with more once I’ve had the opportunity to gather my thoughts.

Lastly, I’ve taken some time to think through the flow for what a rework of my restricted child account system might look like in the case of multi-tenancy which it sounds like folks at Dapper Wallet might need given what @rrrkren described in our call last week.

  1. A child account has some @Account resource to manage any shared access to its auth account.
  2. There must be at least one “owner” of the account which can pass that ownership along. This owner could have rules attached to its access, but it has to be able to maintain ownership so that control is never lost fully
  3. There can be other accessors, all of which have access rules attached to them. The owner of the account can change these rules.
  4. Rules must be hosted on the Child account itself, that way no one can revoke another party’s ability to access them unless they have complete ownership of the account
  5. There is some kind of @Manager resource which is used to manage what Accounts you have access to

This is a 10k ft. overview of how I’m thinking about this:

To break it down:

  1. The Child account has a @ManagedAccount resource stored
  2. For each parent account, there is an individually private link exposing &ManagedAccount{Restricted}
  3. A private link is created for each parent, the capability is shared to them which they use to redeem and store into an @AccountManager resource which manages a way to go from address to a capability pointing to its private link
  4. There is a separate Owner interface which is exposed once and is given to an account which has some additional pieces used to manage things such as adding to the CapabilityFactory and CapabilityProxy, adding a new shared account, etc.
  5. The restricted account shared via Capability has getCapability among other functions exposed which allows it to retrieve typed capabilities in a universal way. Any type requested must have a corresponding CapabilityFactory entry.
  6. There is also a proxy that can be shared for one-off capabilities in the absence of a factory. These proxies are for more specific cases like if I wanted to expose a &{TopShot.MomentCollectionPublic} type for the child account publicly.
  7. Any use of getCapability will pass through the CapabilityFilter which ensures that a capability is able to be returned. This will cover Dapper Wallet use cases where some NFTs need to be restricted while others are able to be withdrawn as users see fit (Not all collections are withdraw-able)

And we can reduce all of these features available to use into a ruleset that lets the owner of an account determine:

  1. What paths you can reach (private or public)
  2. What you can mutate on an account (save, load, link, unlink)
  3. What interfaces you can get capabilities to
  4. What types underneath those capabilities you can access

This multi-tenancy would aim to express the three phases of ownership that @bluesign has mentioned:

Phase 1 - Full App Ownership, Restricted User Ownership

In this phase, the App has full control of the account. It might manage signatures via an AuthAccount Capability, or it could make use of the Owner interface should something like what I’ve illustrated above be adopted.

The app would share a link to a restricted account resource which then goes to a user’s main account, granting them partial access to things the app has control over to change, revoke, or expand at any time. This would mean an app can add things to their proxy, factory, filter, or revoke access altogether.

Phase 2 - Restricted App Ownership, Restricted User Ownership

In this phase, the app has an interest in “freezing” an account so that no one can touch it anymore. As mentioned by @rrrkren, Dapper Wallet could, for instance, consider making a new child account for every collection a user has access to. In that case, they only need a single collection in the account, and the ability to add flow tokens to it for storage capacity. They could revoke all other forms of access (or just restrict the ability to alter what’s already there) and signal to other parent accounts that this child account is stable and won’t change.

Phase 3 - Restricted App Ownership - Full User Ownership

Based on feedback from @anir-niftory and @artur at Niftory, it doesn’t seem like there is a world where full ownership for the app and user should be permitted at the same time. This is for compliance and regulatory concerns where an app could be categorized legally in different ways based on what it has access to. As such, the third phase would involve the app passing on ownership to another parent, restricting its own access in the process.

Full Ownership, Multiple Parents

While not one of the three phases, it’s important to highlight one of the use cases here for multi-tenant full ownership which is akin to hot and cold wallet management. Consider delegate cash on Ethereum which lets you use one Wallet ( A ) to designate another Wallet ( B ) as a “hot” wallet.

I might, for instance, always pay for things from a single wallet no matter where I put NFTs when I have a sale! In that case, two Wallets ( A ) and a new wallet ( C ) would both have full access to ( B ). We should take care not to leave that consideration out.


If I’ve missed anything to date regarding goals of this kind of standard, please let me know! To the best of my ability, this is a good summary of the moving parts based on conversations elsewhere. I’ll continue to follow-up as I make progress and hope to see what others think and/or come up with as well

Thanks everyone for the input today!

Before my summary, @gio_on_flow took some great notes and shared them in flow’s discord, you can find a link to them here

I will do my best to summarize the general consensus of everyone in attendance, then we need to determine next steps and who wants to explore solving our remaining problems.

  1. Applying restrictions to the user is acceptable to folks on the Dapper Wallet team
  2. Both Dapper Wallet and Niftory would prefer a simplified approach, so a good portion of the posts I’ve made the last few days will need to be reconsidered
  3. A multi-parent setup is required for how Dapper Wallet wants to manage this system on their side (@rrrkren are you okay sharing the diagrams you presented today?)
  4. Parents can have the same rules (I’m not sure about this part, will address later)
  5. We need to have a conversation about events and how to make sure those are done properly. @gio_on_flow brought this up and mentioned taking the lead on this part.
  6. @dete talked about designing an interface around this restricted account that acts as a delegate to the underlying auth account itself. Going forward with enhancements, the AuthAccount object itself will likely have something similar, so adopting some kind of interface now will prepare us for this change, and would let us express far more than just access to capabilities. I’ll explore this part but if others have interest please don’t hesitate to do so as well

I’ll get started summarizing open topics/issues in github so we can get folks who have interest in each piece going.

The main part I’m not sure of right now is whether it’s reasonable to assume apps will always want to have the same rules apply to all the parents they share things out with. Even if most apps are fine with sharing these rules, what if it turns out we an app needs to split that control? As an example, let’s say we have two parents where one is dapper, and the other is a user’s main account. The main account can only withdraw things which are clear of withdraw restrictions, whereas the dapper account would not need to follow those restrictions and should be able to freely sell any item it can reach on marketplaces in the dapper ecosystem.

1 Like

Thank You.