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; } }