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.
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.
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.
The registration is done through the ISuperfluid
Host contract’s registerAppWithKey
[code], which then calls _gov.getConfigAsUint256(...)
.
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:
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!
Grab the Host contract’s address (e.g. 0x567c4B141ED61923967cA25Ef4906C8781069a10
for Optimism)
Define a function to get an instance of the Host contract
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;
}
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;
}
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,
]
)
);
}
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;
}
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