NFT Keyed Ephemeral Counterfactual Minimal Proxy Contracts

These example contracts demonstrate a gas effective way to deploy counterfactual contracts using CREATE2. Using minimal proxy instances that are destroyed between transactions is a secure way to isolate assets in a way that only the key holder can access, Using the token ID of a known NFT contract allows this access to be transferred based on the permissions associated with that NFT.

This is an example of a simple “smart wallet” implementation. In practice allowing it to execute arbitrary calls will mean no other methods are needed, however additional security can be added by implementing explicit calls followed by selfdestruct().

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.14;

contract Implementation {
    /// @notice A structure to define arbitrary contract calls
    struct Call {
        address to;
        uint256 value;
        bytes data;
    }

    /// @dev reference back to the factory
    address private immutable _owner;

    // called in the factory constructor - when immutable!
    constructor() {
        _owner = msg.sender;
    }

    // only the factory can call functions on the instance
    modifier onlyFactory() {
        require(_owner == msg.sender, 'factory only');
        _;
    }

    /// @dev make call without a return value
    function doSomething() external payable onlyFactory {
        // make a call without a return value, maybe payable
        // ...then selfdestruct the contract
        selfdestruct(payable(address(0)));
    }

    /// @notice Executes calls on behalf of this instance.
    /// @param calls The array of calls to be executed.
    /// @return An array of the return values for each of the calls
    function executeCalls(Call[] calldata calls) external onlyFactory returns (bytes[] memory) {
        // handle the calls
        bytes[] memory results = new bytes[](calls.length);
        for (uint256 i = 0; i < calls.length; i++) {
            // solhint-disable-next-line avoid-low-level-calls
            (bool success, bytes memory result) = calls[i].to.call{value: calls[i].value}(calls[i].data);
            require(success, string(result));
            results[i] = result;
        }

        // NOTE: cleanup() must be called from the factory!
        return results;
    }

    /// @notice Destroys this contract
    function cleanup() external onlyFactory {
        // remove the bytecode - mayble handle balances on the factory?
        selfdestruct(payable(address(0)));
    }
}

This factory contract will create new minimal proxy instances using the token ID as the salt. Only the token owner is allowed to call the function to create the contract, so any assets in the contracts storage location will be safe between calls.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.14;

import '@openzeppelin/contracts/interfaces/IERC721Enumerable.sol';
import '@openzeppelin/contracts/proxy/Clones.sol';
import './Implementation.sol';

contract Create2Factory {
    /// @dev the nft used to calculate the address
    IERC721Enumerable private _token;

    /// @dev the address of the implementation for the minimal proxy
    address private _implementation;

    /// @dev constructor
    constructor(address nft) {
        // nft contract used as key
        _token = IERC721Enumerable(nft); // pass into constructor, etc.
        // smart wallet implementation for minimal proxy
        _implementation = address(new Implementation());
    }

    /// @notice get the address of the instance for the given tokenId
    /// @param tokenId the tokenId
    /// @return the address of the instance
    function getAddressForTokenId(uint256 tokenId) external view returns (address) {
        return Clones.predictDeterministicAddress(_implementation, _salt(tokenId));
    }

    /// @notice some call without a callback
    /// @param tokenId the tokenId
    function doSomething(uint256 tokenId) external payable {
        // get a minimal proxy instance of the implementation
        Implementation instance = _createInstance(tokenId);
        // will auto destruct
        instance.doSomething{value: msg.value}();
    }

    /// @notice Allows the owner of an ERC721 to execute arbitrary calls on behalf of the associated wallet.
    /// @dev The wallet will be counterfactually created, calls executed, then the contract destroyed.
    /// @param tokenId The token ID
    /// @param calls The array of call structs that define that target, amount of ether, and data.
    /// @return The array of call return values.
    function executeCalls(uint256 tokenId, Implementation.Call[] calldata calls) external returns (bytes[] memory) {
        Implementation instance = _createInstance(tokenId);
        bytes[] memory result = instance.executeCalls(calls);
        // manuall cleanup
        instance.cleanup();
        return result;
    }

    /// @dev Computes the CREATE2 salt for the given token.
    /// @param tokenId The token ID
    /// @return salt bytes32 value that is unique to that token.
    function _salt(uint256 tokenId) private pure returns (bytes32 salt) {
        return keccak256(abi.encodePacked(tokenId));
    }

    /// @dev Creates a Implementation for the given token id.
    /// @param tokenId The token ID
    /// @return The address of the newly created Implementation.
    function _createInstance(uint256 tokenId) private returns (Implementation) {
        require(msg.sender == _token.ownerOf(tokenId), 'not owner');
        // get the create2 clone address
        address payable _address = payable(Clones.cloneDeterministic(_implementation, _salt(tokenId)));
        // get a minimal proxy instance of the locker
        Implementation instance = Implementation(_address);
        // return the clone instance
        return instance;
    }
}

You may also like...

Leave a Reply

Your email address will not be published.