forge-deploy
A cli and associated contracts to keep track of deployments by name and reuse them in solidity.
It tries to keep compatibility with hardhat-deploy as far as possible (work in progress).
forge-deploy aims at providing the minimal set of function to provide an elegant deployment system for foundry.
Template to get started:
https://github.com/wighawag/template-foundry
Features
- generate type-safe deployment function for forge contracts. no need to pass in string of text and hope the abi encoded args are in the correct order or of the correct type.
- save deployments in json file (based on hardhat-deploy schema)
- modular system based on templates and solidity library
Modularity
The system is modular. The deploy functions provided by default offer a basic set of feature but the system can be extended by custom function easily. See contracts/DefaultDeployerFunction.sol and how this is a simple library that you can provide yourself. The only thing forge-deploy really provide is the specific set of functions in contrats/Deployer.sol to save
and get
deployments
How to use
There are 2 way to get started, one without npm and one with npm
with npm
-
have a forge project with npm and cd into it
mkdir my-project; cd my-project; forge init; npm init
-
add the forge-deploy package
npm i -D forge-deploy
This will install the forge-deploy binary automatically
-
add to .gitignore the generated files
cat >> .gitignore <<EOF # forge-deploy /generated /deployments/localhost /deployments/31337 EOF
-
you also need to allow forge to read and write on certain paths by editing foundry.toml:
cat >> foundry.toml <<EOF fs_permissions = [ { access = "read", path = "./deployments"}, { access = "read", path = "./out"}, ] EOF
-
generate the type-safe deployment functions
add some scripts in the package.json
{ "scripts": { "compile": "forge-deploy gen-deployer && forge build", "deploy": "forge script script/Counter.s.sol --rpc-url $RPC_URL --broadcast --private-key $DEPLOYER_PRIVATE_KEY -v && forge-deploy sync;" } }
Note how we execute
forge-deploy sync
directly after the script executiom. this is how forge-deploy will keep track of new deployments. -
add a deploy script
add the file
script/Deploy.s.sol
with this content:// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import "forge-deploy/DeployScript.sol"; import "generated/deployer/DeployerFunctions.g.sol"; contract Deployments is DeployScript { using DeployerFunctions for Deployer; function deploy() external returns (Counter) { return deployer.deploy_Counter("MyCounter"); } }
The deploy function will be called and as the script extends DeployScript (which itself extends Script from forge-std) you ll have access to the deployer variable.
This variable mostly expose save and get functions. Deploy functionality is actually implemented in library like the one provided here: "DeployerFunctions.g.sol", which is actually generated code from the command above:
forge-deploy gen-deployer;
-
You can now execute the script via forge script
npm run deploy
Note that with anvil (localhost network), you need to set the DEPLOYMENT_CONTEXT env variable for forge-deploy to save the deployment
DEPLOYMENT_CONTEXT=localhost npm run deploy
This is necessary for localhost which use chain id 31337 as by default forge-deploy will not save the deployment on that chainId (same for 1337). This is so it does not interfere with in-memory tests which also use chainId=31337
The DEPLOYMENT_CONTEXT env var also allows you to segregate different deployment context on the same network. If not specified, the context is the chainId
without npm
-
have a forge project and cd into it
mkdir my-project; cd my-project; forge init;
-
add the forge-deploy package
forge install wighawag/[email protected];
-
build the cli directly from lib/forge-deploy
cd lib/forge-deploy; cargo build --release; cp target/release/forge-deploy ../../forge-deploy;
In the last step above, we also copy it in the project folder for easy access;
This way you can then execute it via the following:
./forge-deploy <command>
You could also download the binaries (if you dont want to use cargo): https://github.com/wighawag/forge-deploy/releases
-
add to .gitignore the generated file + the binary we just installed
cat >> .gitignore <<EOF # forge-deploy /generated /deployments/localhost /deployments/31337 # forge-deploy cli binary /forge-deploy EOF
-
you also need to allow forge to read and write on certain paths by editing foundry.toml:
cat >> foundry.toml <<EOF fs_permissions = [ { access = "read", path = "./deployments"}, { access = "read", path = "./out"} ] EOF
-
generate the type-safe deployment functions
./forge-deploy gen-deployer;
-
add a deploy script
add the file
script/Deploy.s.sol
with this content:// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import "forge-deploy/DeployScript.sol"; import "generated/deployer/DeployerFunctions.g.sol"; contract Deployments is DeployScript { using DeployerFunctions for Deployer; function deploy() external returns (Counter) { return deployer.deploy_Counter("MyCounter"); } }
The deploy function will be called and as the script extends DeployScript (which itself extends Script from forge-std) you ll have access to the deployer variable.
This variable mostly expose save and get functions. Deploy functionality is actually implemented in library like the one provided here: "DeployerFunctions.g.sol", which is actually generated code from the command above:
./forge-deploy gen-deployer;
-
You can now execute the script via forge script
Note that you need to execute
./forge-deploy sync
directly afterwardFor example:
forge script script/Counter.s.sol --rpc-url $RPC_URL --broadcast --private-key $DEPLOYER_PRIVATE_KEY -v && ./forge-deploy sync;
with anvil and default account
DEPLOYMENT_CONTEXT=localhost forge script script/Counter.s.sol --rpc-url http://localhost:8545 --broadcast --private-key ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -v && ./forge-deploy sync;
Note that here we specify the DEPLOYMENT_CONTEXT env variable. This is necessary for localhost which use chain id 31337 as by default forge-deploy will not save the deployment on that chainId (same for 1337). This is so it does not interfere with in-memory tests which also use chainId=31337
The DEPLOYMENT_CONTEXT env var also allows you to segregate different deployment context on the same network. If not specified, the context is the chainId
Quick Start
Get anvil started somewhere:
anvil;
then copy and execute this and see the result
mkdir my-forge-deploy-project;
cd my-forge-deploy-project;
forge init;
forge install wighawag/[email protected];
cd lib/forge-deploy;
cargo build --release;
cp target/release/forge-deploy ../../forge-deploy;
cd ../..;
cat >> foundry.toml <<EOF
fs_permissions = [
{ access = "read", path = "./deployments"},
{ access = "read", path = "./out"}
]
EOF
cat >> .gitignore <<EOF
# forge-deploy
/generated
/deployments/localhost
/deployments/31337
EOF
./forge-deploy gen-deployer;
cat > script/Deploy.s.sol <<EOF
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-deploy/DeployScript.sol";
import "generated/deployer/DeployerFunctions.g.sol";
contract Deployments is DeployScript {
using DeployerFunctions for Deployer;
function deploy() external returns (Counter) {
return deployer.deploy_Counter("MyCounter");
}
}
EOF
DEPLOYMENT_CONTEXT=localhost forge script script/Deploy.s.sol --rpc-url http://localhost:8545 --broadcast --private-key ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -v && ./forge-deploy sync;
Reusable in tests
One great feature of forge's script that remains in forge-deploy is the ability to use script in tests.
This allow you to have your deployment procedure reusable in tests!
for example, here is a basic test for Counter. Copy the following content in the existing test/Counter.t.sol and run the test to see it in action:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Counter.sol";
import "../script/Deploy.s.sol";
contract CounterTest is Test {
Counter public counter;
function setUp() public {
counter = new Deployments().deploy();
counter.setNumber(0);
}
function testIncrement() public {
counter.increment();
assertEq(counter.number(), 1);
}
function testSetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x);
}
}
As usual to run the tests you can do the following:
forge test
More info
Note that the generated solidity is optional.
You can instead simply use the default deploy function
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-deploy/DeployScript.sol";
import "forge-deploy/DefaultDeployerFunction.sol";
import "../src/Counter.sol";
contract Deployments is DeployScript {
using DefaultDeployerFunction for Deployer;
function deploy() external returns (Counter) {
return Counter(
deployer.deploy(
"MyCounter",
"Counter.sol:Counter", // forge's artifact id
"" // no arguments: empty bytes
)
);
}
}