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.
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​
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.
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
.
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
.
SuperTokenVault
doesn't have any constructor arguments. Therefore we can directly store itscreationCode
without encoding anything along with it.- We can get the forwarder addresses of both these from
forwarderAddresses
mapping. - Since
SuperTokenVault
locksSuperToken
, its needs the token’s onchain address. This address can be fetched usinggetOnChainAddress
function.