ERC-6551: Non-fungible Token Bound Accounts

We were pleasantly surprised to see A3S being mentioned in the discussion. We’ve been diligently working on A3S Protocol since 2022 and our product V1.0 has successfully achieved the NFT-gated contract address feature. This empowers the bulk transfer of on-chain assets and off-chain interaction records, enhancing the liquidity of addresses.

We’re thrilled that ERC6551 has become a trending topic. It has always been our mission to make the concepts and innovations of account abstraction more widely known and understood by the community.

Regarding @jay’s analysis, we find some of the points agreeable and would like to share our perspective as well:
Firstly, the functionalities of A3S and ERC6551 align closely, both are trying to let an NFT have ability to control assets, the slightly differences is that ERC6551 focuses on NFT’s ID can own accounts/wallets while A3S focuses on NFT itself being accounts/wallets. However, technically speaking, we can achieve same outcomes with minor code modifications.

Secondly, concerning “centrally owned and upgradable”, our initial version of product is actually a monetised application, so evidently we need it to be upgradeable to meet the demands of the market and users, it’s a common practice for projects rapidly evolving and iterating like us. We will renounce ownership and achieve full decentralisation at any time when we achieve our desired goals.

We have taken the first step by integrating crypto assets into NFTs and our long-term goal is to improve user-friendly interactions across diverse scenarios in crypto world. Hope we could engage in meaningful discussions with your team and join forces on EIP promotion and adoption.

Thank you to everyone who has commented on this proposal so far! We have submitted an update to the proposal which aims to address many points of feedback received so far. Here is a summary of the proposal changes:

  • The owner() method has been removed from the required account interface

    While this method was originally included to ensure compatibility applications that utilize EIP-173, several compelling reasons to remove it have been voiced in this thread:

    • Requiring account implementations to return a single owner address makes it difficult for this proposal to support semi-fungible tokens
    • The return value of owner() is redundant, as ownership of a token can be queried based on the return value of token()
    • The owner() method may cause unwanted bugs since it overlaps with often used Ownable contract implementations
    • Cross-chain account implementations cannot easily query for a token owner if the token exists on another chain

    Many thanks to @dongshu2013, @RobAnon, @0xTraub, @urataps, and @sullof among others for their dialogue on this point.

  • An isValidSigner(address signer, bytes context) method has been added to the account interface

    This method allows external callers to determine whether a given account is authorized to act on behalf of a token bound account, replacing the owner() method as the primary means of querying account authorization. This method gives account implementations greater control of their authorization models, and is better suited to supporting semi-fungible token bound accounts.

  • The nonce() method has been re-named to state()

    This change better reflects the goal of the function (allowing external contracts to introspect account state) and disambiguates it from the common understanding of an account nonce (an incrementing value used for replay protection). It also ensures that this function does not overlap with existing account implementations, enabling account implementations to more easily adopt this proposal.

  • The executeCall(address to, uint256 value, bytes data) method has been removed from the account interface in favor of a variable execution interface to be exposed by account implementations

    Allowing accounts to define their own execution interface allows this proposal to be compatible with both existing execution interfaces (e.g. Safe, EIP-725x, Kernel, etc) and upcoming execution interface standards (e.g. EIP-6900) without requiring account implementations to support multiple execution paths.

  • The recommended execution interface now supports delegatecall, create, and create2 operations

    This change allows account implementations to support any type of state change operation and ensures forward compatibility with future operation types. Thank you @wighawag for recommending this change.

  • The AccountCreated events emitted by the Registry are now indexed by implementation, tokenContract, and tokenId

    This change enables easier indexing and querying of previously created token bound accounts. Thank you @MidnightLightning for the thoughtful reasoning here.

We are also excited to welcome several new co-authors who have contributed significantly to the development of this proposal and the explorations that preceded it:

This proposal has been moved to the Review stage (pending PR approval). We hope to minimize breaking changes to the interfaces defined in this proposal as we move forward toward finalization.

9 Likes

Noticed that in the updated EIP text this concern wasn’t addressed. Any thoughts on this @jay ?

This was wasn’t addressed either, unless I’m misinterpreting the original text.

You could have the same implementation address, but if different salts are used, you get different resulting TBAs for the same implementation address, no?

We’re working with marketplaces to determine the best approach to handling active approvals during trustless token transfers. This was part of the motivation behind renaming the nonce() function to state(), which allows more fine-grained representations of account state that can be used by marketplaces. However, preventing fraud at the marketplace level is outside the scope of this proposal.

Thanks for catching this! It is no longer correct, I’ve updated the proposal to reflect that.

The “by default” clause here was intended to avoid contradiction, but this might be better phrased as “should” in the first sentence. Would be curious to hear others thoughts on this phrasing!

One common example is the collection creator wishing to deploy accounts on behalf of their holders in order to subsidize the gas costs associated with deployment.

Thanks for all your answers @jay

1 Like

I don’t believe that working with marketplaces will address this due to the abundance of assets with different approval / operator methods - some of which may be unique to the tokens being used.

Many of these tokens may be specific to the NFT community.

We could consider instead a custom pre programmed 4byte list that is excluded from sending transaction which could include approval and operator functions for ERC20 / 721 /1155 / 777 etc

Any indication when this might move to review? Are major changes still in the works?

I agree that the variety of token approval methods makes this a challenging problem to solve. However I don’t think this proposal should specify any token-specific mitigations as they could easily break compatibility with existing applications and may constrain forward compatibility with new token standards.

A 4byte blocklist could address this problem, but would come at the expense of compatibility with the existing ecosystem (e.g. it would prevent tokens held within a token bound account from being sold on marketplaces).

The PR that moves this proposal to review has been merged.

I think it would be useful for the TokenBound repo to provide an additional base account implementation that allows for delegation.

I believe a common usecase for 6551 will be to have a grail NFT used as your main identity in a hardware wallet, but delegate all TBA functionality to a hot wallet.

This way allowing use of your main identity NFT’s TBA as your main wallet for all interactions on Ethereum.

Any plans for this?

1 Like

This solves it, no?

Would be great to have this as an example in the reference repo! Feel free to open a PR :slight_smile:

This method does give accounts more flexibility when implementing alternative authorization mechanisms such as delegation.

I have a few suggestions or questions regarding the current spec.

1. Regarding the functionality of isValidSigner which is currently defined as:

    /**
     * @dev Returns a magic value indicating whether a given signer is authorized to act on behalf of the account
     *
     * MUST return the bytes4 magic value 0x523e3260 if the given signer is valid
     *
     * By default, the holder of the non-fungible token the account is bound to MUST be considered a valid
     * signer
     *
     * Accounts MAY implement additional authorization logic which invalidates the holder as a
     * signer or grants signing permissions to other non-holder accounts
     *
     * @param  signer     The address to check signing authorization for
     * @param  context    Additional data used to determine whether the signer is valid
     * @return magicValue Magic value indicating whether the signer is valid
     */
    function isValidSigner(address signer, bytes calldata context)

I believe it should be amended to specify this method SHOULD NOT revert in case the signer is not valid.

I saw that the reference implementation does not revert in case the signature is invalid, but it is important to specify in this case (as well as other cases) the unhappy path the execution takes.

Consider adding (or I can make a PR) with the following specification:

     * MUST return the bytes4 magic value 0x523e3260 if the given signer is valid
     * SHOULD NOT revert in case the given signer is invalid
     * SHOULD return 0x00000000 in case the given signer is invalid

I want to prevent different types of implementations which would require a smart contract interacting with ERC6551Account implementations where it expects that some calls fail (revert) and some return bytes4(0).

2. Add event to signal correct execution.

The interface IERC6551Executable does not emit any event in case of correct execution.

Consider adding an event which logs the correct execution as well as the returned values.

A possible option could look like:

event Execute(
    address indexed to,
    uint256 value,
    bytes data,
    uint256 operation,
    bytes response
);

Additionally, the reference implementation should be updated to emit this event.

3. Gas optimization in case of reverted execution.

Current reference implementation of ERC6551Account execute is:

    function execute(
        address to,
        uint256 value,
        bytes calldata data,
        uint256 operation
    ) external payable returns (bytes memory result) {
        require(_isValidSigner(msg.sender), "Invalid signer");
        require(operation == 0, "Only call operations are supported");

        ++state;

        bool success;
        (success, result) = to.call{value: value}(data);

        if (!success) {
            assembly {
                revert(add(result, 32), mload(result))
            }
        }
    }

If the execution is reverted, the whole contract state is also reverted, thus the state storage variable remains unchanged. By moving the ++state at the end of the method, you reduce the gas used in case the execution is reverted.

However, it is important to note that if the external call eventually calls back into ERC6551Account, the state will not be changed (yet).

This gas optimization might not be a good idea, considering the state does not change until the full execution finalizes. This is something up for discussion. When should the state be updated?

4. Updating the state in case ether is received

This is something that is also up for discussion. Should the contract state be updated in case ether is received?

Thus, it is worth considering if the execution of receive() should update the state and in what conditions.

Attach the current token bound account state to the marketplace order. If the state of the account has changed since the order was placed, consider the offer void. This functionality would need to be supported at the marketplace level.

Considering the Security Considerations, specifically, if a trade would be made invalid by changing the contract state, a simple call to receive() (with 0 ether or even 1 wei) would update and invalidate the Account state.


I am happy to think about these suggestions further, discuss them or even open PRs if any of them go through.

3 Likes

Though I’m not an author of this proposal, I would like to express my opinion regarding the proposed suggestions, as I consider them insightful and on point.

  1. I agree with specifying that isValidSigner SHOULD NOT revert when the signer is invalid. This will make it easier for contracts that interact with ERC6551Account to handle errors gracefully.
  2. I fully support the inclusion of an Execute event. This will allow for better off-chain monitoring and make it easier to trace successful interactions with the contract.
  3. Your point about gas optimization is valid only in cases when the external call reverts, which usually can be predicted by wallets or other providers before the execution. I think reducing gas for reverting execution paths, at the expense of potential re-entrancy is not worth it.
  4. The point about updating the contract state when ether is received is interesting and I believe needs to be considered by the authors.
2 Likes

Can the NFT wallet’s private key be used to sign something?

Specifically, could you encrypt a string and only let the owner decrypt it using the NFT’s private key?

Smart contracts do not have private keys. A NFT cannot own a private key. ERC6551 uses the owner of the NFT (an EOA) as a signer to allow executing code with/from the TBA.

@jay I made a PR on the ERC6551 reference implementation.

The examples in the repo, despite being basic, are correct, audited and safe. They can be used as a solid base to build more advanced contracts. In order to allow so, the functions must be virtual, so that they can be overridden, and public to be called as super (like supportsInterfaceId).

This PR simply add virtual to the functions’ signature.

PS > I also made a version of the reference implementation for the npm registry at

published as erc6551 on npm, which has already virtualized the functions, since we use the upgradeable example in one of our projects and needed it.

1 Like

Good call - might even be worth specifying that it must not revert, as try-catching external calls is a pain.

This is also a good suggestion. An earlier version of the proposal included an event, but it was removed in favor of letting accounts define their own events (since some accounts already define an execution event such as those supporting ERC-4337). I think it’s worth adding back in.

In order to prevent footguns in the case of reentrancy, the account should update the state prior to the external call. While there may be some gas savings in the case of a reverting call, I don’t know if it makes sense to optimize for the unhappy path while introducing complexity to the happy path.

This is a good question, I think the behavior of state could be made more explicit in the proposal. Since the goal of state is to enable marketplaces to protect against fraud, the thinking is that state is only updated when assets leave the account or an external call is made from the account. In that case, state would not be updated when ether is received.

Thanks for the PR! Merged :slight_smile:

2 Likes

Overall I’m supportive of this EIP becoming standard and commend the authors on their work. I have concerns that I don’t believe to be insurmountable but should at least be addressed before acceptance.

Primarily, I think that Fraud Prevention should be fleshed out before acceptance. Although I agree that a full treatment is beyond the scope of the EIP, at a minimum it’s important to establish a thorough argument for it being possible—I think exploration of asset commitments is the best option for this.

My rationale is that if prevention was found to be more difficult than anticipated, the viability of mass adoption of this ERC would be jeopardised. Brief outlines as currently exist are insufficient as they can provide a false sense of surety— for example, the first recommendation (to commit to account state) is trivially bypassed as transfer of token-owned ERC20/721/1155 assets only change the state of different contracts.

Consequently, the recommendation to use account state to prevent fraud should, in my opinion, be updated to explain why it MUST NOT be used.

A minor point, probably for a follow-up EIP, is consideration of a default, minimally viable implementation for air drops. Counterfactual Accounts and Account Ambiguity can combine for undesirable effects if token owners don’t know the implementation address to access one of their transitively owned assets (or even that such ownership exists). A default implementation would ease discovery of transitive ownership at scale.

1 Like