Superfluid && Mainnet Fork

Are you building a dApp on top of Superfluid.finance? And now you want to test it with a Mainnet Fork instead of your local chain or testnets? But you don’t know how to do it?

GM, you’ve landed at the right place.

Deploying to a Mainnet

Before deploying your Superfluid smart contract (SuperApp) to a Mainnet, you need to be whitelisted, AKA Superfluid needs to allow you to register your SuperApp with them.

There are 2 options; which one you select depends on the architecture of your system. In this article I’ll explain how to use Option 1 in Mainnet Fork, i.e. registering a SuperApp using a registration key. [Click here to learn how to use Option 2]

Basically, you request a registration key from the Superfluid team (through a GitHub issue). That key is for a specific chain, associated with a EOA (the deployer account), and has an expiration date. Once that key is created, you can use your account to deploy SuperApps to that chain.

Deploying to a Mainnet Fork

Most likely you won’t have a registration key yet, when deploying to a Mainnet Fork. The workaround is to take advantage of the fact that you have forked the chain, and that you can impersonate and change the state as you like.

Ok, but what exact state do we need to change, how do we do it, and where do we start?

The SuperApp Registration Process
First, let’s understand how the registration process of a SuperApp works.

  1. The registration is done through the ISuperfluid Host contract’s registerAppWithKey [code], which then calls _gov.getConfigAsUint256(...).

  2. The _gov variable is of type ISuperfluidGovernance, and getConfigAsUint256 [code] simply checks if the arguments are present in the Governance’s internal _configs mapping.

Was this confusing? Maybe this diagram can clarify things:

The SuperApp Registration Floooow
The SuperApp Registration Floooow

Setting a Registration Key in a Mainnet Fork
Ok, now that we know how things work, it should be easy to understand what we need to do: we simply have to set a valid value into the SuperfluidGovernance’s _config mapping!

By inspecting SuperfluidGovernanceBase code, we can see some interesting functions: setConfig [code], setAppRegistrationKey [code], and some others.

What value to pass to setConfig? If you paid attention, ISuperfluid.registerAppWithKey actually does a transformation before calling ISuperfluidGovernance.getConfigAsUint256. That transformation is a call to SuperfluidGovernanceConfigs.getAppRegistrationConfigKey [code] which simply does a hash of the deployer’s address and the registration key.

Cool, but how do we get access to the Governance contract? Well, the Host contract has a getGovernance function! [code]

We have the main pieces now, let’s assemble things!

const HOST_ABI = ["function getGovernance() external view returns (address)"];
const HOST_ADDR = "0x567c4B141ED61923967cA25Ef4906C8781069a10";

function getHost(hostAddr, providerOrSigner) {
  const hostInstance = new ethers.Contract(hostAddr, HOST_ABI, providerOrSigner);
  return hostInstance;
}
  • Define a function to get an instance of the Governance contract
const GOV_II_ABI = [
  "function setConfig(address host, address superToken, bytes32 key, uint256 value) external",
  "function setAppRegistrationKey(address host, address deployer, string memory registrationKey, uint256 expirationTs) external",
  "function getConfigAsUint256(address host, address superToken, bytes32 key) external view returns (uint256 value)",
  "function verifyAppRegistrationKey(address host, address deployer, string memory registrationKey) external view returns (bool validNow, uint256 expirationTs)",
  "function owner() public view returns (address)",
];

function getGovernance(govAddr, providerOrSigner) {
  const govInstance = new ethers.Contract(
    govAddr,
    GOV_II_ABI,
    providerOrSigner
  );

  return govInstance;
}
  • Define some helper functions
const { network } = require("hardhat");
const { hexValue } = require("@ethersproject/bytes");
const { parseEther } = require("@ethersproject/units");

async function impersonate(addr) {
  await network.provider.request({
    method: "hardhat_impersonateAccount",
    params: [addr],
  });

  await network.provider.send("hardhat_setBalance", [
    addr,
    hexValue(parseEther("1000000")),
  ]);

  return await ethers.getSigner(addr);
}

function getConfigKey(deployerAddr, registrationKey) { 
  return ethers.utils.keccak256(
    ethers.utils.defaultAbiCoder.encode(
      ["string", "address", "string"],
      [
        "org.superfluid-finance.superfluid.appWhiteListing.registrationKey",
        deployerAddr,
        registrationKey,
      ]
    )
  );
}
  • Define the function to set the registration key, and another to verify it
async function setRegistrationKey(
  hostAddr,
  govAddr,
  govOwnerAddr,
  deployerAddr
) {
  // impersonate the contract owner so we can modify things (also funds it with some balance)
  const govOwnerSigner = await impersonate(govOwnerAddr);

  // get the superfluid governance instance
  const govInstance = getGovernance(govAddr, govOwnerSigner);

  // generate a registration key, and pack it up
  const registrationKey = `GM-${Date.now()}`;
  const configKey = getConfigKey(deployerAddr, registrationKey);
  let tx = await govInstance.setConfig(
    hostAddr,
    "0x0000000000000000000000000000000000000000",
    configKey,
    Math.floor(Date.now() / 1000) + 3600 * 24 * 180 // 180 day expiration
  );
  await tx.wait();

  return registrationKey;
}

async function checkRegistrationKey(
  hostAddr,
  govAddr,
  govOwnerAddr,
  deployerAddr,
  registrationKey
) {
  const govOwnerSigner = await impersonate(govOwnerAddr);
  const govInstance = getGovernance(govAddr, govOwnerSigner);

  const configKey = getConfigKey(deployerAddr, registrationKey);
  let r = await govInstance.getConfigAsUint256(
    hostAddr,
    "0x0000000000000000000000000000000000000000",
    configKey
  );
  
  return r;
}
  • Then just tie everything together in your main!
async function main() {
  const signer = await hre.ethers.provider.getSigner();
  const signerAddr = await signer.getAddress();

  const hostInstance = getHost(HOST_ADDR, signer);
  const govAddr = await hostInstance.getGovernance();
  const govInstance = getGovernance(govAddr, signer);
  const govOwnerAddr = await govInstance.owner();

  const registrationKey = await setRegistrationKey(HOST_ADDR, govAddr, govOwnerAddr, signerAddr);

  const r = await checkRegistrationKey(HOST_ADDR, govAddr, govOwnerAddr, signerAddr, registrationKey);

  // THEN YOU DEPLOY YOUR CONTRACT WITH THE REGISTRATION KEY

  const factory = await hre.ethers.getContractFactory("MySuperApp");
  const mySuperApp = await factory.deploy(HOST_ADDR, registrationKey);
  await mySuperApp.deployed();
  console.log("MySuperApp contract deployed to", mySuperApp.address);
}

And that’s it, you can now run the script against your Mainnet Fork chain (npx hardhat run --network localhost scripts/deploy-my-super-app.js), and you should be successful in deploying your SuperApp and registering it with Superfluid!

Here’s an example repo:

#WAGMI

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.