Skip to content

Conversation

0xrusowsky
Copy link
Contributor

@0xrusowsky 0xrusowsky commented Aug 25, 2025

This PR introduces a new contract, StdConfig, which encapsulates all the logic to read and write from a user-defined .toml config file that sticks to a predetermined structure (access permissions must be granted via foundry.toml as usual).

It also introduces a new abstract contract, Config, which can be inherited together withTest and Script. Users can then tap into the new features that Config and StdConfig enable to streamline the setup of multi-chain environments.

Features

Comprehensive + Easily Programmable Config File

The TOML structure must have top-level keys representing the target chains. Under each chain key, variables are organized by type in separate sub-tables like [<chain>.<type>].

  • chain must either be: a uint or a valid alloy-chain alias.
  • type must be one of: bool, address, uint, bytes32, string, or bytes.
# see `test/fixtures/config.toml` for a full example

[mainnet]
endpoint_url = "${MAINNET_RPC}"

[mainnet.bool]
is_live = true

[mainnet.address]
weth = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
whitelisted_admins = [
   "${MAINNET_ADMIN}",
   "0x00000000000000000000000000000000deadbeef"
]

NOTE: env vars are supported and automatically resolved by StdConfig.

Ease dev burden when dealing with Multi-Chain Setups

The new Config abstract contract introduces a minimal set of storage variables that expose the user config:

/// @dev Contract instance holding the data from the TOML config file.
StdConfig internal config;

/// @dev Array of chain IDs for which forks have been created.
uint256[] internal chainIds;

/// @dev A mapping from a chain ID to its initialized fork ID.
mapping(uint256 => uint256) internal forkOf;

These variables are populated with a single function that users can call when setting up their tests or scripts:

/// @notice  Loads configuration from a file.
///
/// @dev     This function instantiates a `StdConfig` contract, caching all its config variables.
///
/// @param   filePath The path to the TOML configuration file.
/// @param   writeToFile: whether updates are written back to the TOML file.
function _loadConfig(string memory filePath, bool writeToFile) internal;

/// @notice  Loads configuration from a file and creates forks for each specified chain.
///
/// @dev     This function instantiates a `StdConfig` contract, caches its variables,
///          and iterates through the configured chains to create a fork for each one.
///          It also populates the `forkOf[chainId] -> forkId` map to easily switch between forks.
///
/// @param   filePath The path to the TOML configuration file.
/// @param   writeToFile: whether updates are written back to the TOML file.

function _loadConfigAndForks(string memory filePath, bool writeToFile) internal;

Intuitive and type-safe API with StdConfig and LibVariable

  • StdConfig reads, resolves, and parses all variables when initialized, caching them in storage.
  • To access variables, StdConfig exposes a generic get method that returns a Variable struct. This struct holds the raw data and its type information.
  • The LibVariable library is used to safely cast the Variable struct to a concrete Solidity type. This ensures type safety at runtime, reverting with a clear error if a variable is missing or cast incorrectly.
  • All methods can be used without having to inform the chain ID, and the currently selected chain will be automatically derived.
// GETTER FUNCTIONS

/// @notice   Reads a variable and returns it in a generic `Variable` container.
/// @dev      The caller should use `LibVariable` to safely coerce the type.
///           Example: `uint256 myVar = config.get("my_key").toUint256();`
function get(uint256 chain_id, string memory key) public view returns (Variable memory);
function get(string memory key) public view returns (Variable memory);

/// @notice Reads the RPC URL.
function getRpcUrl(uint256 chainId) public view returns (string memory);
function getRpcUrl() public view returns (string memory);

/// @notice Returns the numerical chain ids for all configured chains.
function getChainIds() public view returns (uint256[] memory);

StdConfig supports bidirectional (read + write capabilities) configuration management:

  • The constructor writeToFile parameter enables automatic persistence of changes.
  • Use function writeUpdatesBackToFile(bool) to toggle write behavior at runtime.
  • All setter methods will update memory (state), but will only write updates back to the TOML file if the flag is enabled.
// SETTER FUNCTIONS

/// @notice   Sets a value for a given key. Overloaded for all supported types and their arrays.
/// @dev      Caches value and writes the it back to the TOML file if `writeToFile` is enabled.
function set(uint256 chainId, string memory key, <type> value) public;

/// @notice Enable or disable automatic writing to the TOML file on `set`.
function writeUpdatesBackToFile(bool enabled) public;

Usage example

NOTE: we use solc ^0.8.13, so that we can globally declare using LibVariable for Variable, which means devs only need to inherit Config and are all set.

contract MyTest is Test, Config {
    function setUp() public {
        // Loads config and creates forks for all chains defined in the TOML.
		// We set `writeToFile = false` cause we don't want to update the TOML file.
        _loadConfigAndForks("./test/fixtures/config.toml", false);
    }

    function test_readSingleChainValues() public {
        // The default chain is the last one from the config.
        // Let's switch to mainnet to read its values.
        vm.selectFork(forkOf[1]);

        // Retrieve a 'uint256' value. Reverts if not found or not a uint.
        uint256 myNumber = config.get("important_number").toUint256();
    }

    function test_readMultiChainValues() public {
        // Read WETH address from Mainnet (chain ID 1)
        vm.selectFork(forkOf[1]);
        address wethMainnet = config.get("weth").toAddress();

        // Read WETH address from Optimism (chain ID 10)
        vm.selectFork(forkOf[10]);
        address wethOptimism = config.get("weth").toAddress();

        // You can now use the chain-specific variables in your test
        assertEq(wethMainnet, 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2);
        assertEq(wethOptimism, 0x4200000000000000000000000000000000000006);
    }

	function test_writeConfig() public {
		// Manually enable as it was set to `false` in the constructor.
		config.writeToFile(true);

        // Changes are automatically persisted to the TOML file
        config.set("my_address", 0x1234...);
        config.set("is_deployed", true);

        // Verify changes were written
        string memory content = vm.readFile("./config.toml");
        address saved = vm.parseTomlAddress(content, "$.mainnet.address.my_address");
        assertEq(saved, 0x1234...);
        address isDeployed = vm.parseTomlBool(content, "$.mainnet.bool.is_deployed");
        assertEq(isDeployed);
    }
}

src/Base.sol Outdated
Comment on lines 64 to 69
console.log("----------");
console.log(string.concat("Loading config from '", filePath, "'"));
config = new StdConfig(filePath);
vm.makePersistent(address(config));
console.log("Config successfully loaded");
console.log("----------");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since the StdConfig constructor impl uses try catch blocks and there may be several errors in the traces, i thought it would be useful to add some logs to easily identify the config loading block

@sakulstra
Copy link

Will this only allow for setting the rpc:

[mainnet]
endpoint_url = "${MAINNET_RPC}"

or will it be possible to also configure evm_version and other solc setting? For us this is still the biggest problem for multichain forge scripts.

@0xrusowsky
Copy link
Contributor Author

0xrusowsky commented Aug 27, 2025

Will this only allow for setting the rpc:

[mainnet]
endpoint_url = "${MAINNET_RPC}"

or will it be possible to also configure evm_version and other solc setting? For us this is still the biggest problem for multichain forge scripts.

i only thought of a endpoint_url as the single special config var, but supporting the evm_version makes sense too.

let me look into it and assess the feasibility of the impl with the rest of the team. I will definitely try to add support for it if possible, thanks for the suggestion.

@0xrusowsky 0xrusowsky self-assigned this Aug 27, 2025
@0xrusowsky 0xrusowsky moved this to Ready For Review in Foundry Aug 27, 2025
@0xrusowsky
Copy link
Contributor Author

@sakulstra are solc setting relevant to you even if the cheatcode is not available yet? if so, i could add them, but otherwise i'd wait until the requested cheatcode is supported

@sakulstra
Copy link

@0xrusowsky For us this has been a major issue since forever yes - not so much related to this cheatcode.
Although it seems like it would be a perfect fit to solve this.

Our main usecase is to calculate create2 addresses on the different chains within a single script and that's currently simply impossible if one chain requires london while the other requires cancun.

@sakulstra
Copy link

sakulstra commented Aug 27, 2025

@0xrusowsky just in case that was not clear - for us is no issue to w8 a few weeks or sth. No need to decouple this if it means additional work. As this problem exists for a while now, we've built lot's of workarounds which work okayish for the most part.

@0xrusowsky
Copy link
Contributor Author

@sakulstra yes, that's clear.
won't be included in this PR, but when we ship the new cheatcode it will me included here too.

good that we have the issue linked now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Ready For Review
Development

Successfully merging this pull request may close these issues.

6 participants