ERC-6551: Non-fungible Token Bound Accounts

Hmmm. That’s a good point.

Note that my proposal also computes based on implementation and index. But the spam attack vector still exists.

Perhaps the reference implementation only allows the current token owner to deploy which would prevent this attack at least for the reference implementation.

Note that I do need to patch up my proxy implementation to defer owner check to the implementation

Allowing for optional initializer calls during account creation seems very useful!

I missed that, thanks for clarifying.

I agree that desired protections against this type of attack should be enforced at the account implementation layer. However, I’m hesitant to recommend owner-only deployments as the default configuration for a wallet implementation, since there are many potential positive use cases for permissionless deployments that could be inhibited.

The reference implementation wouldn’t be part of the eip. But rather the main website explaining and promoting this eip. Similar to royalty registry or delegate.cash

The eip itself would be implementation agnostic. But the site should have a ref implementation that allows every token in existence to have an address from the get-go (which would be based on the ref implementation), and a one click way for any token owner to get write access to the address.

This ref implementation would be deployed to every chain at the same address. And I am suggesting it to be similar to this one:

Which has the features of upgradeable implementation, prevention of non token owners from initializing the wallet and nonce incrementing built in.

Again, this is outside the scope of the eip but IMO needed for the main site and launch in a way that ppl can start engaging with these wallets (pushing content) without the token owner even needing to deploy the wallet.

@wwhchung Agreed that a demo site will be super helpful to help folks understand how this EIP works! In the meantime, this is great as an example in the reference implementation repo. Appreciate the PR :slight_smile:

We have similar ideas at our latest version of EIP-4972: Update EIP-4972 Pull Request. Although our proposal is not for ERC-721 but for domain names, the ideas behind are quite similar: to give nft/name a bounded account

Our interface is little bit different from EIP-6551 but I think there is a potential to merge these two EIPs to provide a more universal interface. Here are some of my thoughts to simplify the interface:

  1. The “executeCall” function is actually not related to this EIP. Each implementation should be able to customize the way to call other functions. It could be included in the reference implementation though.

  2. The “owner” function is not necessary since it can be derived from the token contract and token id.

  3. The chainId param is not necessary for functions/events at IERC6551Registry since you can always get it from block.chainId, (except you want to register account at chain A for chain B, which doesn’t make sense to me). You can still use chainId as a field in the byte code spec when registering the account but it should not be included in the function params. The chainId returned by token function can also be emitted.

1 Like

Another thoughts is that instead of putting implementation as a param of all functions of IERC6551Registry, is it better to extract it out as a general view function so all accounts registered by this registry are guaranteed to share same implementation? It may not be as flexible as current version but it will prevent potential fragmented implementations, making it more easier to integrate.

The new interface will look like this:

interface IERC6551Registry {
    event AccountCreated(
        address tokenContract,
        uint256 tokenId
    );

    function createAccount(
        address tokenContract,
        uint256 tokenId
    ) external returns (address);

    function getImplementation() external returns (address);

    function account(
        address tokenContract,
        uint256 tokenId
    ) external view returns (address);
}

interface IERC6551Account {
    receive() external payable;

    function token()
        external
        view
        returns (address tokenContract, uint256 tokenId);
}
1 Like

@dongshu2013 thanks so much for the comments! Awesome to see other folks exploring the potential impact of smart contract accounts tied to on-chain assets.

I agree that this looks a bit out of place in the proposal. However, including a standard execution function in the account interface enables the very desirable characteristic of having a standard interface for interacting with all token bound accounts regardless of the underlying implementation code. Without a standard execution interface you would need some form of interface detection to determine how to execute calls from different account implementations, which would get messy quickly and make the proposal more difficult to adopt. Definitely not married to the current execution function signature though, so open to suggestions there.

You can definitely query the owner directly from the token contract given the token information. However, this function serves two purposes in the proposal. First, as above, it gives a standard interface for interacting with accounts. Second, it makes token bound accounts somewhat compatible with existing infrastructure built around ownable contracts. This may be useful for supporting token bound accounts in some legacy applications. If this proposal is expanded to support semi-fungible tokens, this interface may change and the current owner function would be removed in favor of something like isOwner(address) returns (bool).

This is true for single-chain applications, but including the chainId as a parameter allows wallet implementations wishing to implement cross-chain compatibility to do so. Hardcoding block.chainid removes that possibility.

Having the ability to support any implementation is actually a very important characteristic of this proposal because it allows for client diversity in wallet implementations. If the registry provided a static wallet implementation, it would either be immutable and thus not able to adapt to improvements in the smart contract account ecosystem, or it would be upgradable and thus pose an unacceptable centralization risk. By allowing for arbitrary smart contract implementations that adhere to this standard to be bound to an NFT, this allows the ecosystem to continuously experiment with different approaches to token bound accounts in a fully decentralized manner.

Additionally, since the holder of an NFT has sovereignty over their asset, it would stand to reason that sovereignty would extend to choosing which wallet implementation they would like to use. By allowing arbitrary implementations, applications wishing to adopt this standard but support only a single implementation are free to do so. However, by restricting to a single implementation at the registry level, the reverse would not be true.

Hope this answers your questions! I think there are some really neat applications to be built around accounts bound to a canonical name, and would love to ensure that there are broadly adopted standards that can support this use case.

2 Likes
  1. How to define a unified function call interface is a common problem for all smart contract wallets. Since the Account Abstraction is getting its momentum, I believe it deserves its own EIP and we should not mix it with this one. e.g. The sample implementation of smart account for account abstraction is called execute and executeBatch. We will need a more comprehensive/unified API set(e.g. the batch one) for smart contract wallets instead of a simple executeCall function.

  2. Removing owner function will not make it in-compatible with existing infrastructure since they can always implement their own owner function. We can suggest them to implement the owner function in the reference implementation but enforcing all implementations to have this function doesn’t make sense to me. Also ERC-4337 are actually intentionally avoiding the specific “owner” function and uses verifyUserOp() function to do the ownership verification.

  3. A more common practice is to put chainId at the spec but not as explicit function params. Again, taking ERC-4337 as example, per the spec, the signature of UserOps should include chainId and the entry point contract, but it’s just in the spec and should not be reflected in the interface definition. For cross-chain application/wallets integration, it could be unified in a higher level(e.g. SDK) instead of hardcoding the chainId into the smart contract interfaces.

  4. I don’t have a preference for this one but I think these two are equivalent. Putting implementation as the function params assumes that there is only one global registry contract for all implementations, while extracting it out as independent view function assumes each implementation will have its own registry contract(just like ERC20). I’m not sure which one is better, but my hunch is that the second approach will be easier for dApps who only care one implementation to integrate.

2 Likes
  1. ERC-6551 accounts are welcome to implement additional execution functions if they see fit to do so, the common interface does not restrict that. Agreed that there is a great opportunity to standardize the interface for these execution calls! I’m fine with execute as the common interface function.
  2. Agreed that this is a long-tail edge case, as it would only apply to dapps that are no longer actively maintained (therefore will not support the proposal) and are also using owner calls as authorization.
  3. Would love to hear any recommendations you have to ensure the possibility of cross-chain support without including the chain ID in the creation params!
  4. With the current proposal, dapps wishing to support a single registry are welcome to do so at the client layer, or create their own registry wrapper contract which uses a single known implementation. The reverse is not true - if the proposal specified a single registry with a known implementation or multiple registries with a single implementation per registry, it would be impossible to build a single canonical registry. This exact tradeoff was explored at length during the development of this proposal, and it was determined that a single global registry contract would present the easiest path towards ecosystem adoption.
1 Like

1 & 2: One principle we should follow here is to minimize the required functions to cover the core functionalities and leave the rest to community, that’s why I insist we should remove the owner and executeCall function and leave it to some other EIPs. As I said, we can always put it in the reference implementation and encourage others to follow but there is no need to enforce it in the spec

3: I just realize that I don’t fully understand the “cross-chain compatibility” problem you are referring here? What exact problems are you trying to solve or avoid by specifying this chain id field in function params? (I’m expecting users will still use chain id when creating the account(since it’s required by the spec) but there is no need to put it in the function params, so I didn’t see why my proposal will undermine the cross-chain compatibility.)

4: For the registry part, I think you made a good point that a global registry is much easier to aggregate than a list of registry contracts if you want to index all implementations. I’m thinking how the ecosystem can benefit from a canonical registry besides easy indexing though, since I’m expecting there will be multiple registry contracts out there anyway even we unify the interface for all implementations.

1 Like
  1. I definitely agree with you in principle here, but think providing a unified way for callers to execute transactions and query for the owner(s) of an account is important functionality that should be included in the interface. Especially since these are likely to be two of the most common calls executed against accounts. Without a standard interface defined in the proposal, how would you suggest that clients wishing to adopt this proposal should perform these calls across all accounts given varying implementations?

  2. In the specific case of owner, having the function defined on the interface means that an external caller given only an account address doesn’t need to know that the address is a 6551 account in order to determine the account owner. I’m very open to other interfaces for this, but this seems like an attractive property and I’m curious how you would recommend handling this case without defining something on the interface?

  3. Assuming NFT #1 has account 0xabc... on a chain with ID 1, the only way for NFT #1 to also use 0xabc... on any other chain is by passing the chain ID as a parameter to the account creation call on the registry contract, which is deployed to the same address on both chains. Without this, how could NFT #1 use their unique address across all chains?

  4. The other major benefit of a canonical registry is that every NFT gets a canonical account address per implementation which can be used prior to contract creation. By allowing any implementation to be deployed via the canonical registry, the need for alternative registries is minimized.

1& 2: Just to clarify, I didn’t say we don’t need to unify the interface, my point is that it’s out of the scope of this EIP since it should be a common interface for all smart contract wallets. Not sure if there is any other EIPs addressing the issue, if there is, we can follow that. It does make it easier for dApps to integrate 6551 accounts with these functions. However, in the long run, if the interface of other smart contract wallets is unified in another way, dApps will be have to maintain two set of interfaces(Ideally dApps should be able to integrate 6551 accounts just like all other smart contract wallets.) Defining the general interface of smart contract wallets is a bigger topic which we can discuss it in other threads.

3: Just curious, why do we need a bound account for NFT at chain 1 for chain 2? In my understanding, the ownership is guaranteed by the ERC721 contract so there is no way to guarantee the ownership at chain 2 if the NFT contract is not deployed at chain 2. Also if the NFT contract is deployed at chain 2 with a different address, then the bound account address will also be different.

4: Minimize the need for alternative registry makes sense to me. My concern is that if we cannot eliminate the need, eventually dApps will have to maintain a list of registry contracts, which brings us back to the original situation. However, again, I don’t have a strong preference for this one, so either one lgtm

1&2: I agree! Would happily support a proposal for a cohesive execution interface across smart contract accounts. In the absence of a recommended existing proposal or the emergence of a new proposal for this purpose, I think these functions should remain in the interface for this proposal for now. It can always be updated prior to last call if an alternative proposal emerges. Account implementations can also be updated to support a new standard that emerges, so I don’t see a conflict here.

3: You’re correct that there is no explicit guarantee of ownership of an NFT across chains, so it would be up to wallet implementations to determine if they would like to support cross-chain execution. This proposal includes chain ID as a parameter to the account creation call in order to leave open this possibility. One potential use case here is allowing a given NFT to collect POAPs (which are limited to use on a single chain).

If an NFT contract is deployed at the same address across chains, they will be distinct tokens (as a token with the same ID across chains can have two separate owners) and should have distinct wallet addresses. This proposal essentially treats an account address as a meta-asset that an NFT can own across all chains if the chosen wallet implementation supports it.

A few new updates to the proposal based on feedback that has been received so far:

  • The Registry now deploys ERC-1967 proxies instead of ERC-1167 proxies. This change increases compatibility with existing proxy detection infrastructure since ERC-1967 proxies can be detected by reading a storage slot rather than pattern matching against bytecode. This change also allows wallet implementations wishing to implement upgradability to do so without requiring an intermediate proxy.
  • The Registry contract is now deployed via the create2 factory at 0x4e59b44847b379578588920cA78FbF26c0B4956C. This allows for wider deployment compatibility with EVM chains that strictly enforce EIP-155, which breaks the deployment method that was used previously.
  • Added a salt parameter to the createAccount function, allowing tokens to have multiple token bound accounts per implementation
  • Added an initData parameter to the createAccount function, allowing for optional initialization of token bound accounts upon deployment
  • Added function nonce() external view returns (uint256) to the IERC6551Account interface to allow for easier tracking of state changes on the account. This will allow marketplaces to more easily implement scam prevention measures when fulfilling orders involving token bound accounts.
3 Likes

Are there any pitfalls to using this for an account-bound token(a token which can’t be transferred). Since the token can’t be moved out of the wallet, in the scenario of the wallet getting compromised will it end up losing everything tied to the account itself?

@tigerthedev good question! Token bound accounts will work out of the box with account-bound tokens, assuming that they conform to the ERC-721 standard. In general, token bound accounts are only as secure as the wallet holding the underlying token. In the case of a compromised wallet, all accounts for tokens stored in that wallet would also be compromised.

got it @jay. Currently what I see most of the projects do is issue a new account-bound token when the wallet is compromised. So the owner needs to take care of moving all the tokens out of the account to the new account which is bound to the token.

I can see a lot of discussions around PFPs as true on-chain identities using ERC-6551. I am a bit confused here about how a transferable token can act as an identity. Will you be able to shed some light here?
article link here: ERC 6551 — Token Bound Accounts. Every NFT will have their own… | by Benny Giang | FUTURE PRIMITIVE | Mar, 2023 | Medium

We’re looking to adopt EIP-6551 for mech and I started catching up on discussions here. :wave:

We modeled the API of our execution interface after Safe where transactions are defined with the following parameters:

address to
uint256 value
bytes memory data
Enum.Operation operation  // 0 = call, 1 = delegatecall

Support for delegatecall seems important to us (e.g.: for batching via multi-send), but it also comes with the implication that we cannot prevent account owners from writing to their TBA’s storage. Account implementations that allow generic delegate calls are implicitly upgradable with storage-based proxies.

That’s why I would like to suggest reconsidering this change:

The Registry now deploys ERC-1967 proxies instead of ERC-1167 proxies. This change increases compatibility with existing proxy detection infrastructure since ERC-1967 proxies can be detected by reading a storage slot rather than pattern matching against bytecode. This change also allows wallet implementations wishing to implement upgradability to do so without requiring an intermediate proxy.

I feel ERC-1167 proxies are better suited for the following reasons:

  • Implementation immutability, even if an account allows for generic delegate calls
  • Lower tx execution cost (no storage read)
  • IMO, they are common enough so that we can assume good support from proxy detection infrastructure.

Due to the nature of TBAs being transferred between people, immutability of the account implementation seems paramount for establishing trust.

1 Like

We actually started out with an 1167 version, with the option to support upgradeability via another upgradeable storage based proxy:

So, initially, it was:
Account = 1167 → Upgradeable Proxy → Implementation

But we debated on whether or not an intermediate proxy was good, or if we should just do 1967 natively.

Sounds like you’re recommending the 1167 → Upgradeable Proxy (even a 1967) → Implementation. Are we on the same page? If so, the changes seem pretty minor, we’re just offloading upgradeability to another intermediate proxy. Seems like we’d be introducing a bit of call overhead though, no?

Thanks for the comments @jan! Would love to ensure 6551 can support mech.

As @wwhchung mentioned, an earlier version of this proposal used ERC-1167 proxies. Part of the motivation for switching to ERC-1967 proxies was the concern that appending extra constant data to the proxy bytecode would cause problems in applications using naive approaches to identifying ERC-1167 proxies (e.g. using regex with an assumed bytecode length). Switching to ERC-1967 proxies sidesteps this issue. However, lack of proxy detection support might be less of a concern in practice.

The other motivation for switching to ERC-1967 proxies was to allow accounts wishing to implement static upgradability to do so without requiring an intermediate proxy. Arbitrary delegate calls allow the executor of the account to use any implementation, but don’t allow additional functions to be defined on the account which are accessible to non-executors. If an ERC-1167 proxy is used, implementing static upgradability would require a second upgradable proxy behind the ERC-1167 proxy. The shift to ERC-1967 was partially an attempt to reduce this redundancy.

You make a good point that the shift to ERC-1967 proxies forces all account implementations to use storage, which breaks compatibility with account implementations that allow for arbitrary delegate calls. Using ERC-1167 would allow for stateless account, while also allowing account implementations that wish to implement upgradability to do so with an intermediate proxy. This comes at the cost of some additional gas overhead for upgradable accounts, but that seems reasonable.

I think that trust in an account implementation can be established via either full immutability (as mech does) or via controls on storage access (so long as arbitrary delegate calls are disallowed). It seems that this decision should be made by each account implementation rather than being enforced at the proposal level.

I would support a return to ERC-1167 proxies with appended constant data in order to accommodate stateless accounts.