-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: code generation for cross-chain messaging (#8)
- Loading branch information
Showing
9 changed files
with
368 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ coverage.json | |
typechain | ||
typechain-types | ||
dist | ||
.DS_Store | ||
|
||
# Hardhat files | ||
cache | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import * as fs from "fs"; | ||
import * as handlebars from "handlebars"; | ||
import * as path from "path"; | ||
|
||
const prepareData = (args: any) => { | ||
const argsList = args.arguments || []; | ||
const names = argsList.map((i: string) => i.split(":")[0]); | ||
const types = argsList.map((i: string) => { | ||
let parts = i.split(":"); | ||
// If there's a type and it's not empty, use it; if not, default to "bytes32" | ||
let t = | ||
parts.length > 1 && parts[1].trim() !== "" ? parts[1].trim() : "bytes32"; | ||
return t; | ||
}); | ||
const pairs = names.map((v: string, i: string) => [v, types[i]]); | ||
const contractName = sanitizeSolidityFunctionName(args.name); | ||
|
||
return { | ||
args, | ||
arguments: { names, pairs, types }, | ||
contractName, | ||
contractNameUnderscore: camelToUnderscoreUpper(contractName), | ||
}; | ||
}; | ||
|
||
const processTemplatesRecursive = async ( | ||
template: string, | ||
outputDir: string, | ||
data: Record<string, unknown> | ||
): Promise<void> => { | ||
try { | ||
const templateDir = path.resolve( | ||
__dirname, | ||
path.resolve(__dirname, "..", "templates", template) | ||
); | ||
|
||
const files = fs.readdirSync(templateDir); | ||
|
||
for (const file of files) { | ||
const templatePath = path.join(templateDir, file); | ||
|
||
// Compiling filename as a template | ||
const filenameTemplate = handlebars.compile(file); | ||
const filename = filenameTemplate(data); | ||
|
||
// Replacing .hbs extension if the file was a handlebars template | ||
const outputPath = path.join(outputDir, filename.replace(".hbs", "")); | ||
|
||
fs.mkdirSync(path.dirname(outputPath), { recursive: true }); | ||
|
||
if (fs.lstatSync(templatePath).isDirectory()) { | ||
// If file is a directory, recursively process it | ||
await processTemplatesRecursive(templatePath, outputPath, data); | ||
} else if (path.extname(file) === ".hbs") { | ||
const templateContent = fs.readFileSync(templatePath, "utf-8"); | ||
const template = handlebars.compile(templateContent); | ||
const outputContent = template(data); | ||
fs.writeFileSync(outputPath, outputContent); | ||
} else { | ||
fs.copyFileSync(templatePath, outputPath); | ||
} | ||
} | ||
} catch (error) { | ||
console.error(`Error processing templates: ${error}`); | ||
} | ||
}; | ||
|
||
export const processTemplates = async (templateName: string, args: any) => { | ||
processTemplatesRecursive( | ||
templateName, | ||
path.resolve(process.cwd()), | ||
prepareData(args) | ||
); | ||
}; | ||
|
||
const camelToUnderscoreUpper = (input: string): string => { | ||
return input.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase(); | ||
}; | ||
|
||
const sanitizeSolidityFunctionName = (str: string): string => { | ||
// Remove any character that's not alphanumeric or underscore | ||
const cleaned = str.replace(/[^a-zA-Z0-9_]/g, ""); | ||
|
||
// If the first character is a digit, prepend with an underscore | ||
return cleaned.match(/^\d/) ? `_${cleaned}` : cleaned; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import * as fs from "fs"; | ||
import { task } from "hardhat/config"; | ||
import { HardhatRuntimeEnvironment } from "hardhat/types"; | ||
import * as path from "path"; | ||
|
||
import { processTemplates } from "../lib"; | ||
|
||
const main = async (args: any, hre: HardhatRuntimeEnvironment) => { | ||
processTemplates("messaging", args); | ||
|
||
const configPath = path.resolve(process.cwd(), "hardhat.config.ts"); | ||
let hardhatConfigContents = fs.readFileSync(configPath, "utf8"); | ||
|
||
// Add the omnichain tasks to the hardhat.config.ts file | ||
["deploy", "interact"].forEach((task) => { | ||
const content = `import "./tasks/${task}";\n`; | ||
if (!hardhatConfigContents.includes(content)) { | ||
hardhatConfigContents = content + hardhatConfigContents; | ||
} | ||
}); | ||
|
||
fs.writeFileSync(configPath, hardhatConfigContents); | ||
}; | ||
|
||
export const messageTask = task( | ||
"message", | ||
"Generate code for a cross-chain messaging contract", | ||
main | ||
) | ||
.addPositionalParam("name", "Name of the contract") | ||
.addOptionalVariadicPositionalParam( | ||
"arguments", | ||
"Arguments for a crosschain call (e.g. dest:address to:bytes32 output:uint256)" | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.7; | ||
|
||
import "@openzeppelin/contracts/interfaces/IERC20.sol"; | ||
import "@openzeppelin/contracts/access/Ownable.sol"; | ||
import "@zetachain/protocol-contracts/contracts/evm/tools/ZetaInteractor.sol"; | ||
import "@zetachain/protocol-contracts/contracts/evm/interfaces/ZetaInterfaces.sol"; | ||
|
||
interface {{contractName}}Errors { | ||
error InvalidMessageType(); | ||
} | ||
|
||
contract {{contractName}} is | ||
ZetaInteractor, | ||
ZetaReceiver, | ||
{{contractName}}Errors | ||
{ | ||
bytes32 public constant {{contractNameUnderscore}}_MESSAGE_TYPE = | ||
keccak256("CROSS_CHAIN_{{contractNameUnderscore}}"); | ||
|
||
event {{contractName}}Event({{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[1]}}{{/each}}); | ||
event {{contractName}}RevertedEvent({{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[1]}}{{/each}}); | ||
|
||
ZetaTokenConsumer private immutable _zetaConsumer; | ||
IERC20 internal immutable _zetaToken; | ||
|
||
constructor( | ||
address connectorAddress, | ||
address zetaTokenAddress, | ||
address zetaConsumerAddress | ||
) ZetaInteractor(connectorAddress) { | ||
_zetaToken = IERC20(zetaTokenAddress); | ||
_zetaConsumer = ZetaTokenConsumer(zetaConsumerAddress); | ||
} | ||
|
||
function sendMessage(uint256 destinationChainId{{#if arguments.pairs}}, {{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[1]}} {{this.[0]}}{{/each}}{{/if}}) external payable { | ||
if (!_isValidChainId(destinationChainId)) | ||
revert InvalidDestinationChainId(); | ||
|
||
uint256 crossChainGas = 18 * (10 ** 18); | ||
uint256 zetaValueAndGas = _zetaConsumer.getZetaFromEth{ | ||
value: msg.value | ||
}(address(this), crossChainGas); | ||
_zetaToken.approve(address(connector), zetaValueAndGas); | ||
|
||
connector.send( | ||
ZetaInterfaces.SendInput({ | ||
destinationChainId: destinationChainId, | ||
destinationAddress: interactorsByChainId[destinationChainId], | ||
destinationGasLimit: 300000, | ||
message: abi.encode({{contractNameUnderscore}}_MESSAGE_TYPE{{#if arguments.pairs}}, {{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[0]}}{{/each}}{{/if}}), | ||
zetaValueAndGas: zetaValueAndGas, | ||
zetaParams: abi.encode("") | ||
}) | ||
); | ||
} | ||
|
||
function onZetaMessage( | ||
ZetaInterfaces.ZetaMessage calldata zetaMessage | ||
) external override isValidMessageCall(zetaMessage) { | ||
/** | ||
* @dev Decode should follow the signature of the message provided to zeta.send. | ||
*/ | ||
(bytes32 messageType{{#if arguments.pairs}}, {{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[1]}} {{this.[0]}}{{/each}}{{/if}}) = abi.decode( | ||
zetaMessage.message, (bytes32{{#if arguments.pairs}}, {{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[1]}}{{/each}}{{/if}}) | ||
); | ||
|
||
/** | ||
* @dev Setting a message type is a useful pattern to distinguish between different messages. | ||
*/ | ||
if (messageType != {{contractNameUnderscore}}_MESSAGE_TYPE) | ||
revert InvalidMessageType(); | ||
|
||
emit {{contractName}}Event({{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[0]}}{{/each}}); | ||
} | ||
|
||
/** | ||
* @dev Called by the Zeta Connector contract when the message fails to be sent. | ||
* Useful to cleanup and leave the application on its initial state. | ||
* Note that the require statements and the functionality are similar to onZetaMessage. | ||
*/ | ||
function onZetaRevert( | ||
ZetaInterfaces.ZetaRevert calldata zetaRevert | ||
) external override isValidRevertCall(zetaRevert) { | ||
(bytes32 messageType{{#if arguments.pairs}}, {{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[1]}} {{this.[0]}}{{/each}}{{/if}}) = abi.decode( | ||
zetaRevert.message, | ||
(bytes32{{#if arguments.pairs}}, {{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[1]}}{{/each}}{{/if}}) | ||
); | ||
|
||
if (messageType != {{contractNameUnderscore}}_MESSAGE_TYPE) | ||
revert InvalidMessageType(); | ||
|
||
emit {{contractName}}RevertedEvent({{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[0]}}{{/each}}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import { getAddress, getChainId } from "@zetachain/addresses"; | ||
import { ethers } from "ethers"; | ||
import { task } from "hardhat/config"; | ||
import { HardhatRuntimeEnvironment } from "hardhat/types"; | ||
|
||
const contractName = "{{contractName}}"; | ||
|
||
const main = async (args: any, hre: HardhatRuntimeEnvironment) => { | ||
const networks = args.networks.split(","); | ||
// A mapping between network names and deployed contract addresses. | ||
const contracts: { [key: string]: string } = {}; | ||
await Promise.all( | ||
networks.map(async (networkName: string) => { | ||
contracts[networkName] = await deployContract(hre, networkName); | ||
}) | ||
); | ||
|
||
for (const source in contracts) { | ||
await setInteractors(hre, source, contracts); | ||
} | ||
}; | ||
|
||
// Initialize a wallet using a network configuration and a private key from | ||
// environment variables. | ||
const initWallet = (hre: HardhatRuntimeEnvironment, networkName: string) => { | ||
const { url } = hre.config.networks[networkName]; | ||
const provider = new ethers.providers.JsonRpcProvider(url); | ||
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY as string, provider); | ||
|
||
return wallet; | ||
}; | ||
|
||
// Deploy the contract on the specified network. deployContract reads the | ||
// contract artifact, creates a contract factory, and deploys the contract using | ||
// that factory. | ||
const deployContract = async ( | ||
hre: HardhatRuntimeEnvironment, | ||
networkName: string | ||
) => { | ||
const wallet = initWallet(hre, networkName); | ||
const zetaNetwork = "athens"; | ||
const connectorAddress = getAddress({ | ||
address: "connector", | ||
networkName, | ||
zetaNetwork, | ||
}); | ||
const zetaTokenAddress = getAddress({ | ||
address: "zetaToken", | ||
networkName, | ||
zetaNetwork, | ||
}); | ||
const zetaTokenConsumerV2 = getAddress({ | ||
address: "zetaTokenConsumerUniV2", | ||
networkName, | ||
zetaNetwork, | ||
}); | ||
const zetaTokenConsumerV3 = getAddress({ | ||
address: "zetaTokenConsumerUniV3", | ||
networkName, | ||
zetaNetwork, | ||
}); | ||
|
||
const { abi, bytecode } = await hre.artifacts.readArtifact(contractName); | ||
const factory = new ethers.ContractFactory(abi, bytecode, wallet); | ||
const contract = await factory.deploy( | ||
connectorAddress, | ||
zetaTokenAddress, | ||
zetaTokenConsumerV2 || zetaTokenConsumerV3 | ||
); | ||
|
||
await contract.deployed(); | ||
console.log(` | ||
🚀 Successfully deployed contract on ${networkName}. | ||
📜 Contract address: ${contract.address} | ||
`); | ||
return contract.address; | ||
}; | ||
|
||
// Set interactors for a contract. setInteractors attaches to the contract | ||
// deployed at the specified address, and for every other network, sets the | ||
// deployed contract's address as an interactor. | ||
const setInteractors = async ( | ||
hre: HardhatRuntimeEnvironment, | ||
source: string, | ||
contracts: { [key: string]: string } | ||
) => { | ||
console.log(` | ||
🔗 Setting interactors for a contract on ${source}`); | ||
const wallet = initWallet(hre, source); | ||
|
||
const { abi, bytecode } = await hre.artifacts.readArtifact(contractName); | ||
const factory = new ethers.ContractFactory(abi, bytecode, wallet); | ||
const contract = factory.attach(contracts[source]); | ||
|
||
for (const counterparty in contracts) { | ||
// Skip the destination network if it's the same as the source network. | ||
// For example, we don't need to set an interactor for a contract on | ||
// Goerli if the destination network is also Goerli. | ||
if (counterparty === source) continue; | ||
|
||
const counterpartyContract = hre.ethers.utils.solidityPack( | ||
["address"], | ||
[contracts[counterparty]] | ||
); | ||
const chainId = getChainId(counterparty as any); | ||
await ( | ||
await contract.setInteractorByChainId(chainId, counterpartyContract) | ||
).wait(); | ||
console.log( | ||
`✅ Interactor address for ${chainId} (${counterparty}) is set to ${counterpartyContract}` | ||
); | ||
} | ||
}; | ||
|
||
task("deploy", "Deploy the contract", main).addParam( | ||
"networks", | ||
"Comma separated list of networks to deploy to" | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { task } from "hardhat/config"; | ||
import { HardhatRuntimeEnvironment } from "hardhat/types"; | ||
import { parseEther } from "@ethersproject/units"; | ||
|
||
const contractName = "{{contractName}}"; | ||
|
||
const main = async (args: any, hre: HardhatRuntimeEnvironment) => { | ||
const [signer] = await hre.ethers.getSigners(); | ||
console.log(`🔑 Using account: ${signer.address}\n`); | ||
|
||
const factory = await hre.ethers.getContractFactory(contractName); | ||
const contract = factory.attach(args.contract); | ||
|
||
const tx = await contract | ||
.connect(signer) | ||
.sendHelloWorld(args.destination, { value: parseEther(args.amount) }); | ||
|
||
const receipt = await tx.wait(); | ||
console.log(`✅ "sendHelloWorld" transaction has been broadcasted to ${hre.network.name} | ||
📝 Transaction hash: ${receipt.transactionHash} | ||
|
||
Please, refer to ZetaChain's explorer for updates on the progress of the cross-chain transaction. | ||
|
||
🌍 Explorer: https://explorer.zetachain.com/cc/tx/${receipt.transactionHash} | ||
`); | ||
}; | ||
|
||
task("interact", "Sends a message from one chain to another.", main) | ||
.addParam("contract", "Contract address") | ||
.addParam("amount", "Token amount to send") | ||
.addParam("destination", "Destination chain ID (integer)"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters