ERC-6551: Non-fungible Token Bound Accounts

@conner thanks for highlighting this!

You’re 100% correct that with the reference account implementation you would have to go through N layers of nested calls to transfer a nested NFT, which isn’t ideal from a gas cost or user experience perspective. The two solutions you’ve proposed are great, and should definitely be considered as a feature at the implementation level.

One edge case that may break these two solutions is accounts that have not been deployed yet. An NFT could be nested 3 levels deep, but if either of the two parent account contracts have not been deployed, the token method will be unavailable to call, causing a revert.

An alternative approach that accounts for pre-deploy accounts would be to pass in a proof of ownership in the form of an array of structs containing information about the parent accounts. The account contract could verify this proof based on the computed addresses of the accounts in the ownership chain. It might look something like this:

struct ERC6551AccountData {
    address implementation;
    uint256 chainId;
    address tokenContract;
    uint256 tokenId;
    uint256 salt;
}

function executeCallNested(
    address to,
    uint256 value,
    bytes calldata data,
    ERC6551AccountData[] calldata proof
) external payable returns (bytes memory result) {
    (, address tokenContract, uint256 tokenId) = this.token();

    for (uint256 i = 0; i < proof.length; i++) {
        require(proof.chainid == block.chainid, "Invalid token");

        address account = ERC6551Registry.account(
            proof.implementation,
            proof.chainId,
            proof.tokenContract,
            proof.tokenId,
            proof.salt
        );

        require(IERC721(tokenContract).ownerOf(tokenId) == account, "Invalid proof");

        tokenContract = proof.tokenContract;
        tokenId = proof.tokenId;
    }

    require(require(IERC721(tokenContract).ownerOf(tokenId) == msg.sender, "Not root owner"));

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

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

This could be further optimized by computing the account addresses locally without calling into the Registry contract.

I think both of these functions are great additions to account implementations, as they will improve the feasibility of executing nested calls and determining the root owner of a set of nested NFTs. However, I’m not sure whether the implementation of these functions should be mandated by the proposal. Perhaps these would be better implemented in a companion library that account implementation authors could include if they wanted to opt into these features.