Skip to main content

How to deploy contracts to chains?

Deploy​

Deployments of onchain contracts from Offchain VM is done using a Deployer contract. Lets look at the Deployer contract of SuperToken example to better understand the workflow and code.

deploy

Onchain contract bytecode stored in the Deployer Contract​

The Deployer Contract has two key pieces of code to ensure that onchain deployments are replicable SuperToken's creationCode with constructor parameters is stored in a mapping. This stored code is used for deploying the token to the underlying chains and written in the constructor.

creationCodeWithArgs[superToken] = abi.encodePacked(
type(superToken).creationCode,
abi.encode(name_, symbol_, decimals_)
);

Using bytes32 variable is use a unique identifier for the SuperToken contract generated using the _createContractId function. This identifier allows us to fetch creationCode, onchain addresses and forwarder addresses from maps in AppGatewayBase. See here to know more about forwarder addresses.

bytes32 public superToken = _createContractId("superToken");

While this example handles a single contract, you can extend it to manage multiple contracts by storing their creation codes.

Onchain contract deployment with the Deployer Contract​

deployment flow

The deployContracts function takes a chainSlug as an argument that specifies the chain where the contract should be deployed.

function deployContracts(uint32 chainSlug) external async {
_deploy(superToken, chainSlug);
}

It calls the inherited _deploy function and uses the async modifier for interacting with underlying chains.

The initialize function is empty in this example. You can use it for setting chain-specific or dynamic variables after deployment if needed.

Initialize​

Since we store the creationCode along with constructor parameters, they essentially become constants. But there can be use cases where the contract need dynamic or chain specific values while setting up. For such cases, the initialize flow has to be used. Lets extend the SuperToken example to set mint limits following the workflow below.

deploy initialize
contract SuperToken is ERC20, Ownable {
(...)
error ExceedsMintLimit(uint256 amount, uint256 limit);

uint256 mintLimit;

function mint(address to_, uint256 amount_) external onlyOwner {
if (amount_ > mintLimit) revert ExceedsMintLimit(amount_, mintLimit);
_mint(to_, amount_);
}

function setMintLimit(uint256 newLimit) external onlyOwner {
mintLimit = newLimit;
}
}

We will set this limit using the initialize function, and to make things a bit more dynamic, we will set a higher limit for Ethereum compared to chains.

interface ISuperToken {
function setMintLimit(uint256 newLimit) external;
}

contract SuperTokenDeployer is AppDeployerBase {
(...)

function initialize(uint32 chainSlug) public override async {
uint256 mintLimit;
if (chainSlug == 1) {
mintLimit = 10 ether;
} else {
mintLimit = 1 ether;
}

ISuperToken(forwarderAddresses[superToken][chainSlug]).setMintLimit(mintLimit);
}
}

The initialize function follows similar flow to how demonstrated on Calling onchain smart contracts using the async modifier and forwarderAddress.

info

You can also note that the forwarder addresses of deployed contracts are stored in forwarderAddresses mapping in the AppDeployerBase and can be accessed easily here.

Deploy multiple contracts​

So far we have been working with a single SuperToken contract onchain. But the deployer also supports working with multiple contracts. Lets create SuperTokenVault to lock tokens on chain and extend the deployer to deploy both contracts.

contract SuperTokenVault is Ownable {
address public superToken;
mapping(address => uint256) public lockedAmount;

function setSuperToken(address superToken_) external onlyOwner {
superToken = superToken_;
}

function lock(uint256 amount) external {
SuperToken(superToken).transferFrom(msg.sender, address(this), amount);
lockedAmount[msg.sender] += amount;
}

function unlock(uint256 amount) external {
lockedAmount[msg.sender] -= amount;
SuperToken(superToken).transfer(msg.sender, amount);
}
}

This contract needs to be onchain, therefore lets change SuperTokenDeployer to include it as well.

contract SuperTokenDeployer is AppDeployerBase {
(...)
bytes32 public superTokenVault = _createContractId("superTokenVault");

constructor(
address addressResolver_,
FeesData memory feesData_,
string calldata name_,
string calldata symbol_,
uint8 decimals_
) AppDeployerBase(addressResolver_, feesData_) {
(...)
creationCodeWithArgs[superTokenVault] = type(SuperTokenVault).creationCode;
}

function deployContracts(uint32 chainSlug) external async {
_deploy(superToken, chainSlug);
_deploy(superTokenVault, chainSlug);
}

function initialize(uint32 chainSlug) public override async {
address superTokenVaultForwarder = forwarderAddresses[superTokenVault][chainSlug];
address superTokenOnChainAddress = getOnChainAddress(superToken, chainSlug);

SuperTokenVault(superTokenVaultForwarder).setSuperToken(superTokenOnChainAddress);
}
}

This SuperTokenDeployer deploys both contracts, sets limit on SuperToken and sets SuperToken’s onchain address on SuperTokenVault.

info
  • SuperTokenVault doesn't have any constructor arguments. Therefore we can directly store its creationCode without encoding anything along with it.
  • We can get the forwarder addresses of both these from forwarderAddresses mapping.
  • Since SuperTokenVault locks SuperToken, its needs the token’s onchain address. This address can be fetched using getOnChainAddress function.