diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..55d5b7d --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# copy this to a `.env` file and set these values. +INTEGRATION_FORK_URL= +INTEGRATION_PRIVATE_KEY= \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..9282e82 --- /dev/null +++ b/.github/workflows/test.yml @@ -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 diff --git a/.gitignore b/.gitignore index 6985cf1..e5d588f 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..888d42d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/README.md b/README.md index 5b440b2..1043b20 100644 --- a/README.md +++ b/README.md @@ -1 +1,83 @@ -# foundry-multichain-deploy \ No newline at end of file +# foundry-multichain-deploy + +> **Warning** +> +> Only testnet multichain deployment is available. Mainnet deployment will be enabled soon! + +Provides `foundry` tooling for the multichain deployment contract built atop Sygma. See +[ChainSafe/hardhat-plugin-multichain-deploy]("https://github.com/ChainSafe/hardhat-plugin-multichain-deploy") +for the Hardhat plugin version. + +## Installation + +Run `forge install chainsafe/foundry-multichain-deploy` to install this plugin to your own foundry project. You might need to use `--no-commit` so as to properly configure your git working directory and commit the dependency yourself. For further instructions, check [the official documentation.](https://book.getfoundry.sh/projects/dependencies) + +## 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` and inherit from it. + +```solidity +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.20 +import {CrosschainDeployScript} from "foundry-multichain-deploy/src/CrosschainDeployScript.sol"; + +contract SampleDeployScript is CrosschainDeployScript { + + function run { + // Remember that forge "builds" the contracts and stores them and their + // ABI in the root level of the `out` folder so you'd just need to use the contract + // file name and the contract name and forge gets it from the ABI. + bytes memory constructorArgs = abi.encode(uint256("10")); + bytes memory initData = abi.encode("add(uint256)", uint256(10)); + addDeploymentTarget("sepolia", constructorArgs, initData); + addDeploymentTarget("holesky", constructorArgs, initData); + deploy{value: msg.value}("SimpleContract.sol:SimpleContract", 50000, false); + } +} +``` + +Now, you can run this with `forge script script/SampleDeployScript.sol:SampleDeployScript --rpc-url $CHAIN_RPC_URL --broadcast -vvv --verify`. + +This script is not deployed, but it instead constructs the calls to the upstream +contract and broadcasts them (thanks to the `--broadcast` flag). + +A good example of how to use this project is demonstrated in the +[`test/unit/CrosschainDeployScript.t.sol`](test/unit/CrosschainDeployScriptTest.t.sol) +file. + +### Encoding Arguments + +The `constructorArgs` and `initData` arguments use the encoded format for the +values that get passed to the adapter for deployment. Notice how in the example +above, these are encoded using `encode` and `encodePacked`. + +The `SimpleContract` example has a constructor that takes a `uint256` value. So +it requires a value to be passed as `constructorArgs`. We use `bytes memory +constructorArgs = abi.encode(uint256(10));` to do so. + +If a contract constructor doesn't have any input arguments, you can just use +`bytes memory constructorArgs = '';` for that particular constructor. + +Now, the `add` function of the `SimpleContract` takes a `uint256` argument as +well, but to pass this to `initDatas`, you need to pass the function signature +as well. So you'd have to use `bytes memory initData = +abi.encodeWithSignature("add(uint256)", uint256(10));`. If you're calling a +function like `inc()` which takes no arguments, just say `bytes memory initData += abi.encodeWithSignature("inc()");` instead. + +To learn more, check out the ways you can use `abi.encode` and +`abi.encodeWithSignature` in the foundry book. + + +## 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. \ No newline at end of file diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..25b918f --- /dev/null +++ b/foundry.toml @@ -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 diff --git a/justfile b/justfile new file mode 100644 index 0000000..db44853 --- /dev/null +++ b/justfile @@ -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 \ No newline at end of file diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..36c303b --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 36c303b7ffdd842d06b1ec2744c9b9b5fb3083f3 diff --git a/src/CrosschainDeployScript.sol b/src/CrosschainDeployScript.sol new file mode 100644 index 0000000..5088400 --- /dev/null +++ b/src/CrosschainDeployScript.sol @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: LGPL-3.0-only + +pragma solidity 0.8.20; + +import {Script} from "forge-std/Script.sol"; +import "forge-std/console.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 { + // 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; + + enum Env{ UNKNOWN, TESTNET, MAINNET } + + struct NetworkIds { + uint8 InternalDomainId; + uint256 ChainId; + Env env; + } + + // given a string, obtain the domain ID; + // https://www.notion.so/chainsafe/Testnet-deployment-0483991cf1ac481593d37baf8d48712a + mapping(string => NetworkIds) private _stringToNetworkIds; + + Env env = Env.UNKNOWN; + + // 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; + + // use this to store a static value for the salt, one that the user can override using `setSalt`. If set to _anything_ other than 0x00000000000000000000, this will be used as the salt. + bytes32 private _staticSalt = 0x00000000000000000000; + + /** + * @notice Constructor, takes the contract name. + */ + constructor() { + _stringToNetworkIds["goerli"] = NetworkIds(1, 5, Env.TESTNET); + _stringToNetworkIds["sepolia"] = NetworkIds(2, 11155111, Env.TESTNET); + _stringToNetworkIds["cronos-testnet"] = NetworkIds(5, 338, Env.TESTNET); + _stringToNetworkIds["holesky"] = NetworkIds(6, 17000, Env.TESTNET); + _stringToNetworkIds["mumbai"] = NetworkIds(7, 80001, Env.TESTNET); + _stringToNetworkIds["arbitrum-sepolia"] = NetworkIds(8, 421614, Env.TESTNET); + _stringToNetworkIds["gnosis-chiado"] = NetworkIds(9, 10200, Env.TESTNET); + } + + function _convertDeploymentTargetToNetworkIds(string memory deploymentTarget) + private + returns (NetworkIds memory) + { + NetworkIds memory deploymentTargetNetworkIds = _stringToNetworkIds[deploymentTarget]; + if(env == Env.UNKNOWN) { + env = deploymentTargetNetworkIds.env; + } else { + require(env == deploymentTargetNetworkIds.env, "Deployment target is not in the same env as previous deployment targets"); + } + uint8 deploymentTargetDomainId = deploymentTargetNetworkIds.InternalDomainId; + require(deploymentTargetDomainId != 0, "Invalid deployment target"); + return deploymentTargetNetworkIds; + } + + /** + * 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) + public + { + NetworkIds memory networkIds = _convertDeploymentTargetToNetworkIds(deploymentTarget); + _domainIds.push(networkIds.InternalDomainId); + _chainIds.push(networkIds.ChainId); + _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 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. + * @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(string calldata contractString, uint256 gasLimit, bool isUniquePerChain) + public + payable + hasDeploymentNetworks + { + // 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; + if (_staticSalt == 0x00000000000000000000) { + salt = generateSalt(); + } else { + salt = _staticSalt; + } + + 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; + } + ICrosschainDeployAdapter(crosschainDeployContractAddress).deploy{value: totalFee}( + deployByteCode, gasLimit, salt, isUniquePerChain, _constructorArgs, _initDatas, _domainIds, fees + ); + console.log("Due to https://github.com/foundry-rs/foundry/issues/3885, we cannot calculate deployed contract address."); + if(env == Env.MAINNET) { + console.log("You can track deployment progress at https://scan.test.buildwithsygma.com/transfer/"); + } + if(env == Env.TESTNET) { + console.log("You can track deployment progress at https://scan.buildwithsygma.com/transfer/"); + } + // 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]); + // contractAddresses[k] = contractAddress; + // } + resetDeploymentNetworks(); + // return contractAddresses; + } + + // empties the deployment networks added so far. Note that this won't change the contract string. + function resetDeploymentNetworks() public { + // purge the deployment targets now. + delete _chainIds; + delete _constructorArgs; + delete _domainIds; + delete _initDatas; + } + + // resets the static salt + function resetSalt() public { + _staticSalt = 0x00000000000000000000; + } + + // sets the static salt + function setSalt(bytes32 salt) public { + _staticSalt = salt; + } + + // returns a pseudorandom bytes32 for salt *if* _staticSalt is not set. + function generateSalt() public returns (bytes32) { + require( + _staticSalt == 0x00000000000000000000, + "Static salt is set. Use `setSalt` to override it, or use resetSalt to reset it." + ); + _randomCounter++; + return keccak256(abi.encodePacked(block.prevrandao, block.timestamp, msg.sender, _randomCounter)); + } + + // check that the user has added deployment networks by calling `addDeploymentNetwork` + modifier hasDeploymentNetworks() { + 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 deploymentTarget the name of the network onto which to deploy the chain. + * @return Address where the contract will be deployed on this chain. + */ + function computeAddressForChain(address sender, bytes32 salt, bool isUniquePerChain, string memory deploymentTarget) + external + returns (address) + { + NetworkIds memory networkIds = _convertDeploymentTargetToNetworkIds(deploymentTarget); + + return ICrosschainDeployAdapter(crosschainDeployContractAddress).computeContractAddressForChain( + sender, salt, isUniquePerChain, networkIds.InternalDomainId + ); + } + + /** + * 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; + } +} diff --git a/src/interfaces/CrosschainDeployAdapterInterface.sol b/src/interfaces/CrosschainDeployAdapterInterface.sol new file mode 100644 index 0000000..241912a --- /dev/null +++ b/src/interfaces/CrosschainDeployAdapterInterface.sol @@ -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); +} diff --git a/test/SimpleContract.sol b/test/SimpleContract.sol new file mode 100644 index 0000000..cbced67 --- /dev/null +++ b/test/SimpleContract.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LGPL-3.0-only + +pragma solidity 0.8.20; + +contract SimpleContract { + uint256 public count; + + constructor(uint256 _count) { + count = _count; + } + + function get() public view returns (uint256) { + return count; + } + + function inc() public { + count++; + } + + function add(uint256 _value) public { + count += _value; + } +} diff --git a/test/integration/CrosschainDeployScriptIntegrationTest.t.sol b/test/integration/CrosschainDeployScriptIntegrationTest.t.sol new file mode 100644 index 0000000..bfc653b --- /dev/null +++ b/test/integration/CrosschainDeployScriptIntegrationTest.t.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.20; + +import {Test, console} from "forge-std/Test.sol"; +import {CrosschainDeployScript} from "../../src/CrosschainDeployScript.sol"; +import {ICrosschainDeployAdapter} from "../../src/interfaces/CrosschainDeployAdapterInterface.sol"; +import {SimpleContract} from "../SimpleContract.sol"; +import {MockCrosschainDeployAdapter} from "../mocks/MockCrosschainDeployAdapter.sol"; + +// NOTE: This needs `--fork-url` to run. +contract CrosschainDeployIntegrationTest is Test { + string constant contractString = "SimpleContract.sol:SimpleContract"; + string[] _deploymentTargets; + bytes[] _constructorArgs; + bytes[] _initDatas; + uint8[] _domainIds; + + modifier isValidChain() { + require(isValidChainId(block.chainid) == true, "Not a valid chain to test on. Are you using `--fork-url`?"); + + _; + } + // add a deployment target and deploy + + function testAddDeploymentTargetIntegration() public isValidChain { + CrosschainDeployScript crosschainDeployScript = new CrosschainDeployScript(); + bytes memory constructorArgs = abi.encode(uint256(1)); + bytes memory initData = ""; + crosschainDeployScript.addDeploymentTarget("sepolia", constructorArgs, initData); + uint256 fee = 0.0001 ether; + // before calling `deploy`, setup everything required to check whether the + // call to the upstream contract is _actually_ performed. + ICrosschainDeployAdapter adapter = ICrosschainDeployAdapter(0x85d62AD850B322152BF4ad9147bfBF097DA42217); + bytes memory _deployByteCode = vm.getCode(contractString); + uint256 _gasLimit = 5000; + bool _isUniquePerChain = false; + // generate a pseudorandom salt and use `setSalt` so that the same value is used in the contract call. + bytes32 _salt = crosschainDeployScript.generateSalt(); + crosschainDeployScript.setSalt(_salt); + + _constructorArgs = new bytes[](1); + _constructorArgs[0] = constructorArgs; + _initDatas = new bytes[](1); + _initDatas[0] = initData; + _domainIds = new uint8[](1); + _domainIds[0] = 2; + + // expect a `calculateDeployFee` call to the *upstream* contract + vm.expectCall( + address(adapter), + abi.encodeCall( + adapter.calculateDeployFee, + (_deployByteCode, _gasLimit, _salt, _isUniquePerChain, _constructorArgs, _initDatas, _domainIds) + ) + ); + uint256[] memory fees = adapter.calculateDeployFee( + _deployByteCode, _gasLimit, _salt, _isUniquePerChain, _constructorArgs, _initDatas, _domainIds + ); + uint256 totalFee; + uint256 feesArrayLength = fees.length; + for (uint256 j = 0; j < feesArrayLength; j++) { + totalFee += fees[j]; + } + + // expect a `deploy` call to the *upstream* contract + vm.expectCall( + address(adapter), + abi.encodeCall( + adapter.deploy, + (_deployByteCode, _gasLimit, _salt, _isUniquePerChain, _constructorArgs, _initDatas, _domainIds, fees) + ) + ); + crosschainDeployScript.deploy{value: fee}(contractString, _gasLimit, _isUniquePerChain); + } + + // checks that the chainID is of a chain that our contracts + // support. + function isValidChainId(uint256 chainId) private pure returns (bool) { + uint256[] memory _chainIds = new uint256[](7); + _chainIds[0] = 5; + _chainIds[1] = 11155111; + _chainIds[2] = 338; + _chainIds[3] = 17000; + _chainIds[4] = 80001; + _chainIds[5] = 421614; + _chainIds[6] = 10200; + for (uint8 i = 0; i < 7; i++) { + if (chainId == _chainIds[i]) { + return true; + } + } + return true; + } + + // tests that the contract is deployed and called with varying initData. + function testDifferentConstructorArgsAndInitDataIntegration() public isValidChain { + CrosschainDeployScript crosschainDeployScript = new CrosschainDeployScript(); + + // setup empty arrays to hold all the inputs to the contract. + _deploymentTargets = new string[](2); + _constructorArgs = new bytes[](2); + _initDatas = new bytes[](2); + _domainIds = new uint8[](2); + + _deploymentTargets[0] = "sepolia"; + _constructorArgs[0] = abi.encode(uint256(1)); + _initDatas[0] = abi.encodeWithSignature("inc()"); + _domainIds[0] = 2; + + _deploymentTargets[1] = "goerli"; + _constructorArgs[1] = abi.encode(uint256(10)); + _initDatas[1] = abi.encodeWithSignature("add(uint256)", uint256(5)); + _domainIds[1] = 1; + + // loop through these and call `addDeploymentTarget` so that they'll be added in order. + for (uint8 i = 0; i < _deploymentTargets.length; i++) { + crosschainDeployScript.addDeploymentTarget(_deploymentTargets[i], _constructorArgs[i], _initDatas[i]); + } + + // before calling `deploy`, setup everything required to check whether the + // call to the upstream contract is _actually_ performed. + ICrosschainDeployAdapter adapter = ICrosschainDeployAdapter(0x85d62AD850B322152BF4ad9147bfBF097DA42217); + bytes memory _deployByteCode = vm.getCode(contractString); + uint256 _gasLimit = 5000; + bool _isUniquePerChain = false; + // generate a pseudorandom salt and use `setSalt` so that the same value is used in the contract call. + bytes32 _salt = crosschainDeployScript.generateSalt(); + crosschainDeployScript.setSalt(_salt); + + // expect a `calculateDeployFee` call to the *upstream* contract + vm.expectCall( + address(adapter), + abi.encodeCall( + adapter.calculateDeployFee, + (_deployByteCode, _gasLimit, _salt, _isUniquePerChain, _constructorArgs, _initDatas, _domainIds) + ) + ); + uint256[] memory fees = adapter.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; + } + + // expect a `deploy` call to the *upstream* contract + vm.expectCall( + address(adapter), + abi.encodeCall( + adapter.deploy, + (_deployByteCode, _gasLimit, _salt, _isUniquePerChain, _constructorArgs, _initDatas, _domainIds, fees) + ) + ); + crosschainDeployScript.deploy{value: totalFee}(contractString, _gasLimit, _isUniquePerChain); + } +} diff --git a/test/mocks/MockCrosschainDeployAdapter.sol b/test/mocks/MockCrosschainDeployAdapter.sol new file mode 100644 index 0000000..6f136b5 --- /dev/null +++ b/test/mocks/MockCrosschainDeployAdapter.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.20; + +contract MockCrosschainDeployAdapter { + /** + * @notice Deposits to the Bridge contract using the PermissionlessGenericHandler, + * @notice to request contract deployments on other chains. + */ + function deploy( + bytes calldata, + uint256, + bytes32, + bool, + bytes[] memory, + bytes[] memory, + uint8[] memory, + uint256[] memory + ) external payable { + // TODO: Fill this with something that mocks it. + } + + /** + * @notice Computes the address where the contract will be deployed on specified chain. + * @return Address where the contract will be deployed on specified chain. + */ + function computeContractAddressForChain(address, bytes32, bool, uint256) + external + pure + returns (address) + { + address newAddress; + return newAddress; + } + + /** + * @notice Returns total amount of native currency needed for a deploy request. + */ + function calculateDeployFee( + bytes calldata, + uint256, + bytes32, + bool, + bytes[] memory, + bytes[] memory, + uint8[] memory + ) external pure returns (uint256[] memory fees) { + fees = new uint256[](4); + return fees; + } +} diff --git a/test/unit/CrosschainDeployScriptUnitTest.t.sol b/test/unit/CrosschainDeployScriptUnitTest.t.sol new file mode 100644 index 0000000..6e8237a --- /dev/null +++ b/test/unit/CrosschainDeployScriptUnitTest.t.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: LGPL-3.0-only + +pragma solidity 0.8.20; + +import {Test, console} from "forge-std/Test.sol"; +import {CrosschainDeployScript} from "../../src/CrosschainDeployScript.sol"; +import {SimpleContract} from "../SimpleContract.sol"; +import {MockCrosschainDeployAdapter} from "../mocks/MockCrosschainDeployAdapter.sol"; + +contract CrosschainDeployScriptUnitTest is Test { + MockCrosschainDeployAdapter mockCrosschainDeployAdapter; + + string constant contractString = "SimpleContract.sol:SimpleContract"; + string[] _deploymentTargets; + bytes[] _constructorArgs; + bytes[] _initDatas; + uint8[] _domainIds; + + + // Deploy the mocked crosschain deploy adapter. + function setUp() public { + vm.startBroadcast(); + mockCrosschainDeployAdapter = new MockCrosschainDeployAdapter(); + vm.stopBroadcast(); + } + + + /** + * This test checks if we are able to deploy to a _mocked_ local contract, + * and checks if the call to the mocked contract is performed. + * It checks the addDeploymentTarget function, and the deploy function. + */ + function testAddDeploymentTargetAnvil() public { + CrosschainDeployScript crosschainDeployScript = new CrosschainDeployScript(); + // set the constructorArgs and the initData. + bytes memory constructorArgs = abi.encode(uint256(1)); + bytes memory initData = ""; + crosschainDeployScript.setCrosschainDeployContractAddress(address(mockCrosschainDeployAdapter)); + crosschainDeployScript.addDeploymentTarget("sepolia", constructorArgs, initData); + uint256 fee = 0.0001 ether; + vm.deal(msg.sender, fee * 2); + crosschainDeployScript.deploy{value: fee}("SimpleContract.sol:SimpleContract", 50000, false); + } + + // add a deployment target and deploy + function testAddDeploymentTargetWithArgsAnvil() public { + CrosschainDeployScript crosschainDeployScript = new CrosschainDeployScript(); + crosschainDeployScript.setCrosschainDeployContractAddress(address(mockCrosschainDeployAdapter)); + bytes memory constructorArgs = abi.encode(uint256(1)); + bytes memory initData = ""; + crosschainDeployScript.addDeploymentTarget("sepolia", constructorArgs, initData); + uint256 fee = 0.0001 ether; + bytes memory _deployByteCode = vm.getCode(contractString); + uint256 _gasLimit = 5000; + bool _isUniquePerChain = false; + // generate a pseudorandom salt and use `setSalt` so that the same value is used in the contract call. + bytes32 _salt = crosschainDeployScript.generateSalt(); + crosschainDeployScript.setSalt(_salt); + + _constructorArgs = new bytes[](1); + _constructorArgs[0] = constructorArgs; + _initDatas = new bytes[](1); + _initDatas[0] = initData; + _domainIds = new uint8[](1); + _domainIds[0] = 2; + + // expect a `calculateDeployFee` call to the *upstream* contract + vm.expectCall( + address(mockCrosschainDeployAdapter), + abi.encodeCall( + mockCrosschainDeployAdapter.calculateDeployFee, + (_deployByteCode, _gasLimit, _salt, _isUniquePerChain, _constructorArgs, _initDatas, _domainIds) + ) + ); + uint256[] memory fees = mockCrosschainDeployAdapter.calculateDeployFee( + _deployByteCode, _gasLimit, _salt, _isUniquePerChain, _constructorArgs, _initDatas, _domainIds + ); + uint256 totalFee; + uint256 feesArrayLength = fees.length; + for (uint256 j = 0; j < feesArrayLength; j++) { + totalFee += fees[j]; + } + + // expect a `deploy` call to the *upstream* contract + vm.expectCall( + address(mockCrosschainDeployAdapter), + abi.encodeCall( + mockCrosschainDeployAdapter.deploy, + (_deployByteCode, _gasLimit, _salt, _isUniquePerChain, _constructorArgs, _initDatas, _domainIds, fees) + ) + ); + crosschainDeployScript.deploy{value: fee}(contractString, _gasLimit, _isUniquePerChain); + } + + // tests that the contract is deployed and called with varying initData. + function testDifferentConstructorArgsAndInitDataAnvil() public { + CrosschainDeployScript crosschainDeployScript = new CrosschainDeployScript(); + crosschainDeployScript.setCrosschainDeployContractAddress(address(mockCrosschainDeployAdapter)); + // setup empty arrays to hold all the inputs to the contract. + _deploymentTargets = new string[](2); + _constructorArgs = new bytes[](2); + _initDatas = new bytes[](2); + _domainIds = new uint8[](2); + + _deploymentTargets[0] = "sepolia"; + _constructorArgs[0] = abi.encode(uint256(1)); + _initDatas[0] = abi.encodeWithSignature("inc()"); + _domainIds[0] = 2; + + _deploymentTargets[1] = "goerli"; + _constructorArgs[1] = abi.encode(uint256(10)); + _initDatas[1] = abi.encodeWithSignature("add(uint256)", uint256(5)); + _domainIds[1] = 1; + + // loop through these and call `addDeploymentTarget` so that they'll be added in order. + for (uint8 i = 0; i < _deploymentTargets.length; i++) { + crosschainDeployScript.addDeploymentTarget(_deploymentTargets[i], _constructorArgs[i], _initDatas[i]); + } + + // before calling `deploy`, setup everything required to check whether the + // call to the upstream contract is _actually_ performed. + bytes memory _deployByteCode = vm.getCode(contractString); + uint256 _gasLimit = 5000; + bool _isUniquePerChain = false; + // generate a pseudorandom salt and use `setSalt` so that the same value is used in the contract call. + bytes32 _salt = crosschainDeployScript.generateSalt(); + crosschainDeployScript.setSalt(_salt); + + // expect a `calculateDeployFee` call to the *upstream* contract + vm.expectCall( + address(mockCrosschainDeployAdapter), + abi.encodeCall( + mockCrosschainDeployAdapter.calculateDeployFee, + (_deployByteCode, _gasLimit, _salt, _isUniquePerChain, _constructorArgs, _initDatas, _domainIds) + ) + ); + uint256[] memory fees = mockCrosschainDeployAdapter.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; + } + + // expect a `deploy` call to the *upstream* contract + vm.expectCall( + address(mockCrosschainDeployAdapter), + abi.encodeCall( + mockCrosschainDeployAdapter.deploy, + (_deployByteCode, _gasLimit, _salt, _isUniquePerChain, _constructorArgs, _initDatas, _domainIds, fees) + ) + ); + crosschainDeployScript.deploy{value: totalFee}(contractString, _gasLimit, _isUniquePerChain); + } +}