-
Notifications
You must be signed in to change notification settings - Fork 4
Add interface for CrosschainDeployAdapter #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 32 commits
307845a
716c3de
6b217ae
8e33f39
fd7434c
4c2d7a6
bd519d5
3636d7a
40f2c07
88bc0b1
ea929d5
70206c9
fe7a0da
06773c5
367fbb9
be4c339
60dedd4
444eba6
8b12528
456569e
fd883f4
f219167
7df5ba6
b4685f3
25a70c2
4c62035
1919e87
3ce7924
b6998f8
b4b6ca5
f82c1e5
b124e3c
e57869d
ed494e0
b926e2c
05912cd
5105657
63bbcb4
45ccc31
59eafcf
749ef6a
73fdcda
2581da3
e2ef7f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # copy this to a `.env` file and set these values. | ||
| INTEGRATION_FORK_URL= | ||
| INTEGRATION_PRIVATE_KEY= |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| name: test | ||
|
|
||
| on: workflow_dispatch | ||
|
|
||
| env: | ||
| FOUNDRY_PROFILE: ci | ||
|
|
||
| jobs: | ||
| check: | ||
| strategy: | ||
| fail-fast: true | ||
|
|
||
| name: Foundry project | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| submodules: recursive | ||
|
|
||
| - name: Install Foundry | ||
| uses: foundry-rs/foundry-toolchain@v1 | ||
| with: | ||
| version: nightly | ||
|
|
||
| - name: Run Forge build | ||
| run: | | ||
| forge --version | ||
| forge build --sizes | ||
| id: build | ||
|
|
||
| - name: Run Forge tests | ||
| run: | | ||
| forge test -vvv | ||
| id: test |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| [submodule "lib/forge-std"] | ||
| path = lib/forge-std | ||
| url = https://github.com/foundry-rs/forge-std |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,37 @@ | ||
| # foundry-multichain-deploy | ||
| # foundry-multichain-deploy | ||
|
|
||
| Provides `foundry` tooling for the multichain deployment contract built atop of Sygma. See [ChainSafe/hardhat-plugin-multichain-deploy]("https://github.com/ChainSafe/hardhat-plugin-multichain-deploy") for more details. | ||
|
|
||
| ## Usage | ||
|
|
||
| The `CrosschainDeployScript` contract is a foundry "script", which means that it is not really deployed onto the blockchain. It provides a few helper methods that make it easier to deal with the `CrosschainDeployAdapter` from the hardhat repository. | ||
|
|
||
| To use it, first import the `CrosschainDeployScript`. | ||
|
|
||
| ```solidity | ||
| // SPDX-License-Identifier: LGPL-3.0-only | ||
| pragma solidity 0.8.20 | ||
|
|
||
| contract SampleContract { | ||
| function deployMultichain public payable() { | ||
| // Remember that forge "builds" the contracts and stores them and their ABI in the root level of `out` so you'd just need to use the contract file name and the contract name and forge gets it from the ABI. | ||
| CrosschainDeployScript crosschainDeployScript = new CrosschainDeployScript("SimpleContract.sol:SimpleContract"); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't it make more sense for SampleContract to "extend" CrosschainDeployScript instead?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Anything that's inheriting
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But foundry recommends deploying using Script: https://book.getfoundry.sh/tutorials/solidity-scripting#writing-the-script
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm confused I think. Isn't that what the test case shows? I'll write up the script in the next PR that shows how users will be using this library, where they will be making a
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm just disagreen with the sample here that instantiates CrosschainDeployScript instead of extending it |
||
| bytes memory constructorArgs = "0x"; | ||
| bytes memory initData = "0x"; | ||
| crosschainDeployScript.setCrosschainDeployContractAddress(crosschainDeployAdapterAddress); | ||
| crosschainDeployScript.addDeploymentTarget("sepolia", constructorArgs, initData); | ||
| crosschainDeployScript.deploy{value: msg.value}(50000, false); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| A good example of how to use this project is demonstrated in the [`test/unit/CrosschainDeployScript.t.sol`](test/unit/CrosschainDeployScriptTest.t.sol) file. | ||
|
|
||
|
|
||
| ## Development | ||
|
|
||
| [Install foundry](https://book.getfoundry.sh/getting-started/installation) and [`just`](https://github.com/casey/just). | ||
|
|
||
| Check the `justfile` for more instructions on how to run this project. Run `just --list` to see all the options. | ||
|
|
||
| Note that all integration tests *should* have `Integration` in the test function name for them to work, unless you'd like to use `--match-test` specifically for those tests. However, to keep things simple, it's best to follow this practice. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| [profile.default] | ||
| src = "src" | ||
| out = "out" | ||
| libs = ["lib"] | ||
|
|
||
| # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| set shell:=["bash", "-uc"] | ||
| set dotenv-load | ||
|
|
||
| # build the contracts | ||
| build: | ||
| forge build | ||
|
|
||
| # format source | ||
| fmt: | ||
| forge fmt | ||
|
|
||
| # run unit tests | ||
| test: | ||
| forge test --no-match-test Integration | ||
|
|
||
| # run integration tests, needs --fork-url | ||
| integration-test: | ||
| set -x | ||
| forge test --mt Integration --fork-url $INTEGRATION_FORK_URL -vvv | ||
|
|
||
| # watches the directory for changes and rebuilds. | ||
| watch-build: | ||
| forge build --watch | ||
|
|
||
| deploy-anvil: build | ||
| echo "Unimplemented" >&2 | ||
| exit 1 | ||
|
|
||
| deploy-sepolia: build | ||
| echo "Unimplemented" >&2 | ||
| exit 1 | ||
|
|
||
| # Builds locally using docker (useful for debugging dependency issues) | ||
| docker-build: | ||
| echo "Unimplemented" >&2 | ||
| exit 1 | ||
|
|
||
| docker-test: docker-build | ||
| echo "Unimplemented" >&2 | ||
| exit 1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| // SPDX-License-Identifier: LGPL-3.0-only | ||
|
|
||
| pragma solidity 0.8.20; | ||
|
|
||
| import {Script} from "forge-std/Script.sol"; | ||
| import {ICrosschainDeployAdapter} from "./interfaces/CrosschainDeployAdapterInterface.sol"; | ||
|
|
||
| /** | ||
| * @title Provides a script to allow users to call the multichain deployment contract defined in `CrossChainDeployAdapter` from chainsafe/hardhat-plugin-multichain-deploy, passing it the contract bytecode and constructor arguments. | ||
| * @author ChainSafe Systems | ||
| */ | ||
| contract CrosschainDeployScript is Script { | ||
|
mpetrunic marked this conversation as resolved.
|
||
| // this is the string for the contract name, | ||
| string public contractString; | ||
| // this is the address of the original contract defined in chainsafe/hardhat-plugin-multichain-deploy | ||
| // this address is the same across all chains | ||
| address private crosschainDeployContractAddress = 0x85d62AD850B322152BF4ad9147bfBF097DA42217; | ||
|
|
||
| struct NetworkIds { | ||
| uint8 InternalDomainId; | ||
| uint256 ChainId; | ||
| } | ||
|
|
||
| // given a string, obtain the domain ID; | ||
| // https://www.notion.so/chainsafe/Testnet-deployment-0483991cf1ac481593d37baf8d48712a | ||
| mapping(string => NetworkIds) private _stringToNetworkIds; | ||
|
|
||
| // NOTE: All three of these need to be stored in the same order since they've | ||
| // a shared index. Storing them in a mapping isn't gas-efficient since I'd | ||
| // have to loop over these to build these arrays later, and that would not | ||
| // translate to a `bytes[] memory` object, which is what the contract method needs. | ||
| // Explicit conversion is a waste of gas. | ||
| // store the domain IDs | ||
| uint8[] private _domainIds; | ||
| // store the constructor args. | ||
| bytes[] private _constructorArgs; | ||
| // store the init datas; | ||
| bytes[] private _initDatas; | ||
| // store the chain ids | ||
| uint256[] private _chainIds; | ||
|
|
||
| uint8 private _randomCounter; | ||
|
|
||
| /** | ||
| * @notice Constructor, takes the contract name. | ||
| * @param _contractString Contract name in the form of `ContractFile.sol`, if the name of the contract and the file are the same, or `ContractFile.sol:ContractName` if they are different. | ||
| */ | ||
| constructor(string memory _contractString) { | ||
| contractString = _contractString; | ||
| _stringToNetworkIds["goerli"] = NetworkIds(1, 5); | ||
| _stringToNetworkIds["sepolia"] = NetworkIds(2, 11155111); | ||
| _stringToNetworkIds["cronos-testnet"] = NetworkIds(5, 338); | ||
| _stringToNetworkIds["holesky"] = NetworkIds(6, 17000); | ||
| _stringToNetworkIds["mumbai"] = NetworkIds(7, 80001); | ||
| _stringToNetworkIds["arbitrum-sepolia"] = NetworkIds(8, 421614); | ||
| _stringToNetworkIds["gnosis-chiado"] = NetworkIds(9, 10200); | ||
| } | ||
|
|
||
| /** | ||
| * This function will take the network, constructor args and initdata and | ||
| * save these to a mapping. | ||
| */ | ||
| function addDeploymentTarget(string memory deploymentTarget, bytes memory constructorArgs, bytes memory initData) | ||
|
mpetrunic marked this conversation as resolved.
|
||
| public | ||
| { | ||
| NetworkIds memory deploymentTargetNetworkIds = _stringToNetworkIds[deploymentTarget]; | ||
| uint8 deploymentTargetDomainId = deploymentTargetNetworkIds.InternalDomainId; | ||
| require(deploymentTargetDomainId != 0, "Invalid deployment target"); | ||
| _domainIds.push(deploymentTargetDomainId); | ||
| uint256 deploymentTargetChainId = deploymentTargetNetworkIds.ChainId; | ||
| _chainIds.push(deploymentTargetChainId); | ||
| _constructorArgs.push(constructorArgs); | ||
| _initDatas.push(initData); | ||
| } | ||
|
|
||
| /** | ||
| * @notice this function takes in the contract string, in the form that | ||
| * @notice `forge`'s `getCode` takes it, along with some other parameters and passes | ||
| * @notice it along to the `deploy` function of the `CrossChainDeployAdapter` | ||
| * @notice contract. | ||
| * @param gasLimit Contract deploy and init gas. | ||
| * @param isUniquePerChain True to have unique addresses on every chain. | ||
| * Users call this function and pass only the function call string as | ||
| * `MyContract.sol:MyContract`. The function call string is then parsed | ||
| * and the `callData` and `bytesCode` are extracted from it. | ||
| * and the contract is deployed on the other chains. | ||
| */ | ||
| function deploy(uint256 gasLimit, bool isUniquePerChain) | ||
| public | ||
| payable | ||
| hasDeploymentNetworks | ||
| returns (address[] memory) | ||
| { | ||
| // We use the contractString to get the bytecode of the contract, | ||
| // reference: https://book.getfoundry.sh/cheatcodes/get-code | ||
| bytes memory deployByteCode = vm.getCode(contractString); | ||
| bytes32 salt = generateSalt(); | ||
| uint256[] memory fees = ICrosschainDeployAdapter(crosschainDeployContractAddress).calculateDeployFee( | ||
| deployByteCode, gasLimit, salt, isUniquePerChain, _constructorArgs, _initDatas, _domainIds | ||
| ); | ||
| uint256 totalFee; | ||
| uint256 feesArrayLength = fees.length; | ||
| for (uint256 j = 0; j < feesArrayLength; j++) { | ||
| uint256 fee = fees[j]; | ||
| totalFee += fee; | ||
| } | ||
| // TODO: Would I need to check if totalFee < msg.value since this `deploy` is itself payable and called with some fee? | ||
|
stonecharioteer marked this conversation as resolved.
Outdated
|
||
| ICrosschainDeployAdapter(crosschainDeployContractAddress).deploy{value: totalFee}( | ||
| deployByteCode, gasLimit, salt, isUniquePerChain, _constructorArgs, _initDatas, _domainIds, fees | ||
| ); | ||
| address[] memory contractAddresses = new address[](_chainIds.length); | ||
| for (uint256 k = 0; k < _chainIds.length; k++) { | ||
| address contractAddress = ICrosschainDeployAdapter(crosschainDeployContractAddress) | ||
| .computeContractAddressForChain(msg.sender, salt, isUniquePerChain, _chainIds[k]); | ||
|
mpetrunic marked this conversation as resolved.
Outdated
|
||
| contractAddresses[k] = contractAddress; | ||
| } | ||
| // purge the deployment targets now. | ||
| delete _chainIds; | ||
| delete _constructorArgs; | ||
| delete _domainIds; | ||
| delete _initDatas; | ||
|
|
||
| return contractAddresses; | ||
| } | ||
|
|
||
| // returns a pseudorandom bytes32 | ||
| function generateSalt() public returns (bytes32) { | ||
| _randomCounter++; | ||
| return keccak256(abi.encodePacked(block.prevrandao, block.timestamp, msg.sender, _randomCounter)); | ||
| } | ||
|
|
||
| modifier hasDeploymentNetworks() { | ||
| // check that the user has added deployment networks by calling `addDeploymentNetwork` | ||
| uint256 deploymentNetworksCount = _domainIds.length; | ||
| require(deploymentNetworksCount > 0, "Need to add deployment networks. Use `addDeploymentNetwork` first"); | ||
| _; | ||
| } | ||
|
|
||
| /** | ||
| * @notice Computes the address where the contract will be deployed on this chain. | ||
| * @param sender Address that requested deploy. | ||
| * @param salt Entropy for contract address generation. | ||
| * @param isUniquePerChain True to have unique addresses on every chain. | ||
| * @param chainId the ID of the chain on which to deploy the contract | ||
| * @return Address where the contract will be deployed on this chain. | ||
| */ | ||
| function computeAddressForChain(address sender, bytes32 salt, bool isUniquePerChain, uint256 chainId) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe also useful to have this function with network name instead of chainId?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. YEah, that's a good idea. Changed it. |
||
| external | ||
| view | ||
| returns (address) | ||
| { | ||
| return ICrosschainDeployAdapter(crosschainDeployContractAddress).computeContractAddressForChain( | ||
| sender, salt, isUniquePerChain, chainId | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * This is a function we only need for tests. | ||
| * TODO: Figure out a safer way of keeping this visible. | ||
| */ | ||
| function setCrosschainDeployContractAddress(address _crosschainDeployContractAddress) public { | ||
| crosschainDeployContractAddress = _crosschainDeployContractAddress; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| // SPDX-License-Identifier: LGPL-3.0-only | ||
|
|
||
| pragma solidity 0.8.20; | ||
|
|
||
| /** | ||
| * @title Provides an interface to the CrosschainDeployAdapter from chainsafe/hardhat-plugin-multichain-deploy | ||
| * @author ChainSafe Systems. | ||
| * @notice The original contract in question is intended to be used with the Bridge contract and Permissionless Generic Handler | ||
| */ | ||
| interface ICrosschainDeployAdapter { | ||
| /** | ||
| * @notice Deposits to the Bridge contract using the PermissionlessGenericHandler, | ||
| * @notice to request contract deployments on other chains. | ||
| * @param deployBytecode Contract deploy bytecode. | ||
| * @param gasLimit Contract deploy and init gas. | ||
| * @param salt Entropy for contract address generation. | ||
| * @param isUniquePerChain True to have unique addresses on every chain. | ||
| * @param constructorArgs Bytes to add to the deployBytecode, or empty, one per chain. | ||
| * @param initDatas Bytes to send to the contract after deployment, or empty, one per chain. | ||
| * @param destinationDomainIDs Sygma Domain IDs of target chains. | ||
| * @param fees Native currency amount to pay for Sygma services, one per chain. Empty for current domain. | ||
| */ | ||
| function deploy( | ||
| bytes calldata deployBytecode, | ||
| uint256 gasLimit, | ||
| bytes32 salt, | ||
| bool isUniquePerChain, | ||
| bytes[] memory constructorArgs, | ||
| bytes[] memory initDatas, | ||
| uint8[] memory destinationDomainIDs, | ||
| uint256[] memory fees | ||
| ) external payable; | ||
|
|
||
| /** | ||
| * @notice Computes the address where the contract will be deployed on specified chain. | ||
| * @param sender Address that requested deploy. | ||
| * @param salt Entropy for contract address generation. | ||
| * @param isUniquePerChain True to have unique addresses on every chain. | ||
| * @param chainId The ID of the chain, as shown on https://chainlist.org | ||
| * @return Address where the contract will be deployed on specified chain. | ||
| */ | ||
| function computeContractAddressForChain(address sender, bytes32 salt, bool isUniquePerChain, uint256 chainId) | ||
| external | ||
| view | ||
| returns (address); | ||
|
|
||
| /** | ||
| * @notice Returns total amount of native currency needed for a deploy request. | ||
| * @param deployBytecode Contract deploy bytecode. | ||
| * @param gasLimit Contract deploy and init gas. | ||
| * @param salt Entropy for contract address generation. | ||
| * @param isUniquePerChain True to have unique addresses on every chain. | ||
| * @param constructorArgs Bytes to add to the deployBytecode, or empty, one per chain. | ||
| * @param initDatas Bytes to send to the contract after deployment, or empty, one per chain. | ||
| * @param destinationDomainIDs Sygma Domain IDs of target chains. | ||
| */ | ||
| function calculateDeployFee( | ||
| bytes calldata deployBytecode, | ||
| uint256 gasLimit, | ||
| bytes32 salt, | ||
| bool isUniquePerChain, | ||
| bytes[] memory constructorArgs, | ||
| bytes[] memory initDatas, | ||
| uint8[] memory destinationDomainIDs | ||
| ) external view returns (uint256[] memory fees); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // SPDX-License-Identifier: LGPL-3.0-only | ||
|
|
||
| pragma solidity 0.8.20; | ||
|
|
||
| contract SimpleContract { | ||
| uint256 public count; | ||
|
|
||
| function get() public view returns (uint256) { | ||
| return count; | ||
| } | ||
|
|
||
| function inc() public { | ||
| count++; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.