diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5a1c7e0 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +MAINNET_RPC_URL="" +SEPOLIA_RPC_URL="https://rpc.sepolia.org" +BASE_SEPOLIA_RPC_URL="https://sepolia.base.org" +BASESCAN_API_KEY="" +CREATE2SALT=""" +DEPLOYER="" +PRIVATE_KEY="0x{YOUR_PRIVATE_KEY}" +MNEMONIC="" \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b538903 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "solidity.packageDefaultDependenciesContractsDirectory": "src", + "solidity.packageDefaultDependenciesDirectory": "lib", + "editor.formatOnSave": true, + "[solidity]": { + "editor.defaultFormatter": "NomicFoundation.hardhat-solidity" + }, + "[toml]": { + "editor.defaultFormatter": "tamasfe.even-better-toml" + }, + "search.exclude": { + "**/node_modules": true + }, + "solidity.formatter": "forge" +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..08cf4de --- /dev/null +++ b/Makefile @@ -0,0 +1,51 @@ +-include .env + +# Build contracts +build :; forge build + +# Run tests +run-tests :; forge test + +# Clean build contracts +clean :; forge clean + +# Generate coverage stats using lcov and genhtml +# See https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/solidity/coverage.sh +tests-coverage :; ./coverage.sh + +# Deploy the {InvoiceModule} contracts deterministically on Base Sepolia +# See Sablier V2 deployments on Base Sepolia: https://docs.sablier.com/contracts/v2/deployments#base-sepolia +# Update the {PRIVATE_KEY} .env variable with the deployer private key +deploy-deterministic-base-sepolia-invoice-module: + forge script script/DeployDeterministicInvoiceModule.s.sol:DeployDeterministicInvoiceModule $(CREATE2SALT) \ + 0xFE7fc0Bbde84C239C0aB89111D617dC7cc58049f 0xb8c724df3eC8f2Bf8fA808dF2cB5dbab22f3E68c 0x85E094B259718Be1AF0D8CbBD41dd7409c2200aa \ + --sig "run(string,address,address,address)" --rpc-url base_sepolia --private-key $(PRIVATE_KEY) --etherscan-api-key $(BASESCAN_API_KEY) + --broadcast --verify + +# Deploy the {InvoiceModule} contracts deterministically on Base +# See Sablier V2 deployments on Base Sepolia: https://docs.sablier.com/contracts/v2/deployments#base +# Update the {PRIVATE_KEY} .env variable with the deployer private key +deploy-deterministic-base-invoice-module: + forge script script/DeployDeterministicInvoiceModule.s.sol:DeployDeterministicInvoiceModule $(CREATE2SALT) \ + 0x4CB16D4153123A74Bc724d161050959754f378D8 0xf4937657Ed8B3f3cB379Eed47b8818eE947BEb1e 0x85E094B259718Be1AF0D8CbBD41dd7409c2200aa \ + --sig "run(string,address,address,address)" --rpc-url base_sepolia --private-key $(PRIVATE_KEY) --etherscan-api-key $(BASESCAN_API_KEY) + --broadcast --verify + + +# Deploy a {Container} contract deterministically on Base +# Update the {PRIVATE_KEY} .env variable with the deployer private key +deploy-deterministic-base-container: + forge script script/DeployDeterministicContainer.s.sol:DeployDeterministicContainer \ + $(CREATE2SALT) 0x85E094B259718Be1AF0D8CbBD41dd7409c2200aa [] \ + --sig "run(string,address,address[])" --rpc-url base_sepolia \ + --private-key $(PRIVATE_KEY) --etherscan-api-key $(BASESCAN_API_KEY) \ + --broadcast --verify + +# Deploy a {Container} contract on Base +# Update the {PRIVATE_KEY} .env variable with the deployer private key +deploy-base-container: + forge script script/DeployContainer.s.sol:DeployContainer \ + 0x85E094B259718Be1AF0D8CbBD41dd7409c2200aa [] \ + --sig "run(address,address[])" --rpc-url base_sepolia \ + --private-key $(PRIVATE_KEY) --etherscan-api-key $(BASESCAN_API_KEY) \ + --broadcast --verify \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 82f6401..707a163 100644 --- a/foundry.toml +++ b/foundry.toml @@ -15,4 +15,13 @@ quote_style = "double" tab_width = 4 wrap_comments = false +[rpc_endpoints] +mainnet = "${MAINNET_RPC_URL}" +sepolia = "${SEPOLIA_RPC_URL}" +base_sepolia = "${BASE_SEPOLIA_RPC_URL}" + +[etherscan] +sepolia = { key = "${ETHERSCAN_API_KEY}" } +base_sepolia = { key = "${BASESCAN_API_KEY}" } + # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/script/Base.s.sol b/script/Base.s.sol new file mode 100644 index 0000000..8c941cc --- /dev/null +++ b/script/Base.s.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.20; + +import { Script } from "forge-std/Script.sol"; + +contract BaseScript is Script { + /// @dev Junk mnemonic seed phrase use as a fallback in case there is no mnemonic set in the `.env` file + string internal constant TEST_MNEMONIC = "test test test test test test test test test test test junk"; + + /// @dev Used to derive the deployer's address + string internal mnemonic; + + /// @dev Stores the deployer address + address deployer; + + constructor() { + address from = vm.envOr({ name: "DEPLOYER", defaultValue: address(0) }); + if (from != address(0)) { + deployer = from; + } else { + mnemonic = vm.envOr({ name: "MNEMONIC", defaultValue: TEST_MNEMONIC }); + (deployer, ) = deriveRememberKey(mnemonic, 0); + } + } + + modifier broadcast() { + vm.startBroadcast(deployer); + _; + vm.stopBroadcast(); + } +} diff --git a/script/DeployContainer.s.sol b/script/DeployContainer.s.sol new file mode 100644 index 0000000..2c86eec --- /dev/null +++ b/script/DeployContainer.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +import { BaseScript } from "./Base.s.sol"; +import { Container } from "../src/Container.sol"; + +/// @notice Deploys an instance of {Container} and enables initial module(s) +contract DeployContainer is BaseScript { + function run( + address initialOwner, + address[] memory initialModules + ) public virtual broadcast returns (Container container) { + // Ddeploy the {InvoiceModule} contracts + container = new Container(initialOwner, initialModules); + } +} diff --git a/script/DeployDeterministicContainer.s.sol b/script/DeployDeterministicContainer.s.sol new file mode 100644 index 0000000..83aafef --- /dev/null +++ b/script/DeployDeterministicContainer.s.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +import { BaseScript } from "./Base.s.sol"; +import { Container } from "../src/Container.sol"; + +/// @notice Deploys at deterministic addresses across chains an instance of {Container} and enables initial module(s) +/// @dev Reverts if any contract has already been deployed +contract DeployDeterministicContainer is BaseScript { + /// @dev By using a salt, Forge will deploy the contract via a deterministic CREATE2 factory + /// https://book.getfoundry.sh/tutorials/create2-tutorial?highlight=deter#deterministic-deployment-using-create2 + function run( + string memory create2Salt, + address initialOwner, + address[] memory initialModules + ) public virtual broadcast returns (Container container) { + bytes32 salt = bytes32(abi.encodePacked(create2Salt)); + + // Deterministically deploy a {Container} contract + container = new Container{ salt: salt }(initialOwner, initialModules); + } +} diff --git a/script/DeployDeterministicInvoiceModule.s.sol b/script/DeployDeterministicInvoiceModule.s.sol new file mode 100644 index 0000000..bcc42bb --- /dev/null +++ b/script/DeployDeterministicInvoiceModule.s.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +import { BaseScript } from "./Base.s.sol"; +import { InvoiceModule } from "../src/modules/invoice-module/InvoiceModule.sol"; +import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; + +/// @notice Deploys and initializes the {InvoiceModule} contracts at deterministic addresses across chains +/// @dev Reverts if any contract has already been deployed +contract DeployDeterministicInvoiceModule is BaseScript { + /// @dev By using a salt, Forge will deploy the contract via a deterministic CREATE2 factory + /// https://book.getfoundry.sh/tutorials/create2-tutorial?highlight=deter#deterministic-deployment-using-create2 + function run( + string memory create2Salt, + ISablierV2LockupLinear sablierLockupLinear, + ISablierV2LockupTranched sablierLockupTranched, + address brokerAdmin + ) public virtual broadcast returns (InvoiceModule invoiceModule) { + bytes32 salt = bytes32(abi.encodePacked(create2Salt)); + + // Deterministically deploy the {InvoiceModule} contracts + invoiceModule = new InvoiceModule{ salt: salt }(sablierLockupLinear, sablierLockupTranched, brokerAdmin); + } +}