Skip to content

Solidity Libraries used in conjunction with `treb-cli` to deploy Solidity contracts with CreateX, Foundry an Gnosis Safe

Notifications You must be signed in to change notification settings

baroooo/treb-sol

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

88 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Developers deploying code with a trebuchet

treb-sol

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.

Overview

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.

Key Features

  • 🎯 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

Installation

forge install trebuchet-org/treb-sol

Architecture

The library is built around three core concepts:

  1. Sender Abstraction: Unified interface for different wallet types
  2. Global Transaction Queue: Maintains execution order across different senders
  3. Harness System: Secure proxy-based contract interaction

Quick Start

Basic Deployment Script (with treb-cli)

// 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);
    }
}

Standalone Usage (without treb-cli)

// 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);
    }
}

Multi-Contract Deployment

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
    }
}

Base Contracts

TrebScript vs ConfigurableTrebScript

treb-sol provides two base contracts for different usage scenarios:

TrebScript

  • Use case: Integration with treb-cli
  • Configuration: Reads from environment variables automatically
  • Best for: Production deployments managed by treb-cli

ConfigurableTrebScript

  • 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
    ) {}
}

Complete Example

See script/ExampleDeploy.sol for a comprehensive example demonstrating:

  • Safe multisig + proposer configuration
  • Contract deployment with ownership transfer
  • Safe transaction execution through harness system

Sender Types

Private Key Senders (Development)

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;
}

Hardware Wallet Senders

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;
}

Safe Multisig with Hardware Wallet Proposer

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;
}

Multiple Senders for Complex Workflows

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.

Transaction Execution

Direct Transaction Execution

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);
    }
}

Using Contract Harness

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
    }
}

Registry Integration

Cross-Environment Lookups

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));
    }
}

Cross-Chain References

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));
    }
}

Advanced Patterns

Labeled Deployments

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
    }
}

Custom Entropy

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();
    }
}

Conditional Deployments

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);
        }
    }
}

Testing and Debugging

Dry Run Mode

# Set environment for dry run
export DRYRUN=true

# Run script without executing transactions
forge script script/Deploy.s.sol

Address Prediction

contract 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);
    }
}

Testing with Multiple Senders

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();
    }
}

Environment Configuration

Development (.env)

# Simple development setup
SENDER_CONFIGS=0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000764656661756c7400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000ac18b2c3e86929874e04ba4ac0c3b92ef2a5e6d8c1405c7e86ba3b5b5720d
NAMESPACE=default

Production (.env)

# Hardware wallet + Safe setup
SENDER_CONFIGS=<complex-abi-encoded-safe-config>
NAMESPACE=production
DEPLOYER_DERIVATION_PATH=m/44'/60'/0'/0/0
SAFE_ADDRESS=0x32CB58b145d3f7e28c45cE4B2Cc31fa94248b23F

Current Limitations

Hardware Wallet Transaction Signing

Due 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 script with --ledger --mnemonic-derivation-path "<path>", Foundry takes exclusive control of the hardware wallet for broadcasting
  • The Safe integration uses cast commands 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.sign to work with hardware wallets, eliminating the need for FFI calls

The canBroadcast Flag

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.

Using with treb-cli

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)

Example: Safe with Hardware Wallet 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;
}

Integration with treb CLI

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

Best Practices

1. Use Descriptive Labels

// Good: Clear versioning
address tokenV2 = deployer.create3("Token").setLabel("v2").deploy();

// Bad: Unclear purpose  
address token2 = deployer.create3("Token").setLabel("new").deploy();

2. Handle Missing Dependencies

address dependency = lookup("RequiredContract");
require(dependency != address(0), "Required contract not deployed");

3. Organize Complex Deployments

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
    }
}

4. Use Safe Defaults

// 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();

API Reference

Core Contracts

  • 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

Key Functions

  • sender(name): Get sender by name
  • lookup(identifier): Look up deployed contract address
  • harness(target): Get harness proxy for contract interaction
  • create3(artifact): Create CREATE3 deployment
  • create2(artifact): Create CREATE2 deployment
  • broadcast: Modifier for automatic transaction broadcasting

License

MIT

About

Solidity Libraries used in conjunction with `treb-cli` to deploy Solidity contracts with CreateX, Foundry an Gnosis Safe

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Solidity 100.0%