Skip to content

Instantly share code, notes, and snippets.

@bullishgopher
Created April 28, 2023 02:51
Show Gist options
  • Save bullishgopher/d7eeac6a99a5d96e28674fa9acec418b to your computer and use it in GitHub Desktop.
Save bullishgopher/d7eeac6a99a5d96e28674fa9acec418b to your computer and use it in GitHub Desktop.
ERC-6551: Non-fungible Token Bound Accounts

Registry

interface IERC6551Registry {
    /// @dev The registry SHALL emit the AccountCreated event upon successful account creation
    event AccountCreated(
        address account,
        address implementation,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId,
        uint256 salt
    );

    /// @dev Creates a token bound account for an ERC-721 token.
    ///
    /// If account has already been created, returns the account address without calling create2.
    ///
    /// If initData is not empty and account has not yet been created, calls account with
    /// provided initData after creation.
    ///
    /// Emits AccountCreated event.
    ///
    /// @return the address of the account
    function createAccount(
        address implementation,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId,
        uint256 salt,
        bytes calldata initData
    ) external returns (address);

    /// @dev Returns the computed address of a token bound account
    ///
    /// @return The computed address of the account
    function account(
        address implementation,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId,
        uint256 salt
    ) external view returns (address);
}

Account Interface

/// @dev the ERC-165 identifier for this interface is `0x400a0398`
interface IERC6551Account {
    /// @dev Token bound accounts MUST implement a `receive` function.
    ///
    /// Token bound accounts MAY perform arbitrary logic to restrict conditions
    /// under which Ether can be received.
    receive() external payable;

    /// @dev Executes `call` on address `to`, with value `value` and calldata
    /// `data`.
    ///
    /// MUST revert and bubble up errors if call fails.
    ///
    /// By default, token bound accounts MUST allow the owner of the ERC-721 token
    /// which owns the account to execute arbitrary calls using `executeCall`.
    ///
    /// Token bound accounts MAY implement additional authorization mechanisms
    /// which limit the ability of the ERC-721 token holder to execute calls.
    ///
    /// Token bound accounts MAY implement additional execution functions which
    /// grant execution permissions to other non-owner accounts.
    ///
    /// @return The result of the call
    function executeCall(
        address to,
        uint256 value,
        bytes calldata data
    ) external payable returns (bytes memory);

    /// @dev Returns identifier of the ERC-721 token which owns the
    /// account
    ///
    /// The return value of this function MUST be constant - it MUST NOT change
    /// over time.
    ///
    /// @return chainId The EIP-155 ID of the chain the ERC-721 token exists on
    /// @return tokenContract The contract address of the ERC-721 token
    /// @return tokenId The ID of the ERC-721 token
    function token()
        external
        view
        returns (
            uint256 chainId,
            address tokenContract,
            uint256 tokenId
        );

    /// @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);

    /// @dev Returns a nonce value that is updated on every successful transaction
    ///
    /// @return The current account nonce
    function nonce() external view returns (uint256);
}

Example Account Implementation

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "openzeppelin-contracts/utils/introspection/IERC165.sol";
import "openzeppelin-contracts/token/ERC721/IERC721.sol";
import "openzeppelin-contracts/interfaces/IERC1271.sol";
import "openzeppelin-contracts/utils/cryptography/SignatureChecker.sol";
import "sstore2/utils/Bytecode.sol";

contract ExampleERC6551Account is IERC165, IERC1271, IERC6551Account {
    receive() external payable {}

    function executeCall(
        address to,
        uint256 value,
        bytes calldata data
    ) external payable returns (bytes memory result) {
        require(msg.sender == owner(), "Not token owner");

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

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

    function token()
        external
        view
        returns (
            uint256 chainId,
            address tokenContract,
            uint256 tokenId
        )
    {
        uint256 length = address(this).code.length
        return
            abi.decode(
                Bytecode.codeAt(address(this), length - 0x60, length),
                (uint256, address, uint256)
            );
    }

    function owner() public view returns (address) {
        (uint256 chainId, address tokenContract, uint256 tokenId) = this
            .token();
        if (chainId != block.chainid) return address(0);

        return IERC721(tokenContract).ownerOf(tokenId);
    }

    function supportsInterface(bytes4 interfaceId) public pure returns (bool) {
        return (interfaceId == type(IERC165).interfaceId ||
            interfaceId == type(IERC6551Account).interfaceId);
    }

    function isValidSignature(bytes32 hash, bytes memory signature)
        external
        view
        returns (bytes4 magicValue)
    {
        bool isValid = SignatureChecker.isValidSignatureNow(
            owner(),
            hash,
            signature
        );

        if (isValid) {
            return IERC1271.isValidSignature.selector;
        }

        return "";
    }
}

Registry Implementation

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "openzeppelin-contracts/utils/Create2.sol";

contract ERC6551Registry is IERC6551Registry {
    error InitializationFailed();

    bytes constant creationCode =
        hex"60208038033d393d517f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5560f78060343d393df3363d3d3760003560e01c635c60da1b1461004e573d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e610049573d6000fd5b3d6000f35b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc543d5260203df3";

    function createAccount(
        address implementation,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId,
        uint256 salt,
        bytes calldata initData
    ) external returns (address) {
        bytes memory code = abi.encodePacked(
            creationCode,
            abi.encode(salt, chainId, tokenContract, tokenId, implementation)
        );

        address _account = Create2.computeAddress(
            bytes32(salt),
            keccak256(code)
        );

        if (_account.code.length != 0) return _account;

        _account = Create2.deploy(0, bytes32(salt), code);

        if (initData.length != 0) {
            (bool success, ) = _account.call(initData);
            if (!success) revert InitializationFailed();
        }

        emit AccountCreated(
            _account,
            implementation,
            chainId,
            tokenContract,
            tokenId,
            salt
        );

        return _account;
    }

    function account(
        address implementation,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId,
        uint256 salt
    ) external view returns (address) {
        bytes32 bytecodeHash = keccak256(
            abi.encodePacked(
                creationCode,
                abi.encode(
                    salt,
                    chainId,
                    tokenContract,
                    tokenId,
                    implementation
                )
            )
        );

        return Create2.computeAddress(bytes32(salt), bytecodeHash);
    }
}

Account Proxy Implementation

// ERC6551AccountProxy
// modified from https://github.com/jtriley-eth/minimum-viable-proxy/blob/main/huff/UUPSProxy.huff

// -------------------------------------------------------------------------------------------------
// ABI

#define function implementation() view returns (uint256)

// -------------------------------------------------------------------------------------------------
// CONSTANTS

// uint256(keccak256("eip1967.proxy.implementation")) - 1
#define constant PROXY_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc

#define constant WORD_SIZE = 0x20

#define constant CONTRACT_SIZE = 0xf7

#define macro CONSTRUCTOR() = takes (0) returns (0) {
    // STORE IMPLEMENTATION
    [WORD_SIZE]     // [word]
    dup1            // [word, word]
    codesize        // [codesize, word, word]
    sub             // [impl_offset, word]
    returndatasize  // [zero, impl_offset, word]
    codecopy        // []
    returndatasize  // [zero]
    mload           // [impl]
    [PROXY_SLOT]    // [proxy_slot]
    sstore          // []

    // STORE BYTECODE + FOOTER
    [CONTRACT_SIZE]             // [size]
    dup1                        // [size, size]
    __codesize(CONSTRUCTOR)     // [constructor_size, size, size]
    returndatasize              // [zero, constructor_size, size, size]
    codecopy                    // [size]
    returndatasize              // [zero, size]
    return                      // []
}

#define macro MAIN() = takes (0) returns (0) {
    // COPY CALLDATA TO MEMORY
    calldatasize                // [calldatasize]
    returndatasize              // [zero, calldatasize]
    returndatasize              // [zero, zero, calldatasize]
    calldatacopy                // []

    // LOAD CALLDATA SIGNATURE
    0x00 calldataload           // [calldata @ 0x00]
    0xE0 shr                    // [func_sig (calldata @ 0x00 >> 0xE0)]
    __FUNC_SIG(implementation)  // [func_sig(implementation), func_sig]
    eq                          // [func_sig(implementation) == func_sig]
    implementation
    jumpi

    // DELEGATECALL
    returndatasize              // [retsize]
    returndatasize              // [retoffset, retsize]
    calldatasize                // [argsize, retoffset, retsize]
    returndatasize              // [argoffset, argsize, retoffset, retsize]
    [PROXY_SLOT]                // [proxy_slot, argoffset, argsize, retoffset, retsize]
    sload                       // [impl, argoffset, argsize, retoffset, retsize]
    gas                         // [gas, impl, argoffset, argsize, retoffset, retsize]
    delegatecall                // [success]

    // COPY RETURNDATA TO MEMORY
    returndatasize              // [retsize, success]
    0x00                        // [retoffset, retsize, success]
    dup1                        // [memoffset, retoffset, retsize, success]
    returndatacopy              // [success]

    // RETURN IF SUCCESS, ELSE BUBBLE UP ERROR
    call_success                // [call_success, success]
    jumpi                       // []

    // FAILED
    returndatasize              // [retsize]
    0x00                        // [zero, retsize]
    revert                      // []

    // SUCCESS
    call_success:
        returndatasize          // [retsize]
        0x00                    // [zero, retsize]
        return                  // []

    // IMPLEMENTATION
    implementation:
        [PROXY_SLOT]            // [proxy_slot]
        sload                   // [impl]
        returndatasize          // [0x00, impl]
        mstore                  // []
        0x20                    // [0x20]
        returndatasize          // [0x00, 0x20]
        return
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment