Superfluid && Factory/Clones

Let’s say you have a SuperApp contract, of which you want to deploy multiple instances but with different arguments - e.g. your contract is a DeFi pool between token pairs like USDC/BTC, USDC/ETH, etc. Instead of manually deploying each contract, you can make use of the Factory/Clones pattern that some gigabrains have come up with!

Sounds cool, but what exactly is this Factory/Clones pattern thing?

The Factory pattern is a common design pattern that delegates the responsibility of creating instances of some type (e.g. your smart contracts) to another entity (the Factory). This was a very abstract definition - you should read more about it [here] or [here] - but essentially you use this to make your system easier to manage.

Clones, technically not a pattern, is an implementation by the brainiacs at OpenZeppelin of EIP-1167, which allows us to clone a smart contract in a very gas efficient way.

It should be easy to see how the Factory pattern and Clones fit very well (indeed many have written about it before [here], [here], [here], [here], [here], etc).

Ok, but how does Superfluid fit in the picture?

Well, if you are developing on Superfluid, and you’re deploying the same SuperApp over and over again, but simply with some different parameters, then you should be using the Factory/Clones pattern!

But when deploying a SuperApp through a Factory contract, you can’t follow the same registration process as with a directly deployed SuperApp, i.e. you cannot use registerAppWithKey. The good news is that our friends at Superfluid are aware of that, and built something specific for this: registerAppByFactory!

And not only am I going to show you how things work, but also how to use this with a Mainnet Fork. You should read my previous article for some context around that.

Yeah, show me the goods!

Superfluid has 2 ways for getting your SuperApp registered to deploy on a Mainnet. This article describes Option 2, where you provide the address of the Factory contract that will be allowed to register unlimited SuperApps through the registerAppByFactory function.

So how does registerAppByFactory work? The implementation is in the Superfluid Host contract, and it simply checks if the caller is a contract, and if some configKey is present in the Governance configuration mappings.

if (APP_WHITE_LISTING_ENABLED) {
    // check if msg sender is authorized to register
    bytes32 configKey = SuperfluidGovernanceConfigs.getAppFactoryConfigKey(msg.sender);
    bool isAuthorizedAppFactory = _gov.getConfigAsUint256(this, ISuperfluidToken(address(0)), configKey) == 1;
    if (!isAuthorizedAppFactory) revert HOST_UNAUTHORIZED_SUPER_APP_FACTORY();
}
_registerApp(configWord, app, false);

That’s pretty vanilla! And the configKey is a simple hash of a key name and the Factory address.

How does this key get set? During the whitelisting process, the Superfluid team calls the Governance function authorizeAppFactory, which will write an entry into the aforementioned mapping.

That’s it, then you’re ready to start deploying and registering your SuperApps through your “SuperFactory”!

Here’s an example on a Mainnet Fork

Let’s take this example SuperApp contract, which has a doSomething function that changes some internal state, and makes use of Open Zeppelin’s Initializable contract for initializing itself.

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";


contract MySuperApp is IMySuperApp, Initializable, SuperAppBase {
    uint256 public myVar;

    constructor() {}

    function initialize(ISuperfluid host) external initializer {
        // initialize your SuperApp
    }

    function doSomething() external returns (bool) {
        // some internal logic
        myVar = 123;

        return true;
    }
}

It implements an interface (defined below) so that we don’t have to import the whole thing later in the Factory.

import {ISuperfluid, ISuperApp} from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol";

interface IMySuperApp is ISuperApp {
    function initialize(ISuperfluid host) external;

    function doSomething() external returns (bool);
}

The Factory contract has the logic for creating instances of the SuperApp contract. It does that through a createMySuperAppInstance function that will create a clone of the contract and deploy it.

contract MySuperFactory {
    address public mySuperAppAddr;
    mapping(string => address) public deployedSuperApps;

    constructor(address _mySuperAppAddr) {
        mySuperAppAddr = _mySuperAppAddr;
    }

    function createMySuperAppInstance(
        ISuperfluid _host,
        uint256 configWord,
        string memory _superAppName
    ) external returns (address deployedAddr) {
        console.log("creating the MySuperApp instance");
        // we're using the app name for the salt, so we want to disallow creating 2 instances with the same salt
        require(
            deployedSuperApps[_superAppName] == address(0),
            "SUPER_APP_EXISTS"
        );

        // create salt and use the create2 for deploying the clone
        bytes32 salt = keccak256(abi.encodePacked(_superAppName));
        deployedAddr = Clones.cloneDeterministic(mySuperAppAddr, salt);

        // store the new instance, and register it on superfluid
        deployedSuperApps[_superAppName] = deployedAddr;
        ISuperfluid(_host).registerAppByFactory(
            ISuperApp(deployedAddr),
            configWord
        );

        // initialize the instance here (not required to be done here)
        // but you probably want to avoid letting this leak
        IMySuperApp(deployedAddr).initialize(_host);
    }
}

Let’s dissect it in parts.

Our example Factory keeps track of 2 important things: the address of the target contract to Clone, and a mapping with the created clones. For this example, we require a name for each clone, which becomes the key of the mapping.

In the createMySuperAppInstance function, we check that no clone exists with the passed name, and we use the hash(name) as an input to Clones.cloneDeterministic(…), which will create the clone and deploy it using CREATE2.

We then store the deployed address, and register the clone with Superfluid, using registerAppByFactory - this will allows it to interact with the Superfluid primitives.

Finally, we initialize the clone, so it can start being used.

Ok, how do we deploy all these things? Here’s the meat of it.

console.log("DEPLOY THE SUPER APP CONTRACT");
const mySuperAppDeployer = await hre.ethers.getContractFactory("MySuperApp");
const mySuperApp = await mySuperAppDeployer.deploy();
await mySuperApp.deployed();

console.log("DEPLOY THE SUPER FACTORY CONTRACT");
const MySuperFactoryDeployer = await hre.ethers.getContractFactory("MySuperFactory");
const mySuperFactory = await MySuperFactoryDeployer.deploy(mySuperApp.address);
await mySuperFactory.deployed();

console.log("AUTHORIZING SUPER FACTORY TO REGISTER APPS");
const factoryRegistered = await registerFactory(HOST_ADDR, govAddr, govOwnerAddr, mySuperFactory.address);

registerFactory does all of the business of impersonating a Superfluid Governor, and calling authorizeAppFactory to allow our Factory to register SuperApps with Superfluid.

Check the GitHub repo for the whole thing.

That’s All Folks!

Subscribe to omnifient
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.