Contract factory for upgradeable erc721

I am trying to find some way for users to deploy an erc721 contract and would like to clone an implementation contract through a contract factory like eip-1167. However when the contract factory deploys a new contract, the interaction with the transparent proxy and erc721 do not work. The interaction schema i am trying to implement is below:


My thought is that a user would be able to call the factory to create a new proxy contract and that contract would point to the transparent proxy of my erc721. I want the transparent proxy so that I can upgrade the erc721 if needed. When my contracts are deployed you are unable to interact with the proxies created from the factory. I think this is because the erc721 has an initializer that only allows it to be called once but I am not sure how to get around that…

I’m pretty new to all of this but I think my questions are:

Are two layer proxies possible?
Is metadata stored in the transparent proxy or in the contract itself?
Is there a pattern to clone erc721 contracts but modify name and symbol for each proxy created by the factory? (eip-3448?)

I am not set on this layout or implementation and would like suggestions for a better schema to achieve the same result or fixes to my current code.
I would also appreciate any clarifying explanation on the topic of proxies as I am sure I have more to learn. I also put my code below as it might help understand what I am trying to do.

ERC721

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

import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";

import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol";

contract ERC721ControlledUpgradeable is ERC721Upgradeable, ERC721URIStorageUpgradeable, 
                    PausableUpgradeable, AccessControlUpgradeable, ERC721BurnableUpgradeable {
    using CountersUpgradeable for CountersUpgradeable.Counter;

    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
    CountersUpgradeable.Counter private _tokenIdCounter;

    function initialize(string memory name, string memory symbol) public {
        __ERC721_init(name, symbol);
        _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
        _setupRole(PAUSER_ROLE, _msgSender());
        _setupRole(MINTER_ROLE, _msgSender());
        _setupRole(BURNER_ROLE, _msgSender());
    }

    function safeMint(address to) public {
        require(hasRole(MINTER_ROLE, _msgSender()));
        _safeMint(to, _tokenIdCounter.current());
        _tokenIdCounter.increment();
    }

    function pause() public {
        require(hasRole(PAUSER_ROLE, _msgSender()));
        _pause();
    }

    function unpause() public {
        require(hasRole(PAUSER_ROLE, _msgSender()));
        _unpause();
    }

    function _baseURI() internal pure override returns (string memory) {
        return "";
    }

    function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal whenNotPaused override{
        super._beforeTokenTransfer(from, to, tokenId);
    }

    function _burn(uint256 tokenId) internal override(ERC721Upgradeable, ERC721URIStorageUpgradeable) {
        require(hasRole(BURNER_ROLE, _msgSender()));
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId) public view override(ERC721Upgradeable, ERC721URIStorageUpgradeable) 
            returns (string memory){
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId) public view override(ERC721Upgradeable, AccessControlUpgradeable) 
            returns (bool){
        return super.supportsInterface(interfaceId);
    }
}

Contract Factory

// USES ERC 1167
pragma solidity  >=0.8.0;
 
import "./ERC721ControlledUpgradeable.sol";

contract Factory{
    address public implementation;
    address[] public clonedContracts;


    constructor(address _implementationAddress) {
        implementation = _implementationAddress;
    }


    function createProxy() internal returns (address result) {
        bytes20 targetBytes = bytes20(implementation);
        assembly {
            let clone := mload(0x40)
            mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
            mstore(add(clone, 0x14), targetBytes)
            mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
            result := create(0, clone, 0x37)
        }
    }

    function createERC721Controlled(string memory name, string memory symbol) public {
        address clone = createProxy(); 
        ERC721ControlledUpgradeable(clone).initialize(name, symbol); // THIS IS A LITTLE FUNNY 
        //emit ClonedContract(clone)
        clonedContracts.push(clone);
    }
    
    function getAddress(uint i) view external returns(address) {
        return clonedContracts[i];
    }
  
}

I am using truffle to deploy the erc721 and then the factory.

1 Like

This architecture is not possible with the contracts that we have available. The problem is that the implementation address for the transparent proxy is kept in the storage of the transparent proxy, but when the user goes through the clone it will be using the storage of the clone, so it doesn’t know where to delegate.

I would recommend looking into beacon proxies. Check it out and let me know if it fits your use case.

1 Like

Thank you. The beacons and beacon proxies worked and I was able to deploy them manually. Can the beacon proxies be deployed through a factory? On my initial attempt with the factory the beacon proxy deployed from the factory but I was unable to mint a coin through the beacon proxy by calling safeMint.

The code for the factory is below:

pragma solidity >=0.8.0;

import './ERC721ControlledUpgradeable.sol';
import '@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol';

contract Factory {
    address public implementation;
    address[] public clonedContracts;

    constructor(address _impl) {


        implementation = _impl;
    }
    function createToken(string memory name, string memory symbol) external returns (address){
        // address clone = Clones.clone(implementation);
        // ERC721ControlledUpgradeable(clone).initialize(name, symbol);

        // clonedContracts.push(clone);
        // return clone;

        BeaconProxy proxy = new BeaconProxy(
            implementation, 
            abi.encodeWithSelector(ERC721ControlledUpgradeable.initialize.selector, name, symbol)
        );
        clonedContracts.push(address(proxy));
        return address(proxy);
    }
}

Any ideas?

The BeaconProxy works by holding the implementation address in a beacon contract. If you see the documentation for the constructor of BeaconProxy you can see that the first argument is not an implementation address but a beacon address.

You need to deploy an instance of UpgradeableBeacon, and then use that when you deploy each BeaconProxy.