A powerful Solidity library for deterministic smart contract deployments with multi-sender coordination, hardware wallet support, and Safe multisig integration. Part of the Trebuchet deployment framework - because sometimes you need perfect ballistics for your contract launches.
treb-sol provides a sophisticated framework for writing deployment scripts that can execute transactions through different wallet types while maintaining deterministic addresses across chains. Unlike traditional deployment tools, treb-sol allows you to write arbitrary scripts with automatic transaction coordination and broadcasting.
- 🎯 Deterministic Deployments: CreateX-based deployments with predictable addresses
- 🔄 Multi-Sender Coordination: Unified interface for EOA, hardware wallets, and Safe multisig
- 🛡️ Transaction Batching: Automatic batching for Safe multisig efficiency
- 🔍 Address Prediction: Predict deployment addresses before execution
- 📚 Registry Integration: Lookup previously deployed contracts across environments
- đź§Ş Harness System: Secure proxy-based contract interaction
- 🏗️ Flexible Scripting: Write arbitrary deployment logic with automatic broadcasting
forge install trebuchet-org/treb-solThe library is built around three core concepts:
- Sender Abstraction: Unified interface for different wallet types
- Global Transaction Queue: Maintains execution order across different senders
- Harness System: Secure proxy-based contract interaction
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {TrebScript} from "treb-sol/TrebScript.sol";
import {Deployer} from "treb-sol/internal/sender/Deployer.sol";
contract DeployCounter is TrebScript {
using Deployer for Senders.Sender;
using Deployer for Deployer.Deployment;
function run() public broadcast {
// Get default sender
Senders.Sender storage deployer = sender("default");
// Deploy contract with deterministic address
address counter = deployer.create3("Counter").deploy();
console.log("Counter deployed at:", counter);
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {ConfigurableTrebScript} from "treb-sol/ConfigurableTrebScript.sol";
import {Deployer} from "treb-sol/internal/sender/Deployer.sol";
import {Senders} from "treb-sol/internal/sender/Senders.sol";
import {SenderTypes} from "treb-sol/internal/types.sol";
contract StandaloneDeployment is ConfigurableTrebScript {
using Deployer for Senders.Sender;
constructor() ConfigurableTrebScript(
_getSenderConfigs(), // Custom sender configuration
"production", // Namespace
"deployments.json", // Registry file
false, // Not dry run
false // Not quiet mode
) {}
function _getSenderConfigs() internal pure returns (Senders.SenderInitConfig[] memory) {
Senders.SenderInitConfig[] memory configs = new Senders.SenderInitConfig[](1);
configs[0] = Senders.SenderInitConfig({
name: "deployer",
account: 0xYourDeployerAddress,
senderType: SenderTypes.InMemory,
canBroadcast: true,
config: abi.encode(0xYourPrivateKey)
});
return configs;
}
function run() public broadcast {
Senders.Sender storage deployer = sender("deployer");
address counter = deployer.create3("Counter").deploy();
console.log("Counter deployed at:", counter);
}
}contract DeploySystem is TrebScript {
using Deployer for Senders.Sender;
function run() public broadcast {
Senders.Sender storage deployer = sender("default");
// Deploy contracts in order
address token = deployer.create3("Token").deploy();
address vault = deployer.create3("Vault").deploy(abi.encode(token));
address router = deployer.create3("Router").deploy(abi.encode(vault, token));
// All transactions are automatically broadcast at the end
}
}treb-sol provides two base contracts for different usage scenarios:
- Use case: Integration with treb-cli
- Configuration: Reads from environment variables automatically
- Best for: Production deployments managed by treb-cli
- Use case: Standalone usage without treb-cli
- Configuration: Manual configuration via constructor parameters
- Best for: Testing, custom deployment frameworks, standalone usage
// With treb-cli (environment variables)
contract MyDeployment is TrebScript {
// Automatically reads SENDER_CONFIGS, NAMESPACE, etc.
}
// Standalone (manual configuration)
contract MyDeployment is ConfigurableTrebScript {
constructor() ConfigurableTrebScript(
_getSenderConfigs(), // Define your own configs
"production", // Explicit namespace
"registry.json", // Explicit registry file
false, // Explicit dry-run setting
false // Explicit quiet mode
) {}
}See script/ExampleDeploy.sol for a comprehensive example demonstrating:
- Safe multisig + proposer configuration
- Contract deployment with ownership transfer
- Safe transaction execution through harness system
function _getSenderConfigs() internal pure returns (Senders.SenderInitConfig[] memory) {
Senders.SenderInitConfig[] memory configs = new Senders.SenderInitConfig[](1);
// In-memory private key sender for development
configs[0] = Senders.SenderInitConfig({
name: "deployer",
account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266,
senderType: SenderTypes.InMemory,
canBroadcast: true,
config: abi.encode(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80)
});
return configs;
}function _getSenderConfigs() internal pure returns (Senders.SenderInitConfig[] memory) {
Senders.SenderInitConfig[] memory configs = new Senders.SenderInitConfig[](1);
// Ledger hardware wallet
configs[0] = Senders.SenderInitConfig({
name: "ledger-deployer",
account: 0x742d35Cc6448Bf4C7D2b6C7c8d9c2a51d4e2d98f,
senderType: SenderTypes.Ledger,
canBroadcast: true, // Set to false if using as Safe signer
config: abi.encode("m/44'/60'/0'/0/0") // derivation path
});
return configs;
}function _getSenderConfigs() internal pure returns (Senders.SenderInitConfig[] memory) {
Senders.SenderInitConfig[] memory configs = new Senders.SenderInitConfig[](2);
// Hardware wallet as proposer
configs[0] = Senders.SenderInitConfig({
name: "proposer",
account: 0x742d35Cc6448Bf4C7D2b6C7c8d9c2a51d4e2d98f,
senderType: SenderTypes.Ledger,
canBroadcast: false, // Cannot broadcast when used as Safe signer
config: abi.encode("m/44'/60'/0'/0/0")
});
// Safe multisig that uses the proposer
configs[1] = Senders.SenderInitConfig({
name: "safe",
account: 0x1234567890123456789012345678901234567890, // Safe address
senderType: SenderTypes.GnosisSafe,
canBroadcast: true,
config: abi.encode("proposer") // references proposer by name
});
return configs;
}function _getSenderConfigs() internal pure returns (Senders.SenderInitConfig[] memory) {
Senders.SenderInitConfig[] memory configs = new Senders.SenderInitConfig[](4);
// Fast deployer for development contracts
configs[0] = Senders.SenderInitConfig({
name: "dev-deployer",
account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266,
senderType: SenderTypes.InMemory,
canBroadcast: true,
config: abi.encode(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80)
});
// Hardware wallet for production deploys
configs[1] = Senders.SenderInitConfig({
name: "prod-deployer",
account: 0x742d35Cc6448Bf4C7D2b6C7c8d9c2a51d4e2d98f,
senderType: SenderTypes.Ledger,
canBroadcast: true,
config: abi.encode("m/44'/60'/0'/0/0")
});
// Proposer for Safe transactions
configs[2] = Senders.SenderInitConfig({
name: "proposer",
account: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8,
senderType: SenderTypes.InMemory,
canBroadcast: true,
config: abi.encode(0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d)
});
// Treasury Safe for holding assets
configs[3] = Senders.SenderInitConfig({
name: "treasury",
account: 0x1234567890123456789012345678901234567890,
senderType: SenderTypes.GnosisSafe,
canBroadcast: true,
config: abi.encode("proposer")
});
return configs;
}Note: When using treb-cli, sender configuration is managed automatically through environment variables and configuration files. The examples above are for standalone usage with
ConfigurableTrebScript.
contract CustomDeployment is TrebScript {
function run() public broadcast {
Senders.Sender storage deployer = sender("default");
// Execute arbitrary transaction
Transaction memory tx = Transaction({
to: someContract,
data: abi.encodeWithSignature("initialize(address)", owner),
value: 0,
label: "initialize-contract"
});
RichTransaction memory result = deployer.execute(tx);
console.logBytes(result.simulatedReturnData);
}
}contract InteractWithContracts is TrebScript {
function run() public broadcast {
Senders.Sender storage deployer = sender("default");
// Get harness for existing contract
address counterAddr = lookup("Counter");
Counter counter = Counter(deployer.harness(counterAddr));
// Interact through harness - transactions are queued
counter.increment();
counter.setName("My Counter");
// All harness calls are broadcast automatically
}
}contract CrossEnvDeployment is TrebScript {
function run() public broadcast {
// Reference production token while deploying to staging
address prodToken = lookup("Token", "production");
// Deploy staging vault that references production token
address vault = sender("default")
.create3("Vault")
.deploy(abi.encode(prodToken));
}
}contract CrossChainSetup is TrebScript {
function run() public broadcast {
// Reference mainnet deployment while on testnet
address mainnetBridge = lookup("Bridge", "production", "1");
// Deploy testnet side with mainnet reference
address testnetBridge = sender("default")
.create3("TestnetBridge")
.deploy(abi.encode(mainnetBridge));
}
}contract VersionedDeployment is TrebScript {
function run() public broadcast {
Senders.Sender storage deployer = sender("default");
// Deploy different versions
address v1 = deployer.create3("Token").setLabel("v1").deploy();
address v2 = deployer.create3("Token").setLabel("v2").deploy();
// Each gets a unique address based on label
}
}contract CustomAddresses is TrebScript {
function run() public broadcast {
Senders.Sender storage deployer = sender("default");
// Use custom entropy for specific address
bytes memory bytecode = vm.getCode("SpecialContract");
address special = deployer
.create3("SpecialContract"),
.setEntropy("special-deployment-2024")
.deploy();
}
}contract ConditionalDeployment is TrebScript {
function run() public broadcast {
Senders.Sender storage deployer = sender("default");
// Check if already deployed
address existing = lookup("UpgradeableProxy");
if (existing == address(0)) {
// First time deployment
address impl = deployer.create3("Implementation").deploy();
address proxy = deployer.create3("UpgradeableProxy").deploy(abi.encode(impl));
} else {
// Upgrade existing
address newImpl = deployer.create3("Implementation").setLabel("v2").deploy();
UpgradeableProxy(deployer.harness(existing)).upgradeTo(newImpl);
}
}
}# Set environment for dry run
export DRYRUN=true
# Run script without executing transactions
forge script script/Deploy.s.solcontract PredictAddresses is TrebScript {
function predict() public view {
// Predict address before deployment
bytes32 salt = keccak256(abi.encodePacked("default/Counter"));
address predicted = CREATEX.computeCreate3Address(salt);
console.log("Counter will deploy to:", predicted);
}
}contract TestMultiSender is TrebScript {
function run() public broadcast {
// Use different senders for different operations
address adminContract = sender("admin").create3("AdminContract").deploy();
address userContract = sender("user").create3("UserContract").deploy();
// Admin operations through admin sender
AdminContract(sender("admin").harness(adminContract)).setConfig();
// User operations through user sender
UserContract(sender("user").harness(userContract)).interact();
}
}# Simple development setup
SENDER_CONFIGS=0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000764656661756c7400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000ac18b2c3e86929874e04ba4ac0c3b92ef2a5e6d8c1405c7e86ba3b5b5720d
NAMESPACE=default# Hardware wallet + Safe setup
SENDER_CONFIGS=<complex-abi-encoded-safe-config>
NAMESPACE=production
DEPLOYER_DERIVATION_PATH=m/44'/60'/0'/0/0
SAFE_ADDRESS=0x32CB58b145d3f7e28c45cE4B2Cc31fa94248b23FDue to current limitations in Foundry's hardware wallet integration, there is a restriction when using hardware wallets (Ledger/Trezor) in deployment scripts:
You can either:
- Sign Safe multisig transactions with a hardware wallet (when the hardware wallet is a Safe owner/proposer)
- Broadcast transactions directly from a hardware wallet
But NOT both in the same script execution.
This limitation exists because:
- When running
forge scriptwith--ledger --mnemonic-derivation-path "<path>", Foundry takes exclusive control of the hardware wallet for broadcasting - The Safe integration uses
castcommands via FFI to sign transactions, which cannot access the hardware wallet while Foundry is using it - There is ongoing work in Foundry to add async cheatcodes that would allow
vm.signto work with hardware wallets, eliminating the need for FFI calls
To handle this limitation, the library introduces a canBroadcast flag in the sender configuration:
Senders.SenderInitConfig({
name: "ledger-signer",
account: 0x...,
senderType: SenderTypes.Ledger,
canBroadcast: false, // Cannot broadcast when used as Safe signer
config: abi.encode("m/44'/60'/0'/0/0")
})When canBroadcast is false, the sender can only be used for signing Safe transactions, not for direct broadcasting.
When using the treb CLI, you must declare which senders should broadcast using the @custom:senders natspec annotation:
contract MyDeployment is TrebScript {
// Only "deployer" and "treasury" senders will broadcast
/// @custom:senders deployer,treasury
function run() broadcst {
}
}The CLI validates that:
- All specified senders exist and have
canBroadcast = true - There are no conflicts (e.g., trying to broadcast from both a Ledger and a Safe that uses the same Ledger as a signer)
function _getSenderConfigs() internal pure returns (Senders.SenderInitConfig[] memory) {
Senders.SenderInitConfig[] memory configs = new Senders.SenderInitConfig[](3);
// Hardware wallet that ONLY signs Safe transactions
configs[0] = Senders.SenderInitConfig({
name: "ledger-signer",
account: 0x742d35Cc6634Eb89C11c4c8D0cA746e842507208,
senderType: SenderTypes.Ledger,
canBroadcast: false, // Cannot broadcast
config: abi.encode("m/44'/60'/0'/0/0")
});
// Safe that uses the hardware wallet as a signer
configs[1] = Senders.SenderInitConfig({
name: "treasury-safe",
account: 0x1234567890123456789012345678901234567890,
senderType: SenderTypes.GnosisSafe,
canBroadcast: true, // Safe can "broadcast" (propose transactions)
config: abi.encode("ledger-signer")
});
// Regular deployer for other operations
configs[2] = Senders.SenderInitConfig({
name: "deployer",
account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266,
senderType: SenderTypes.InMemory,
canBroadcast: true,
config: abi.encode(privateKey)
});
return configs;
}While treb-sol can be used standalone, it's designed to work seamlessly with the treb CLI:
- Registry Management: Automatic registry updates from script output
- Environment Coordination: Consistent namespace and configuration management
- Verification: Automatic contract verification on block explorers
- Multi-chain Orchestration: Deploy to multiple chains with shared configuration
// Good: Clear versioning
address tokenV2 = deployer.create3("Token").setLabel("v2").deploy();
// Bad: Unclear purpose
address token2 = deployer.create3("Token").setLabel("new").deploy();address dependency = lookup("RequiredContract");
require(dependency != address(0), "Required contract not deployed");contract SystemDeployment is TrebScript {
function run() public broadcast {
deployCore();
deployPeripherals();
configureSystem();
}
function deployCore() internal {
// Core contract deployments
}
function deployPeripherals() internal {
// Peripheral contracts
}
function configureSystem() internal {
// System configuration
}
}// Use CREATE3 for flexibility (can upgrade later)
address contract = deployer.create3("Contract").deploy();
// Use CREATE2 only when you need deterministic init code
address factory = deployer.create2("Factory").deploy();- TrebScript: Base contract for treb-cli managed deployment scripts
- ConfigurableTrebScript: Base contract for standalone deployment scripts
- SenderCoordinator: Manages multiple transaction senders
- Registry: Deployment address lookup system
- Deployer: CreateX-based deterministic deployments
- Senders: Low-level sender abstraction library
sender(name): Get sender by namelookup(identifier): Look up deployed contract addressharness(target): Get harness proxy for contract interactioncreate3(artifact): Create CREATE3 deploymentcreate2(artifact): Create CREATE2 deploymentbroadcast: Modifier for automatic transaction broadcasting
MIT
