Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
307845a
feat(wip): foundry init, add interface for CrossChainDeployAdapter
stonecharioteer Jan 16, 2024
716c3de
feat(wip): update computeContractAddress fn definition
stonecharioteer Jan 16, 2024
6b217ae
fix(wip): typo with interfaces, don't use {}
stonecharioteer Jan 16, 2024
8e33f39
fix(wip): fix deploy interface definition
stonecharioteer Jan 16, 2024
fd7434c
feat: add CrossChainDeployScript.sol, update the interface with the d…
stonecharioteer Jan 19, 2024
4c2d7a6
chore: rename CrosschainDeployScript file and contract for consistenc…
stonecharioteer Jan 19, 2024
bd519d5
feat(wip): fix function calls and type for callData
stonecharioteer Jan 19, 2024
3636d7a
chore(wip): remove FIXME
stonecharioteer Jan 19, 2024
40f2c07
feat(wip): update justfile with some more planned tools
stonecharioteer Jan 19, 2024
88bc0b1
feat(wip): add `value` to `deploy` call so that it gets the payment.
stonecharioteer Jan 19, 2024
ea929d5
feat(wip): track deployment targets using an array and store the avai…
stonecharioteer Jan 25, 2024
70206c9
feat(wip): cleanup docstrings, fix typos
stonecharioteer Jan 25, 2024
fe7a0da
feat(wip): implement computeAddressForChain
stonecharioteer Jan 25, 2024
06773c5
feat(wip): map constructorArgs and initDatas separately as well
stonecharioteer Jan 25, 2024
367fbb9
feat(wip): use arrays to store a list of constructor args and init da…
stonecharioteer Jan 25, 2024
be4c339
feat(wip): remove unused domain IDs from the constructor
stonecharioteer Jan 25, 2024
60dedd4
chore(wip): remove unnecessary custom error situation
stonecharioteer Jan 29, 2024
444eba6
feat(wip): add generateSalt
stonecharioteer Jan 29, 2024
8b12528
chore(wip): move deployment target check to modifier
stonecharioteer Jan 29, 2024
456569e
fix(wip): use generateSalt instead of asking users to provide the salt
stonecharioteer Jan 29, 2024
fd883f4
fix(wip): increment randomness counter
stonecharioteer Jan 29, 2024
f219167
feat(wip): fix function signature for computeContractAddress, and cal…
stonecharioteer Jan 29, 2024
7df5ba6
chore(wip): purge the deployment targets after deploying
stonecharioteer Jan 29, 2024
b4685f3
fix(wip): rename functions for uniformity, fix array reset syntax to …
stonecharioteer Jan 31, 2024
25a70c2
chore(wip): update justfile to use forge's native `watch` flag
stonecharioteer Jan 31, 2024
4c62035
feat(wip): add unit tests and a mock for adapter
stonecharioteer Jan 31, 2024
1919e87
docs(wip): Update README to show how to use this, and also add some d…
stonecharioteer Jan 31, 2024
3ce7924
feat(wip): add integration test, test steps to justfile
stonecharioteer Jan 31, 2024
b6998f8
feat(wip): add support for .env file for the justfile and use it for …
stonecharioteer Jan 31, 2024
b4b6ca5
chore: update justfile to not use private key for integration tests.
stonecharioteer Jan 31, 2024
f82c1e5
feat(wip): fix argument for constructor and initdata
stonecharioteer Feb 1, 2024
b124e3c
feat(wip): fix argument for constructor and initdata
stonecharioteer Feb 1, 2024
e57869d
feat(wip): Move reset steps into a new function so users can choose t…
stonecharioteer Feb 2, 2024
ed494e0
feat(wip): add `vm.expectCall` to ensure contract calls
stonecharioteer Feb 5, 2024
b926e2c
feat(wip): Add multiple deployment targets & args
stonecharioteer Feb 6, 2024
05912cd
docs(wip): update README to show how to encode arguments
stonecharioteer Feb 6, 2024
5105657
Update README.md
stonecharioteer Feb 7, 2024
63bbcb4
docs(wip): update with installation instructions
stonecharioteer Feb 7, 2024
45ccc31
docs(wip): update with link to forge install docs
stonecharioteer Feb 7, 2024
59eafcf
docs(wip): update the usage example to show how to inherit from this …
stonecharioteer Feb 7, 2024
749ef6a
fix: warnings and stack too deep errors. move contract name to deploy…
mpetrunic Feb 8, 2024
73fdcda
update readme
mpetrunic Feb 8, 2024
2581da3
add env, remove contract address calculation, add link to sygma explorer
mpetrunic Feb 8, 2024
e2ef7f5
more unit tests
mpetrunic Feb 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
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=
34 changes: 34 additions & 0 deletions .github/workflows/test.yml
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
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,12 @@ Cargo.lock

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

# editor specifics
.vscode/

# foundry-specific
out/
cache/

.env
3 changes: 3 additions & 0 deletions .gitmodules
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
38 changes: 37 additions & 1 deletion README.md
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
Comment thread
mpetrunic marked this conversation as resolved.

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");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't it make more sense for SampleContract to "extend" CrosschainDeployScript instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anything that's inheriting Script cannot be deployed. It runs locally, which is why the cheatcodes work there. So users would have to do something like use forge script and provide the contract name and the ABI code for the init variables (using cast abi-encode perhaps) to get those values and then pass them on, or use it this way, which seems much cleaner to be honest.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
we don't want CrosschainDeployScript to end up being deployed (just ran locally) so when user is scripting deployment he would create DeployMyContract.sol which will extend ˙ScriptandCrosschainDeployScript` and script deployment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 Script that calls their contract and uses the deploy script to call the upstream contract. I'd have written that here, but I thought this PR was growing big as it is and that could have been separate.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.
6 changes: 6 additions & 0 deletions foundry.toml
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
40 changes: 40 additions & 0 deletions justfile
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
1 change: 1 addition & 0 deletions lib/forge-std
Submodule forge-std added at 36c303
164 changes: 164 additions & 0 deletions src/CrosschainDeployScript.sol
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 {
Comment thread
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)
Comment thread
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?
Comment thread
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]);
Comment thread
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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;
}
}
66 changes: 66 additions & 0 deletions src/interfaces/CrosschainDeployAdapterInterface.sol
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);
}
15 changes: 15 additions & 0 deletions test/SimpleContract.sol
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++;
}
}
Loading