Sitemap
Justin Jordan

Justin Jordan

Blockchain Developer

Easily Deploy Gas Station Network Locally

7 min readFeb 27, 2024

So I’ve been trying to deploy the Gas Station Network (GSN) contracts on my dev machine to test some ideas with meta-transactions, but it seems like I am having difficulty with all the available guides.

These guides span multiple versions across workshops, OpenZeppelin tutorials and even the actual deployment files within OpenGSN’s monorepo.

Seems like there’s always some obstacle I run into that is difficult to overcome, be it compiler issues, missing config vars, dependency/version conflicts, or what have you.

I decided to create a simple, easy to use, deployment repo, that will include the deployment and initial configuration for all the platform’s required contracts, all in single file that is clean, concise and current.

If you have not heard of GSN, you can read about it here. It’s a decentralized network of relayers that can accept signed messages that conform to a specific format, which allow for execution of on-chain operations without the need for the sender/signer of the message to pay for the gas (not directly anyways). Essentially gasless meta-transactions.

There are a number of smart contracts that are deployed on each of the supported Ethereum-based networks, which manage participation, payments, verification and even penalization within the GSN ecosystem.

Here’s a quick list of the contracts involved:

  • Forwarder
  • Penalizer
  • StakeManager
  • RelayRegistrar
  • RelayHub
  • Paymaster

You can read summaries about what each contract does here, and you can review the source code for the latest versions here.

Okay, let’s get into the details of the deployment…

We start with a cookie-cutter hardhat deployment script:

import { ethers } from "hardhat";

async function main() {
// code here
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

Now, we can start inserting the deployment code in the main function. Start by exposing the deployer from the ethers instance, and then deploying the first contract, which is just a test token that will be used for staking (I guess I didn’t mention it above, but Relayers need to stake supported tokens in order to discourage them from misbehaving — hopefully you read about it in GSN docs):

const [deployer] = await ethers.getSigners();

// DEPLOY STAKING TOKEN
const stakingToken = await ethers.deployContract('TestToken', []);
await stakingToken.waitForDeployment();
console.log(`Test Token (for staking) deployed to ${stakingToken.target}`);

Now that the test token is deployed, we can deploy the Forwarder contract:

//  DEPLOY FORWARDER
const forwarder = await ethers.deployContract("Forwarder", []);
await forwarder.waitForDeployment();
console.log(`Forwarder deployed to ${forwarder.target}`);

We are gonna send a couple txns to the Forwarder contract to register a Request Type and a Domain Separator:

//  FORWARDER - REGISTER REQUEST TYPE
console.log(`Registering request type for Forwarder...`);
await forwarder.registerRequestType(GsnRequestType.typeName, GsnRequestType.typeSuffix);

// FORWARDER - REGISTER DOMAIN SEPARATOR
console.log(`Registering domain separator for Forwarder...`);

await forwarder.registerDomainSeparator(
defaultGsnConfigPartial.domainSeparatorName,
GsnDomainSeparatorType.version
);

Next we will deploy the Penalizer contract, using config settings found in the actual Penalizer contract deployed to Polygon mainnet:

//  DEPLOY PENALIZER
const penalizer = await ethers.deployContract("Penalizer", [
'5', // _penalizeBlockDelay used in polygon
'5', // _penalizeBlockExpiration used in polygon
]);

await penalizer.waitForDeployment();
console.log(`Penalizer deployed to ${penalizer.target}`);

Deployment will then proceed with the StakeManager contract, once again using settings from Polygon:

//  DEPLOY STAKEMANAGER
const stakeManager = await ethers.deployContract("StakeManager", [
'100000000', // _maxUnstakeDelay
'1000', // _abandonmentDelay used in polygon
'500', // _escheatmentDelay used in polygon
deployer.address,
deployer.address,
]);

await stakeManager.waitForDeployment();
console.log(`StakeManager deployed to ${stakeManager.target}`);

Next the RelayRegistrar — you guessed it, using settings from Polygon:

//  DEPLOY RELAYREGISTRAR
const relayRegistrar = await ethers.deployContract("RelayRegistrar", [
'15552000', // _relayRegistrationMaxAge used in polygon
]);

await relayRegistrar.waitForDeployment();
console.log(`RelayRegistrar deployed to ${relayRegistrar.target}`);

Now we can deploy the RelayHub, which is the main interface through which the various parties will interact with the system:

//  DEPLOY RELAYHUB
const relayHub = await ethers.deployContract("RelayHub", [
stakeManager.target,
penalizer.target,
ZERO_ADDRESS,
relayRegistrar.target,
{
...POLYGON_RELAY_HUB_CONFIG,
devAddress: deployer.address,
devFee: 0, // polygon = 10
}
]);

await relayHub.waitForDeployment();
console.log(`RelayHub deployed to ${relayHub.target}`);

The object POLYGON_RELAY_HUB_CONFIG was imported from another file and defined as:

export const POLYGON_RELAY_HUB_CONFIG: any = {
maxWorkerCount: 10,
gasReserve: 100000,
postOverhead: 17516,
gasOverhead: 55909,
minimumUnstakeDelay: 15000,
devAddress: '0xd21934eD8eAf27a67f0A70042Af50A1D6d195E81',
devFee: 10,
baseRelayFee: '0',
pctRelayFee: 30,
}

Next we will configure the RelayHub to use the test token as the staking token, and require a minimum of 100 tokens staked:

//  RELAYHUB - SET STAKING TOKEN CONFIG
console.log(`Setting staking token config for RelayHub - min stake for Test Token is 100 TEST...`);
await relayHub.setMinimumStakes([stakingToken.target], [ONE_HUNDRED_ETHER]);

Another field we need to set is the devAddress fo the StakeManager contract:

//  STAKEMANAGER - SETTING DEV ADDRESS
console.log(`Setting devAddress for StakeManager to ${deployer.address}...`);
tawait stakeManager.setDevAddress(deployer.address);

The final contract we deploy is the Paymaster:

//  DEPLOY PAYMASTER
const payMaster = await ethers.deployContract("TestPaymasterEverythingAccepted", []);
await payMaster.waitForDeployment();
console.log(`PayMaster deployed to ${payMaster.target}`);

And then we set a couple fields within the Paymaster contract:

//  PAYMASTER - SETTING RELAYHUB
console.log(`Setting relayHub for PayMaster to ${relayHub.target}...`);
await payMaster.setRelayHub(relayHub.target);

// PAYMASTER - SETTING TRUSTED FORWARDER
console.log(`Setting _trustedForwarder for PayMaster to ${forwarder.target}...`);
await payMaster.setTrustedForwarder(forwarder.target);

And finally, we deposit funds in the RelayHub for our Paymaster:

//  RELAYHUB - DEPOSITING FUNDS FOR PAYMASTER
console.log(`Depositing funds for Paymaster in RelayHub...`);

await relayHub.depositFor(payMaster.target, {
value: ethers.parseEther("1.0"),
});

A final step in the deployment scripts found within GSN’s repo included printing of the relay info, so I included tha here as well:

//  PRINT RELAY INFO
printRelayInfo(
String(relayHub.target),
deployer.address,
hre.network.config.url,
hre.hardhatArguments.network,
String(stakingToken.target),
ONE_HUNDRED_ETHER,
);

This called an imported function that was defined as:

export function printRelayInfo (
hubAddress: string,
deployerAddress: string,
networkUrl: string,
network: string, // hre.hardhatArguments.network
stakingTokenAddress: string,
stakingTokenValue: string,
): void {
console.log('\nExample for Relayer config JSON file:');

console.log(JSON.stringify({
relayHubAddress: hubAddress,
ownerAddress: deployerAddress,
managerStakeTokenAddress: stakingTokenAddress,
gasPriceFactor: 1,
maxFeePerGas: 1e12,
ethereumNodeUrl: networkUrl
}, null, 2));

console.log('\nRegister your Relay Server:');
console.log('Go to https://relays.opengsn.org/ and register your relay server with the web app, or use the following CLI command:');
console.log(`gsn relayer-register -m $SECRET --network ${networkUrl} --relayUrl https://${network}.v3.opengsn.org/v3 --token ${stakingTokenAddress} --stake ${stakingTokenValue} --wrap`);
}

And when calling this function after all the deployments and configuration, the output looks something like this:

Example for Relayer config JSON file:
{
"relayHubAddress": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
"ownerAddress": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"managerStakeTokenAddress": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
"gasPriceFactor": 1,
"maxFeePerGas": 1000000000000,
"ethereumNodeUrl": "http://127.0.0.1:8545"
}

Register your Relay Server:
Go to https://relays.opengsn.org/ and register your relay server with the web app, or use the following CLI command:
gsn relayer-register -m $SECRET --network http://127.0.0.1:8545 --relayUrl https://localhost.v3.opengsn.org/v3 --token 0x5FbDB2315678afecb367f032d93F642f64180aa3 --stake 100000000000000000000 --wrap

Here’s the complete script:

import { ethers } from "hardhat";
const hre = require("hardhat");

import {
defaultGsnConfigPartial,
GsnDomainSeparatorType,
GsnRequestType,
POLYGON_RELAY_HUB_CONFIG,
ZERO_ADDRESS
} from "./constants";

import {printRelayInfo} from "./utils";
const ONE_HUNDRED_ETHER = ethers.parseEther("100").toString();

async function main() {
const [deployer] = await ethers.getSigners();

// ===========================================================================
//
// DEPLOY STAKING TOKEN
//
// ===========================================================================

const stakingToken = await ethers.deployContract('TestToken', []);
await stakingToken.waitForDeployment();
console.log(`Test Token (for staking) deployed to ${stakingToken.target}`);

// ===========================================================================
//
// DEPLOY FORWARDER
//
// ===========================================================================

const forwarder = await ethers.deployContract("Forwarder", []);
await forwarder.waitForDeployment();
console.log(`Forwarder deployed to ${forwarder.target}`);

// ===========================================================================
//
// FORWARDER - REGISTER REQUEST TYPE
//
// ===========================================================================

console.log(`Registering request type for Forwarder...`);
await forwarder.registerRequestType(GsnRequestType.typeName, GsnRequestType.typeSuffix);

// ===========================================================================
//
// FORWARDER - REGISTER DOMAIN SEPARATOR
//
// ===========================================================================

console.log(`Registering domain separator for Forwarder...`);

await forwarder.registerDomainSeparator(
defaultGsnConfigPartial.domainSeparatorName,
GsnDomainSeparatorType.version
);

// ===========================================================================
//
// DEPLOY PENALIZER
//
// ===========================================================================

const penalizer = await ethers.deployContract("Penalizer", [
'5', // _penalizeBlockDelay used in polygon
'5', // _penalizeBlockExpiration used in polygon
]);

await penalizer.waitForDeployment();
console.log(`Penalizer deployed to ${penalizer.target}`);

// ===========================================================================
//
// DEPLOY STAKEMANAGER
//
// ===========================================================================

const stakeManager = await ethers.deployContract("StakeManager", [
'100000000', // _maxUnstakeDelay
'1000', // _abandonmentDelay used in polygon
'500', // _escheatmentDelay used in polygon
deployer.address,
deployer.address,
]);

await stakeManager.waitForDeployment();
console.log(`StakeManager deployed to ${stakeManager.target}`);

// ===========================================================================
//
// DEPLOY RELAYREGISTRAR
//
// ===========================================================================

const relayRegistrar = await ethers.deployContract("RelayRegistrar", [
'15552000', // _relayRegistrationMaxAge used in polygon
]);

await relayRegistrar.waitForDeployment();
console.log(`RelayRegistrar deployed to ${relayRegistrar.target}`);

// ===========================================================================
//
// DEPLOY RELAYHUB
//
// ===========================================================================

const relayHub = await ethers.deployContract("RelayHub", [
stakeManager.target,
penalizer.target,
ZERO_ADDRESS,
relayRegistrar.target,
{
...POLYGON_RELAY_HUB_CONFIG,
devAddress: deployer.address,
devFee: 0, // polygon = 10
}
]);

await relayHub.waitForDeployment();
console.log(`RelayHub deployed to ${relayHub.target}`);

// ===========================================================================
//
// RELAYHUB - SET STAKING TOKEN CONFIG
//
// ===========================================================================

console.log(`Setting staking token config for RelayHub - min stake for Test Token is 100 TEST...`);
await relayHub.setMinimumStakes([stakingToken.target], [ONE_HUNDRED_ETHER]);

// ===========================================================================
//
// STAKEMANAGER - SETTING DEV ADDRESS
//
// ===========================================================================

console.log(`Setting devAddress for StakeManager to ${deployer.address}...`);
await stakeManager.setDevAddress(deployer.address);

// ===========================================================================
//
// DEPLOY PAYMASTER
//
// ===========================================================================

const payMaster = await ethers.deployContract("TestPaymasterEverythingAccepted", []);
await payMaster.waitForDeployment();
console.log(`PayMaster deployed to ${payMaster.target}`);

// ===========================================================================
//
// PAYMASTER - SETTING RELAYHUB
//
// ===========================================================================

console.log(`Setting relayHub for PayMaster to ${relayHub.target}...`);
await payMaster.setRelayHub(relayHub.target);

// ===========================================================================
//
// PAYMASTER - SETTING TRUSTED FORWARDER
//
// ===========================================================================

console.log(`Setting _trustedForwarder for PayMaster to ${forwarder.target}...`);
await payMaster.setTrustedForwarder(forwarder.target);

// ===========================================================================
//
// RELAYHUB - DEPOSITING FUNDS FOR PAYMASTER
//
// ===========================================================================

console.log(`Depositing funds for Paymaster in RelayHub...`);

await relayHub.depositFor(payMaster.target, {
value: ethers.parseEther("1.0"),
});

// ===========================================================================
//
// PRINT RELAY INFO
//
// ===========================================================================

printRelayInfo(
String(relayHub.target),
deployer.address,
hre.network.config.url,
hre.hardhatArguments.network,
String(stakingToken.target),
ONE_HUNDRED_ETHER,
);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

Sorry for the expanded comment blocks — I like to use them visually segregate my code into logical sections.

Get Justin Jordan’s stories in your inbox

Join Medium for free to get updates from this writer.

Note that this script was saved into a standard typescript hardhat project, which included all the contracts mentioned in the above link and the following project dependencies:

"devDependencies": {
"@nomicfoundation/hardhat-toolbox": "^4.0.0",
"hardhat": "^2.20.1"
},
"dependencies": {
"@openzeppelin/contracts": "^4.2.0"
}

And the following hardhat config file:

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";

const config: HardhatUserConfig = {
solidity: {
version: "0.8.7",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
evmVersion: 'london',
},
},
};

export default config;

Note that I had made subtle changes to some of the smart contracts to make sure they compiled. You can view the changes in the repository with the complete project here.

You can easily clone and run the examples in the repo like this:

First clone the repo:

git clone https://github.com/jjordan-quantum/gsn-contract-deployments.git

Install deps:

npm i

note: only tested on windows using node v16.20.0 and npm 8.19.4

Compile contracts:

npm run compile

Try deployment using built-in hardhat network:

npm run deploy

Try deploying to target network:

—> first run the hardhat network in a separate terminal:

npx hardhat node

→ run the deploy script using hardhat’s localhost network option

npm run deploy:local

Run tests:

note: tests adapted from here

npm run test

Any questions or comments, or maybe you have a better guide, or I missed some steps in the code snippets that were included in the completed file, feel free to reach out to me on LinkedIn.

Cheers

--

--