DeployHelper is the base contract for all deployment scripts. Inherit from it and override virtual methods to customize behavior.
import {DeployHelper} from "foundry-deployer/DeployHelper.sol";
contract MyDeploy is DeployHelper {
function setUp() public override {
_setUp("category");
}
function run() public { ... }
}function setUp() public virtualMust be overridden. Call _setUp() with your deployment category.
Example:
function setUp() public override {
_setUp("production");
}function _setUp(string memory subfolder, address deployer) internal withCreateXInitialize the deployment helper with a category name and explicit deployer address.
Parameters:
subfolder: Deployment category for organizing files (e.g., "production", "staging")deployer: Explicit deployer address (used for salt computation and ownership)
What it does:
- Reads environment variables from
.env - Sets up JSON file paths
- Initializes CreateX integration
Example:
_setUp("my-contracts", msg.sender);
// Creates deployments in: deployments/my-contracts/function _setUp(string memory subfolder) internalConvenience wrapper that calls _setUp(subfolder, msg.sender).
Parameters:
subfolder: Deployment category for organizing files (e.g., "production", "staging")
Example:
_setUp("my-contracts");
// Creates deployments in: deployments/my-contracts/function deploy(bytes memory creationCode) internal returns (address deployed)Deploy a contract using CREATE3.
Parameters:
creationCode: Contract creation bytecode (usetype(MyContract).creationCode)
Returns:
deployed: Address of the deployed contract
Behavior:
- Skips deployment if contract already exists at computed address
- Logs deployment status
- Tracks deployment in JSON
- Generates verification files
- Uses plain
deployCreate3by default; override_getPostDeployInitData()to enable atomic deploy+init viadeployCreate3AndInit
Example:
address myContract = deploy(type(MyContract).creationCode);function __deploy(bytes memory creationCode, string memory subfolder)
internal returns (bool didDeploy, address deployed)Core deployment function with additional control.
Parameters:
creationCode: Contract creation bytecodesubfolder: Deployment category (usuallydeploymentCategory)
Returns:
didDeploy:trueif new deployment,falseif already deployeddeployed: Address of the deployed contract
Example:
(bool isNew, address addr) = __deploy(
type(MyContract).creationCode,
"production"
);
if (isNew) {
// Post-deploy setup (e.g., cross-contract references)
// For ownership init, prefer atomic deploy+init via _getPostDeployInitData()
MyContract(addr).configure(someParam);
}function computeDeploymentAddress(bytes memory creationCode)
public returns (address predicted)Compute deployment address without deploying.
Parameters:
creationCode: Contract creation bytecode
Returns:
predicted: Predicted deployment address
Note: Requires _setUp() to be called first. Not view because it creates a temporary contract to extract version.
Example:
address predictedAddr = computeDeploymentAddress(
type(MyContract).creationCode
);
console.log("Will deploy to:", predictedAddr);
address deployed = deploy(type(MyContract).creationCode);
assert(deployed == predictedAddr);function _afterAll() internal virtualSave deployment artifacts to JSON files. Call at the end of your run() function.
Behavior:
- Only saves if
_deployermatchesALLOWED_DEPLOYMENT_SENDER - Writes timestamped JSON file if there are new deployments
- Updates
-latest.jsonfile only when new deployments occur
Example:
function run() public {
address deployed = deploy(type(MyContract).creationCode);
_afterAll(); // Save deployment artifacts
}function _checkChainAndSetOwner(address instance) internal virtualTransfer ownership to production owner on mainnet chains.
Parameters:
instance: Contract address to transfer ownership
Behavior:
- Checks if current chain ID is in
MAINNET_CHAIN_IDS - If mainnet: transfers ownership to
PROD_OWNER - If testnet: skips transfer (deployer retains ownership)
- Skips if owner already set to
PROD_OWNER
Example:
address deployed = deploy(type(MyContract).creationCode);
_checkChainAndSetOwner(deployed); // Transfer on mainnet onlyfunction _getSalt(string memory version) internal view virtual returns (bytes32)Generate CREATE3 salt from version string.
Parameters:
version: Version string (e.g., "1.0.0-MyContract-cancun")
Returns:
- Salt for CREATE3 deployment
Default Implementation:
bytes1 crosschainProtectionFlag = bytes1(0x00);
bytes11 randomSeed = bytes11(keccak256(abi.encode(version)));
return bytes32(abi.encodePacked(_deployer, crosschainProtectionFlag, randomSeed));Override for custom salt logic:
function _getSalt(string memory version) internal view override returns (bytes32) {
// Include chain ID in salt for per-chain addresses
return keccak256(abi.encodePacked(msg.sender, version, block.chainid));
}These methods can be overridden to customize deployment behavior.
function _getPostDeployInitData() internal virtual returns (bytes memory)Get initialization data for atomic deploy+init.
Returns:
- Calldata to execute after deployment (executed atomically via
deployCreate3AndInit), or empty bytes for plaindeployCreate3
Default Implementation:
return ""; // Plain deployCreate3, no post-deploy initOverride to enable atomic initialization:
function _getPostDeployInitData() internal virtual override returns (bytes memory) {
return abi.encodeWithSignature("initializeOwner(address)", _deployer);
}Override for custom initialization:
function _getPostDeployInitData() internal view override returns (bytes memory) {
return abi.encodeWithSignature("initialize(address,uint256)", customOwner, initialValue);
}function _getDeployValues() internal virtual returns (ICreateX.Values memory)Get ETH values to send during deployment.
Returns:
ICreateX.Valuesstruct withconstructorAmountandinitCallAmount
Default Implementation:
return ICreateX.Values({constructorAmount: 0, initCallAmount: 0});Override to send ETH during deployment:
function _getDeployValues() internal pure override returns (ICreateX.Values memory) {
return ICreateX.Values({
constructorAmount: 1 ether, // Send to constructor
initCallAmount: 0.5 ether // Send to init call
});
}string public jsonPath; // Timestamped JSON file path
string public jsonPathLatest; // Latest JSON file path (-latest.json)
string public deploymentCategory; // Deployment category (subfolder)
string public unixTime; // Deployment timestampstring public jsonObjKeyDiff; // Key for new deployments
string public jsonObjKeyAll; // Key for all deployments
string public finalJson; // Serialized new deployments
string public finalJsonLatest; // Serialized all deploymentsaddress internal _PROD_OWNER; // Production owner
uint256[] internal _MAINNET_CHAIN_IDS; // Mainnet chain IDs
bool internal _FORCE_DEPLOY; // Force redeploy flag
address internal _ALLOWED_DEPLOYMENT_SENDER; // Allowed saver addressimport {CreateXHelper} from "foundry-deployer/CreateXHelper.sol";CreateXHelper provides CreateX factory setup and verification logic for Foundry scripts. It automatically ensures CreateX is available on the target network by etching it if missing.
- Automatic CreateX deployment detection
- Etching CreateX on local/test networks where it's missing
- CreateX code hash verification
withCreateXmodifier for ensuring setup before deployment
address internal constant CREATEX_ADDRESS = 0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed;
bytes32 internal constant CREATEX_EXTCODEHASH = 0xbd8a7ea8cfca7b4e5f5041d7d4b17bc317c5ce42cfbc42066a00cf26b43eb53f;
bytes32 internal constant EMPTY_ACCOUNT_HASH = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;
ICreateX internal constant createX = ICreateX(CREATEX_ADDRESS);modifier withCreateX()Ensures CreateX is deployed before executing the function. Automatically etches CreateX on chains where it's missing (using vm.etch - only works in Foundry context).
error CreateXDeploymentFailed();
error UnexpectedCodeAtCreateXAddress();import {Versionable} from "foundry-deployer/Versionable.sol";Versionable is an abstract contract that provides version management with optional EVM suffix support.
constructor(string memory evmSuffix_)Accepts an EVM version suffix (e.g., "", "-cancun", "-shanghai").
function _baseVersion() internal pure virtual returns (string memory);Override this function to return your contract's base version string.
import {Versionable} from "foundry-deployer/Versionable.sol";
import {Ownable} from "solady/auth/Ownable.sol";
contract MyContract is Versionable, Ownable {
constructor(string memory evmSuffix_, address owner_) Versionable(evmSuffix_) {
_initializeOwner(owner_);
}
function _baseVersion() internal pure override returns (string memory) {
return "1.0.0-MyContract";
}
}// EVM suffix is auto-detected from foundry.toml
bytes memory creationCode = abi.encodePacked(
type(MyContract).creationCode,
abi.encode(_getEvmSuffix(), _deployer)
);
address deployed = deploy(creationCode);The EVM suffix is automatically detected from foundry.toml using the active FOUNDRY_PROFILE (falling back to profile.default, then root-level evm_version):
- No
evm_versionin config: Returns"1.0.0-MyContract" evm_version = "cancun": Returns"1.0.0-MyContract-cancun"evm_version = "shanghai": Returns"1.0.0-MyContract-shanghai"
import {IVersionable} from "foundry-deployer/interfaces/IVersionable.sol";interface IVersionable {
function version() external view returns (string memory);
}The recommended way to implement IVersionable is to extend the Versionable abstract contract:
contract MyContract is Versionable, Ownable {
constructor(string memory evmSuffix_, address owner_) Versionable(evmSuffix_) {
_initializeOwner(owner_);
}
function _baseVersion() internal pure override returns (string memory) {
return "1.0.0-MyContract";
}
}Follow this format for consistency:
{major}.{minor}.{patch}-{ContractName}{evmSuffix}
Examples:
1.0.0-MyContract2.3.1-TokenVault-shanghai0.1.0-BetaFeature-cancun-beta
Configure these in your .env file.
PROD_OWNER=0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0Type: address
Description: Address that will own contracts on mainnet chains. Used by _checkChainAndSetOwner().
Behavior:
- On mainnet (chain ID in
MAINNET_CHAIN_IDS): ownership transferred to this address - On testnet: deployer retains ownership
MAINNET_CHAIN_IDS=1,56,137,8453Type: uint256[] (comma-separated)
Description: List of chain IDs considered "mainnet" where ownership transfer occurs.
Common Values:
1- Ethereum Mainnet56- BNB Chain137- Polygon8453- Base42161- Arbitrum One10- Optimism
ALLOWED_DEPLOYMENT_SENDER=0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0Type: address
Description: Only this address can save deployment JSON files. Should match your deployer address.
Security: Prevents unauthorized modification of deployment records.
FORCE_DEPLOY=falseType: bool
Default: false
Description: Allow deployment to proceed when the stored standard JSON input differs, and save a timestamped verification input. It does not bypass the "already deployed" address check.
Use Cases:
- Testing deployment scripts
- Accepting verification input changes while keeping deterministic addresses
SKIP_STANDARD_JSON_INPUT=falseType: bool
Default: false
Description: Skips standard JSON input generation, comparison, and saving. This bypasses the FFI call used for verification inputs.
Use Cases:
- Restricted CI or sandboxed environments where FFI is unavailable
- Offline workflows
- Faster test runs when verification artifacts are not needed
Override virtual methods for custom behavior:
contract CustomDeploy is DeployHelper {
// Custom salt generation
function _getSalt(string memory version)
internal view override returns (bytes32)
{
// Include chain ID for per-chain addresses
return keccak256(abi.encodePacked(
msg.sender,
version,
block.chainid
));
}
// Custom ownership transfer
function _checkChainAndSetOwner(address instance)
internal override
{
// Transfer ownership on all chains, not just mainnet
vm.broadcast();
Ownable(instance).transferOwnership(_PROD_OWNER);
}
// Custom post-deployment logic
function _afterAll() internal override {
// Call parent to save JSON
super._afterAll();
// Custom logic
console.log("Deployment complete!");
}
}You can also override _shouldSkipStandardJsonInput() to bypass standard JSON generation/checks programmatically (for example, in tests or restricted CI environments).
Deploy only if certain conditions are met:
function run() public {
// Check if contract should be deployed
if (shouldDeploy()) {
address deployed = deploy(type(MyContract).creationCode);
_checkChainAndSetOwner(deployed);
}
_afterAll();
}
function shouldDeploy() internal view returns (bool) {
// Custom logic
return block.chainid != 1; // Skip Ethereum mainnet
}Deploy contracts in stages with dependencies:
function run() public {
// Stage 1: Core contracts
address token = deploy(type(Token).creationCode);
address vault = deploy(type(Vault).creationCode);
// Stage 2: Initialize
vm.startBroadcast();
Vault(vault).setToken(token);
vm.stopBroadcast();
// Stage 3: Dependent contracts
address router = deploy(
abi.encodePacked(
type(Router).creationCode,
abi.encode(token, vault)
)
);
// Stage 4: Ownership transfer
_checkChainAndSetOwner(token);
_checkChainAndSetOwner(vault);
_checkChainAndSetOwner(router);
_afterAll();
}function run() public {
// Encode constructor arguments
bytes memory creationCode = abi.encodePacked(
type(MyContract).creationCode,
abi.encode(
arg1, // constructor argument 1
arg2, // constructor argument 2
arg3 // constructor argument 3
)
);
address deployed = deploy(creationCode);
_checkChainAndSetOwner(deployed);
_afterAll();
}Note: Each unique constructor argument set creates a different deployment address.
"Computed address mismatch"
- Cause: CREATE3 deployment failed
- Solution: Check salt generation and CreateX is properly deployed
"Already deployed"
- Cause: Contract exists at computed address
- Solution: Change the version (deterministic deployments cannot overwrite existing code)
"Skipping deployment save"
- Cause: Sender doesn't match
ALLOWED_DEPLOYMENT_SENDER - Solution: Set correct address in
.env
"FFI disabled"
- Cause:
ffi = truenot set infoundry.toml - Solution: Enable FFI in config, or set
SKIP_STANDARD_JSON_INPUT=true