ERC-721 collection deployed via metatransactions show up in OpenSea as owned by the relayer instead of the `from` address

I have a smart contract RevenueSplitter which is used to create ERC-721 contracts via a CollectionFactory using the minimal proxy pattern. We recently switched to using meta transactions to initiate contract creation. One unintended side effect of this is that the relayer is now listed as the contract creator for the ERC-721 contract which affects who can manage the contract on OpenSea.

Meta transactions are issued through openzeppelin relayers / auto-tasks and verified by a Forwarder contract before calling the RevenueSplitter with the encapsulated transaction.

The autotask sends an Execute transaction to Forwarder contract which then calls createCollection on the RevenueSplitter contract. The RevenueSplitter then calls createCollection on the CollectionFactory contract which creates the actual collection using a minimal proxy pattern (OpenZeppelin Clones).

What I would like to accomplish is that the contract creator be the from address in the meta transaction rather than the relayer's address. Is this possible, and if yes, what changes do I need to make?

:1234: Code to reproduce

Relevant contracts and transaction:

This is an example of a meta transaction used to create a collection:

https://polygonscan.com/tx/0xac1896b6e72786a581c5f106156bff290323aa5ee80cb36b4e588c835a55e216

The Forwarder is https://polygonscan.com/address/0x0ee7dd0f427077922918ef836d2e80aa7fdcc717

The RevenueSplitter is https://polygonscan.com/address/0xfcd4d3faa3c7d7f1c61e9d1dc8391c96a55675d8

The CollectionFactory is https://polygonscan.com/address/0x27478bdeb95673400207ba78a2cd3973f1064967

The resulting ERC-721 contract is https://polygonscan.com/address/0xa4708ce0adb069ade32854fabb759831b6120b5e


Relevant code

Forwarder.execute

  function execute(
        ForwardRequest calldata req,
        bytes calldata signature
    ) external payable returns (bool, bytes memory) {
        require(
            verify(req, signature),
            "REMXForwarder: signature does not match request"
        );
        _nonces[req.from] = req.nonce + 1;

        uint256 gasForTransfer = 0;

        (bool success, bytes memory returndata) = req.to.call{
            gas: req.gas,
            value: req.value
        }(abi.encodePacked(req.data, req.from));

        require(success, "REMXForwarder: call failed");

        if (gasleft() + gasForTransfer <= req.gas / 63) {
            /// @solidity memory-safe-assembly
            assembly {
                invalid()
            }
        }
        return (success, returndata);
    }

RevenueSplitter.createCollection

    function createCollection(
        string memory name,
        string memory symbol,
        uint256 royaltyAmount,
        address[] memory _payees,
        uint256[] memory _shares,
        string memory baseURI,
        address minter
    ) external override returns (address) {
        require(
            hasRole(CREATE_COLLECTION_ROLE, _msgSender()) ||
                owner() == _msgSender(),
            "RMX: caller is not owner or collection creator"
        );

        require(bytes(name).length > 5, "RMX: Name too short");
        require(bytes(symbol).length >= 3, "RMX: Symbol too short");
        require(royaltyAmount <= 100, "RMX: Invalid royalty amount");

        address _collection = _collectionFactory.createCollection(
            owner(),
            minter,
            address(this),
            name,
            symbol,
            royaltyAmount,
            baseURI
        );

        _collections[_collection].registered = true;
        _collections[_collection].totalShares = 0;

        emit CreateCollectionEvent(address(_collection));

        _addCollectionPayees(address(_collection), _payees, _shares);

        return address(_collection);
    }

CollectionFactory

    constructor() {
        implementation = address(new REMXCollection());
    }

    function createCollection(
        address _admin,
        address _minter,
        address revenueSplitter,
        string memory name,
        string memory symbol,
        uint256 royalty,
        string memory baseURI
    ) external override returns (address) {
        address clone = Clones.clone(implementation);
        REMXCollection collection = REMXCollection(payable(clone));
        collection.initialize(
            _admin,
            _minter,
            revenueSplitter,
            name,
            symbol,
            royalty,
            baseURI
        );
        return clone;
    }

:computer: Environment

I don't think you can change how OpenSea detects the contract creator.

However, I'm pretty sure OpenSea will read the owner() getter of a contract to assign permissions to manage the collection, which I think is ultimately what you're interested in. See https://docs.opensea.io/docs/8-customizing-your-storefront

So if you make the ERC721 Ownable and you make the right account the initial owner you should get it set up as you want.

This worked perfectly! It was not obvious from their documentation and example code at all, but now you point it out it makes a lot of sense :slight_smile:

Many thanks

1 Like