ERC-6551: Non-fungible Token Bound Accounts

Sorry, for so many comments. But EIP173 and 5313 are both defining owner() for contract ownership,

/// @title ERC-173 Contract Ownership Standard
///  Note: the ERC-165 identifier for this interface is 0x7f5828d0
interface ERC173 /* is ERC165 */ {
    /// @dev This emits when ownership of a contract changes.    
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /// @notice Get the address of the owner    
    /// @return The address of the owner.
    function owner() view external returns(address);
	
    /// @notice Set the address of the new owner of the contract
    /// @dev Set _newOwner to address(0) to renounce any ownership.
    /// @param _newOwner The address of the new owner of the contract    
    function transferOwnership(address _newOwner) external;	
}

and

/// @title EIP-5313 Light Contract Ownership Standard
interface EIP5313 {
    /// @notice Get the address of the owner    
    /// @return The address of the owner
    function owner() view external returns(address);
}

while in 6551 you use owner() for account ownership:

    /// @dev Returns the owner of the ERC-721 token which controls the account
    /// if the token exists.
    ///
    /// This is value is obtained by calling `ownerOf` on the ERC-721 contract.
    ///
    /// @return Address of the owner of the ERC-721 token which owns the account
    function owner() external view returns (address);

Got it, thanks for clarifying!

I think you make a good point here - malicious (or unaware) developers could easily create implementations where the owner() function is provided by inheriting from Ownable, causing the implementation to be incompatible with this proposal at best and insecure at worst.

I actually don’t see this as an issue - using owner() was a deliberate design decision, as was our approach of using a separate ownable contract in our implementation. I think that in principle the owner() method is the right way to expose ownership of the account to external callers - token bound accounts should be “owned” by the holder of an NFT in the same way that traditional accounts are owned by a certain wallet address (as this allows the account contracts to be self-sovereign).

I do think that the behavior of the owner() method should be clarified within the proposal to highlight the difference between token bound accounts and traditional ownable contracts. The owner() method also seems to cause issues beyond prohibiting use of Ownable, especially when it comes to tokens that do not have a canonical owner such as ERC-1155.

One way around this which I think holds promise is removing the owner() method from the required account interface and instead suggesting that accounts support ERC-5313, with some specific suggestions around how to determine the owner if an account implementation chooses to do so. Do you think this would mitigate your concerns here?

1 Like

I am afraid that that would end up with dev using owner() to return the smart contract’s ownership, and implementing some non-standard, arbitrary way to return the owner of the account. The only scenario where that would work seamlessly is when the owner of the smart contract is the owner of the tokenId, which is unlikely.

I like the interface how it is now and believe that there is a need for a standard function to be called to know who is the owner of the account. But, to avoid conflicts and misuses, I suggest that you rename it.
accountOwner() looks like the most obvious name to me, but whatever you like is fine for me :slight_smile:

BTW, adding a specification for adherence to EIP-173 or EIP-5313 could be an extra. I think that for any user it would be good to know who owns the smart contract and can possibly modify it.

I like this proposal, it gives wallets a more clear sense of scarcity (owned assets) and authenticity (wallet-owner = NFT owner), and it enables various interesting utilities and mechanics to be introduced. However, I would like to point out the following things which seemed unclear to me:

  1. What is the motivation behind the name “Registry”? For me it doesn’t fit in since nothing is actually registered there, it only deploys proxies with a given account implementation. I think the word “Factory” would be more suitable as it clearly outlines the creational design pattern that takes place. In my world, “Registry” would be used for some single-point-of-entry contract that tracks different account implementations registered by somebody.
  2. I have concerns regarding the owner()method, besides the ones that @sullof mentioned. How should the account retrieve its current NFT owner if the token is on another chain? Perhaps we should not enforce this method at all, since the owner is also retrievable off-chain in this two-step process from the information returned by token(), i.e. get the tokenId and ask the owner from the tokenContract. In this way, the standard would get rid of the compatibility issues such as Cryptopunks whose contract do not have ownerOf(tokenId) method, at the expense of moving the ownership computation on client-side, which is not that bad. On the account side, we should worry about retrieving the NFT owner only if the implementation attaches some behavior it, such as transfer or upgrades.
  3. Since there are a lot of use-cases for upgradeable or modular accounts requiring other type of proxies (UUPS, Beacon, Diamond), we would need to make a two-step delegate call ERC-1167 → OtherProxy → Implementation which introduces a call overhead and security risks. I believe we can introduce more flexibility if we require only the last three parameters ( chainId, tokenContract, tokenId) to be embedded at the end of the bytecode, since they are the only ones that are accessed afterwards in token(). I don’t see why salt has to be in the account bytecode, and I would leave specific details about the implementation, it could be also placed in a dedicated storage slot as in ERC-1967.

Overall, I believe addressing these concerns will further improve the proposal’s clarity, functionality, and compatibility with existing standards and contracts. I look forward to minting an NFT for my wallet =)

1 Like

While I am not the author of this proposal and so I defer to them for an official response, I can share my thoughts on the points raised.

I concur with this sentiment. The proponent appears to conceive of the registry as a singular entry point, but in reality, it’s likely that multiple registries will be created for various purposes. Thus, referring to it as a Factory seems more fitting. To me, a Registry evokes an official index of audited or trustworthy factories.

I agree that omitting specific requirements on how the owner() function should be implemented may be advantageous, particularly as an account could support pre-ERC721 tokens. Nevertheless, I believe that a smart contract wallet should be designed to operate without necessitating a client-side component to verify whether a transaction originates from the token owner. Removing detailed specifications on the owner() function could pave the way for alternative approaches, such as those based on ZK proofs.

I am in agreement that the proposal may offer more flexibility in this regard, enabling the implementer to directly deploy the proxy of their choice, rather than mandating a specific type of proxy. The end result may be more costly, but in my view, this should not be a primary concern of the proposal.

1 Like

I assume there will be a special case for Punks.

For NFTR we do a special check for Punks as so:

Is there a plan for this in 6551?

@0xDigitalOil great question! We think that the concept of every NFT having deterministic accounts is most powerful when it can become a standard pattern across the ecosystem. It’s also very beneficial to have a single entry point for all token bound accounts, as this maximizes the benefits of deterministic addressing. In our opinion the EIP process is the best way to accomplish this, as it allows everyone to contribute to the refinement of this pattern and enables broad support for it throughout the ecosystem once consensus has been achieved.

@sullof I think you make a good point here, will give some thought to potential alternative names. I think there’s an opportunity to solve several of the existing issues with the owner() function in one change here.

@urataps The term “Registry” was chosen over “Factory” for two primary reasons. First, it frames it as a central entry point rather than one entry point among many. One goal of this proposal is to define a single, immutable, non-owned entry point that can be used for all token bound accounts. Having a single entry point makes it much simpler for this pattern to be implemented by applications and allows for the benefits of deterministic addressing to be most easily realized. Second, it focuses on the action of querying for an account address over the action of creating an account. This makes it clear that accounts for every NFT can be queried using the Registry.

This is a good point - ownership data cannot (currently) be retrieved within the owner() function across chains. This limits the usefulness of this function in the scenario of a token bound account being deployed on a different chain than the token that controls it.

The purpose of owner is primarily to allow external applications to query the owner of an account without having to introspect the code of the account. This is primarily useful when dealing with certain NFTs that don’t follow the ERC-721 spec (such as CryptoPunks or ERC-1155 tokens with canonical owners). It allows the implementation to determine ownership of the account, without the implementation details being leaked to the client. I think this is an important characteristic, but the current approach has some clear tradeoffs that warrant deeper consideration of how this is implemented.

You are correct regarding the additional gas overhead, but as far as I’m aware there are no additional security risks to having double-layer proxies as you describe since the implementation address of the account is constant. We use this pattern in our own implementation to enable upgradability (an ERC-1967 proxy behind an ERC-1167 proxy).

While nesting proxies does introduce additional gas costs, upgradable proxies will always cost more gas than immutable proxies since they read the implementation address from storage. Because upgradable proxies already come with additional gas overhead, the extra gas caused by a double-proxy is reasonable in my opinion. More flexibility will always come at the cost of higher gas fees. By defaulting to ERC-1167 proxies, the extra fees that come with a more complex proxy are opt-in rather than required for all users.

We’ve explored the use of native ERC-1967 proxies deployed by the registry previously in this thread and within the proposal. However, storage-based proxies come with a significant downside: they make it impossible for any stateless implementations to exist. This means that developers wishing to build stateless account implementations are not able to do so, and must accept the security risks that come with storage use despite their desire to avoid them. The reverse is not true - if proxies are deployed as ERC-1167 proxies, both stateless and stateful account implementations can be created.

Ultimately, ERC-1167 is the simplest form of proxy that is widely used and has broad ecosystem support. By opting to use the simplest possible proxy, this proposal gives maximum flexibility to developers wishing to build on top of it.

@sullof you are correct - the registry is designed to be a single entry point. The proposal specifically allows for the deployment of alternative registries (by specifying that accounts “should” be deployed via the registry rather than “must”), but highly encourages the use of the registry defined in the proposal to enable broad adoption of this pattern in the ecosystem with minimal integration effort. Applications that support this proposal must support the canonical Registry, and may support other registries if they choose to.

I agree with this, and would love to encourage the development of alternative authentication mechanisms for token bound accounts.

The primary reason for enforcing a single proxy type at the registry level is not to minimize gas costs, it is to maximize the benefits of deterministic addressing while enabling the most developer flexibility. By deploying ERC-1167 proxies from the registry, it ensures that the bytecode of all token bound accounts is well known and easily computable by other smart contracts.

Allowing the registry to deploy arbitrary bytecode would mean that smart contracts wishing to locally compute the address of an account would need to load the entire bytecode of the implementation contract into memory. It would also make it more difficult for clients to query the registry for the account corresponding to a certain token, as they would also need to be aware of the account bytecode.

Since ERC-1167 is very simple and can serve as the proxy for smart contracts with any logic, this still allows developers full flexibility to implement account logic as they see fit (including using alternative proxy models). I’ve yet to see a use case that cannot be supported by the current Registry/proxy model, if you know of any I would love to explore them and adapt the model to enable them.

Yes, implementations wishing to support Punks and other NFTs that are not compatible with ERC-721 are welcome to do so.

2 Likes

Thanks for you long response.

1 Like

It’s important to include the salt value in order to allow the account contract (and any contracts that that introspect the account bytecode) to locally verify that the appended data is valid by computing the valid account address for the appended data and comparing it with the actual account address. While not currently used in the example implementation, having local access to the salt value used to create the account will be important for contracts that wish to use token bound account addresses as an authentication mechanism.

1 Like

I agree that is important. It also allows a tokenId to owns more than one smart wallet, despite using the same implementation.

PS > I just realized that I left more comments here that on the couple of ERCs I am proposing. I must really like this one :smiley:

1 Like

Regarding the Punks question… ok, that makes sense!

So it is a N-to-1 relationship

Yep, thanks for mentioning this!

Amazing! Thanks so much for all of your comments :slight_smile:

Yes - every NFT can have an unlimited number of token bound accounts.

I’m genuinely impressed by the concept of Token Bound Accounts, and I’m truly excited about developing something using this concept!

I’m considering a mechanism for NFT rental using Token Bound Accounts (TBAs). The advantage of TBAs is that developers can freely implement the account as they please. For instance, one can create an account that can validate possession via the isValidSignature method, but not transfer assets.

By adding a mechanism that allows asset transfers to a specific account after a certain period, we can create a structure for NFT rental. Users who wish to rent out their NFTs can transfer them to such a TBA and sell them on marketplaces like OpenSea to facilitate the rental process. The buyer, although unable to transfer the asset on the TBA, can validate possession using ERC-1271 signatures.

Once the predetermined period ends, the original NFT owner can move the NFT on the TBA back to their account, marking the end of the rental period.

By using this method, there’s no need to create wrapped NFTs, and we can fully leverage the existing ecosystem as is.

This freedom to implement accounts opens up numerous possibilities for utilizing TBAs. However, a significant challenge is that users cannot easily verify the implementation of a TBA. If the relationship between an NFT and a TBA is one-to-one, a one-time verification would suffice. But if it’s a one-to-many relationship, it’s not realistic for users to verify the implementation of each TBA.

Are there any possible solutions to these challenges?

1 Like

Since token bound accounts are deployed as ERC-1167 proxies, users can easily verify the implementation for any token bound account. The implementation contract can be verified once, and the account instance contract can be verified by introspecting the proxy to determine the implementation address. On Etherscan, this can be done by clicking “is this a proxy?” on the token bound account contract.

Additionally, because token bound account addresses are deterministic, you can verify that an account is using a trusted implementation by computing the address for the account with that implementation and checking that it matches the address being used. This can be done prior to the deployment of the account contract.

1 Like

I mean that it’s unrealistic for average users to use Etherscan to verify each contract. But probably this is a UX issue, rather than TBA itself. I’ll consider how to improve the UX.

I wonder if there is any news about the new proposed name.

@sullof For sure! Here are a few of the options we’ve been considering if the owner() function is to be re-named:

  • executor()
  • operator()
  • signer()
  • controller()
  • custodian()
  • steward()
  • manager()
  • admin()

The goal of these names is to reduce the emphasis on the holder of the token as the canonical owner of the account, since the token itself is the owner. The holder simply has temporary execution permissions.

However, as discussed earlier in this thread, the concept of a canonical controller makes it difficult for this pattern to be applied to tokens that lack a canonical owner. Additionally, there is currently no way to determine whether a given address has execution permissions on an account without assuming that the executor must be the owner. This assumption will likely be broken in practice quite quickly. As a result, it might make sense to replace owner() with a new method that can be queried to determine whether a given address has permissions on the account.

Here are a few options that are being considered:

  • is...(address) returns (bool) for any of the above names (e.g. isExecutor).
  • isValidSigner(address) returns (bool)
  • isAuthorized(address) returns (bool)
  • controlledBy(address) returns (bool)
  • managedBy(address) returns (bool)

Another option is to add a function that can be queried to determine a caller’s ability to execute a certain function on the account. This could look like:

  • isAuthorized(bytes4 sig, address caller) returns (bool)
  • canExecute(bytes4, address) returns (bool)
  • hasPermission(bytes4, address) returns (bool)

This could be even further extended to accept the full calldata bytecode as a parameter instead of just the method signature. Something like:

  • canExecute(bytes calldata data, address caller) returns (bool)

Additionally, each of the above functions which return a boolean could return a magic value instead, allowing for further extension of functionality at the account implementation level.

I don’t want to stray too far into mandating specific access control patterns here (e.g. role-based permissions) as I think those are out of scope for the proposal. However, access control is a central part of this proposal and I think some minimal pattern should included in the account interface.

Part of the challenge here is determining a term that can be used in place of owner to properly captures the distinction between the token (which “owns” the account) and the token holder (which “uses” the account).

I’m leaning towards the term signer, as I think it fits well with existing smart contract account terminology (e.g. “signer on a gnosis safe”). It’s also similar to the verbiage used in EIP-1271.

I would propose that the owner function be replaced by the function:

  • isValidSigner(address) returns (bytes4)

The proposal would specify that the token holder should be regarded as the default signer and that all valid signers should also have execution rights.

Would love any and all feedback on these potential interfaces :slight_smile:

4 Likes

Thanks so much for the update.

I agree.
signer makes a lot of sense in the context of the account.

To keep the same approach of owner(), maybe you can go with signer() instead of the more verbose isValidSigner(). I am not sure if signer() creates any conflicts with existing EIPs, but I don’t recall any general EIP where that was used.

I understand the connection salt has with an account’s address and the idea of leveraging this for authentication is really cool, but then wouldn’t it make more sense to keep this private as some kind of key?

Also, since you mentioned that some smart contracts may use salt as authentication, not all of them, then this should not be a requirement (i.e. SHALL) but rather an option (i.e. MAY) for the implementer of the proposal. But we have to acknowledge that including or excluding the salt (or any data) from the bytecode changes the address computation using CREATE2.

So, I’m wondering, does the standard allow for some flexibility regarding the appended data and what can be introspected from the bytecode? I predict the answer is no, so let’s try to answer this follow-up question: Can we introspect arbitrary data in some other way?

I’m asking this because the account bytecode doesn’t actually know that implementation contains a smart contract account, as far as the ERC-1167 proxy is concerned, it is an arbitrary address. Therefore, I’m exploring the possibility of other token-bound approaches, or even a more universal NFT-owned smart contracts proposal, where the ability to introspect arbitrary data from the proxy bytecode is crucial, as well as its compatibility with TBAs.

1 Like