ERC-7656: Variation to ERC6551 to deploy any kind of contract linked to an NFT

Abstract

This proposal introduces a variation of ERC6551 that extends to all types of contracts linked to non-fungible tokens (NFTs). This generalization allows NFTs not only to own assets and interact with applications as accounts but also to be linked with any contract, enhancing their utility without necessitating modifications to existing smart contracts or infrastructure.

For reference:

Motivation

Our initial approach involved proposing an expansion of ERC6551 to encompass a broader scope of token-bound contracts beyond accounts. The goal was to enable any deployed token-bound contract to potentially represent more than just an account, with the actual functionality determined by checking the contract’s interface. Unfortunately, this suggestion was not adopted due to concerns about complicating the trust model built into ERC6551, where projects rely on emitted events to understand the nature of the contract without additional verification steps. Our proposed change, while introducing versatility, would have necessitated interface checks by developers, introducing a layer of friction and potentially leading to non-account contracts being banned as spam by Token Bound Account (TBA) indexes.

Reflecting on this feedback and considering the vast potential applications, we have crafted this new proposal. It aims to transcend the limitations of having NFTs solely function as accounts, thus unlocking a myriad array of interactions and functionalities. By proposing a separate but complementary standard, we seek to preserve the integrity and trust model of ERC6551 while offering a framework for NFTs to engage with diverse contracts. This approach not only enriches the ecosystem but also aligns with the evolving complexity of digital and real-world assets that NFTs aim to represent.

Of course, if ERC6551 evolves in a more general way, we’d be happy to dismiss this proposal.

Specification (updated on March 23 to latest version)

The interface IERC7656Registry is defined as follows (the number is temporary, waiting for an EIP editor to assign it):

interface IERC7656Registry {
  /**
   * @notice The registry MUST emit the Created event upon successful contract creation.
   * @param contractAddress The address of the created contract
   * @param implementation The address of the implementation contract
   * @param salt The salt to use for the create2 operation
   * @param chainId The chain id of the chain where the contract is being created
   * @param tokenContract The address of the token contract
   * @param tokenId The id of the token
   */
  event Created(
    address contractAddress,
    address indexed implementation,
    bytes32 salt,
    uint256 chainId,
    address indexed tokenContract,
    uint256 indexed tokenId
  );

  /**
   * The registry MUST revert with CreationFailed error if the create2 operation fails.
   */
  error CreationFailed();

  /**
   * @notice Creates a token linked account for a non-fungible token.
   * If account has already been created, returns the account address without calling create2.
   * @param implementation The address of the implementation contract
   * @param salt The salt to use for the create2 operation
   * @param chainId The chain id of the chain where the account is being created
   * @param tokenContract The address of the token contract
   * @param tokenId The id of the token
   * Emits Created event.
   * @return account The address of the token linked account
   */
  function create(
    address implementation,
    bytes32 salt,
    uint256 chainId,
    address tokenContract,
    uint256 tokenId
  ) external returns (address account);

  /**
   * @notice Returns the computed token linked account address for a non-fungible token.
   * @param implementation The address of the implementation contract
   * @param salt The salt to use for the create2 operation
   * @param chainId The chain id of the chain where the account is being created
   * @param tokenContract The address of the token contract
   * @param tokenId The id of the token
   * @return account The address of the token linked account
   */
  function compute(
    address implementation,
    bytes32 salt,
    uint256 chainId,
    address tokenContract,
    uint256 tokenId
  ) external view returns (address account);
}

Any contract developed using the ERC76xxRegistry SHOULD implement the IERC76xxContract interface:

interface IERC7656Contract {
  /**
  * @notice Returns the token linked to the contract
  * @return chainId The chainId of the token
  * @return tokenContract The address of the token contract
  * @return tokenId The tokenId of the token
  */
  function token() external view returns (uint256 chainId, address tokenContract, uint256 tokenId);

}

or the IERC6551Account interface or both.

Here an implementation for a contract implementing IERC7656Contract.

contract ERC7656Contract is IERC7656Contract {

  function token() public view virtual returns (uint256, address, uint256) {
    bytes memory footer = new bytes(0x60);
     assembly {
      extcodecopy(address(), add(footer, 0x20), 0x4d, 0x60)
    }
    return abi.decode(footer, (uint256, address, uint256));
  }
}

Note: The interfaces above have been updated to reflect latest version in the ERC repo

2 Likes

I made a PR for the proposal at

Co-author of ERC-6551 here. This is cool! I like that it adds an additional level of flexibility for contracts that wish to be bound to a token but don’t want to support signature validation or execution capabilities.

It would be awesome if every ERC-6551 account were able to be compatible with this proposal out of the box, given that this is a higher level abstraction which borrows from its structure.

One way to achieve this would be to modify the interface slightly to allow for multiple sub-registries to exist and be compatible with this proposal’s registry. Something like:

interface IERC76xxRegistry {
  /**
   * The registry MUST emit the TokenLinkedContractCreated event upon successful account creation.
   */
  event TokenLinkedContractCreated(
    address registry,
    address contractAddress,
    address indexed implementation,
    bytes32 salt,
    uint256 chainId,
    address indexed tokenContract,
    uint256 indexed tokenId
  );

  /**
   * Deploys a contract linked to a token
   * It a new contract is deployed, it MUST emit a TokenLinkedContractCreated event.
   * If the contract has already been created, it just returns the account address.
   */
  function createTokenLinkedContract(
    address registry,
    bytes4 createSelector,
    address implementation,
    bytes32 salt,
    uint256 chainId,
    address tokenContract,
    uint256 tokenId
  ) external returns (address account);

  /**
   * Returns the computed address for the contract linked to a token
   */
  function tokenLinkedContract(
    address registry,
    bytes4 computeSelector,
    address implementation,
    bytes32 salt,
    uint256 chainId,
    address tokenContract,
    uint256 tokenId
  ) external view returns (address account);
}

createTokenLinkedContract and tokenLinkedContract would then execute a call to the registry contract specified with abi.encodePacked(selector, abi.encode(implementation, salt, chainId, tokenContract, tokenId)) as calldata.

This would allow any ERC-6551 account that wished to enable compatibility with this proposal to do so without requiring any changes to ERC-6551 itself.

2 Likes

Hi @jay, thanks for your feedback.

I thought of this proposal as a complement to ERC6551. In fact, in the Cruna protocol, we deploy managers and plugins that are not accounts using this variation of your registry, while we deploy plugins that are accounts using ERC6551Registry.

However, I like you suggestion. The only tradeoff I see is that the caller must know the address of the registry that will actually deploy the token bound contract, and the selector of the function to trigger the deployment or to get the computed address.
It looks like there isn’t a real advantage in calling this pre-registry instead of calling the actual registry, since the caller will spend more gas. Considering that the internal call has to pass many parameters to the registry, I suspect the cost will increase of at least 15,000 gas.

One optimization may be removing the selector and expect that any registry calls the two functions createAccount and account (like in ERC6551), despite if the deployed contracts are actually accounts. This way we can skip the selector as a parameter. Having it, though, gives more flexibility, which is in the end the goal of your suggestion.

What about the registries? Should any registry try to set up a standard? If so, this ERC could evolve and become the specification for the registry we use in Cruna, emitting the TokenLinkedContractCreated, and we may create a new ERC that works as a superset, opening the road for future evolutions that we don’t see now.

What do you think?

I was pondering over the idea that if we consider each sub-registry to have its own distinct scope, it would actually be perfectly acceptable for the Cruna registry to function as an ERC6551Registry without being strictly for accounts. This means there shouldn’t be an automatic expectation that contracts deployed by it must conform to IERC6551Account. In other words, just because the registry triggers the same event as the canonical ERC6551Registry doesn’t necessarily mean the deployed contract is an account.

Perhaps it could be suggested in the ERC-6551 proposal that any registry diverging from the canonical ERC6551Registry — while still implementing the IERC6551Registry interface — isn’t bound by the stipulation to ensure deployed contracts support IERC6551Account and IERC6551Execute.

Following our Telegram conversation, I made a change to the proposal to make any ERC6551 account compatible with it out of the box. Basically, the proposal says that any deployed contract should implement IERC76xxContract, or IERC6551Account, or both.

This is very intriguing. What kinds of use cases inspired this or do you have in mind?

In the protocol I work on — Hats Protocol — developers in the ecosystem are creating a number of contracts meant to be deployed in association with a given ERC1155 token (a “hat”). Some of these are ERC6551 accounts, but many others are other types of contracts that do various non-account things.

Recently we have created a factory to enable cheap and standardized deployment of instances of these contracts. It enables instances to be deployed as minimal proxy clones with immutable args for more gas efficiency at both deployment and runtime. To facilitate this, our factory allows arbitrary bytes to be passed in during creation.

I know this is relatively opinionated, but if you were to extend the registry to enable that kind of contract creation logic, I would strongly consider moving away from our factory and to this registry. Curious to hear what you think!

That is precisely why we propose a generic registry that would apply whenever a certain contract’s owner is a specific token.

The Cruna protocol primarily adds security and inheritance features to NFTs. To do so, any NFT must be associated with its own manager. The manager can manage the transferability of the NFT by adding protectors and safe recipients and plugging more contracts to expand the NFT’s functionality.
The use cases for plugins are innumerable: lending platforms, asset distributors, identity managers, etc. Anything that has a sense if associated with a token can be a plugin.

Currently, the Cruna protocol does not support ERC1155. Still, we are working on adding a voting system that allows the owners of ERC1155 editions to define a virtual owner that owns the manager and the plugins. The virtual owner should be a particular contract that acts like a multi-sig.

I see two separate components in your protocol, the technology to deploy the contracts and the logic connected with this deployment. This is similar to how the Cruna protocol works. In fact, you can plug something only under very strict conditions to avoid vulnerabilities, but the basic tool used to deploy the contract is the ERC6551Registry if the plugin is an account and the ERC7656Registry if it is not.
You may do the same without losing flexibility.

Just to add more details, we are waiting to complete a technical audit of the protocol; then, we will deploy the protocol, including a canonical version of the registry, to all EVM chains, starting from some tenets. In any case, we will provide the code to deploy the registry everywhere it is needed.

In case you want to read more, you can find info at

and

I guess my main question is whether the ERC7656Registry can append arbitrary bytes to the footer of a newly deployed contract instance.

From what I can gather from the reference implementation, create() appends the salt, chainId, tokenContract, and tokenId to the instance’s deployed bytecode. In addition to that, I would also like to see something like a bytes extraImmutableData argument handled by create().

Is my question clear? Happy to elaborate if not.

Understood. As it stands, the current proposal does not include support for an additional data field for appended code. However, I am not sure there is a necessity of such a feature.

If I’ve understood correctly, the primary objective behind incorporating immutable data is to safeguard certain contract logic from modifications. It seems plausible to achieve this without embedding the code directly within the primary proxy contract.

Consider a scenario where the contract structure is bifurcated: one part being the actual implementation and the other, a proxy. In this setup, the registry deploys what essentially acts as the proxy contract. Thus, we are dealing with a triad of contracts:

  1. The proxy created via the registry,
  2. Your designated proxy contract, and
  3. The actual implementation contract.

In such an arrangement, function calls are forwarded to the implementation only if they are not recognized within the proxy contract, allowing the immutable code to reside within the proxy. This distinction enables the separation of contracts into those that are ERC6551Account-compliant and those that are not, managed respectively by the ERC6551Registry and the ERC7656Registry. It’s also worth noting the inherent benefits of aligning with the ERC6551Registry, given its widespread acceptance across numerous marketplaces and platforms. Additionally, we’re in the process of integrating an ERC7656 scanner to further enhance compatibility and functionality.

The primary objective in my case is to enable runtime usage of that data in a more gas efficient manner. In other words, enable the clone proxy instance to access those values roughly as an immutable value rather than with an SLOAD. This gas savings is important for us because Hats modules are invoked quite often.

Many of the Hats module contracts interact with other contracts or have configuration values. For example, our ERC20 eligibility module, when invoked, checks whether a given account has a balance of the established token gte the established threshold value.

Got it.

What if we move the salt to the last part of the bytecode and change its type from bytes32 to bytes?
In this case, the salt may even be null, making the proxy smaller, or be just a typical salt, or be whatever you put in there to be used to set immutable data.

I am not sure it will work efficiently, but what do you think?

1 Like

At first blush I like it!

It does deviate from ERC6551 a bit, but that’s likely ok since the relationship between 7656 and 6551 seems more important to hold for contracts themselves than at the registry level.

1 Like

I am playing with it.
A big issue is that the gas cost becomes higher, even with 0x extra data, because of the dynamic size of the salt and the added keccak256 of the salt to get the bytes32 salt to be used with the create2.
Adding extra costs for everyone seems a bad trade-off.
Anyway, I will try to dig in my assembly knowledge to see if I can reduce the gas cost. A solution may be to set two functions, one with a standard salt and one with a dynamic salt, using an extra byte in the code to specify the case and the possibility of calling the favorite function.
That would diverge from ERC6551 quite a bit.

@spengrah I got stuck with the code trying to figure out if that makes sense.

After the initial experiments, I am afraid that the extraData, if not properly validated, could include malicious bytecode or operations that compromise the contract’s integrity. Since the safety of the registry is paramount, I think we have to stick to the current approach.

However, you may encode the extra data in the salt. 32 bytes can contain a lot of information.