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

grandizzy
grandizzy previously approved these changes Sep 3, 2025
Copy link
Contributor

@grandizzy grandizzy left a comment

Choose a reason for hiding this comment

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

lgtm!

optimizer = true
optimizer_runs = 200

# A list of solidity error codes to ignore.
# 3860 = init-code-size
ignored_error_codes = [3860]
Copy link
Contributor

Choose a reason for hiding this comment

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

for other reviewers - this already happens in master but not showing up as no ignored_error_codes specified

Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
Comment on lines +44 to +81
# -- OPTIMISM ------------------------------------

[optimism]
endpoint_url = "${OPTIMISM_RPC}"

[optimism.bool]
is_live = false
bool_array = [false, true]

[optimism.address]
weth = "${WETH_OPTIMISM}"
deps = [
"0x2222222222222222222222222222222222222222",
"0x3333333333333333333333333333333333333333",
]

[optimism.uint]
number = 9999
number_array = [1234, 5678]

[optimism.int]
signed_number = 9999
signed_number_array = [-1234, -5678]

[optimism.bytes32]
word = "0x000000000000000000000000000000000000000000000000000000000000270f" # 9999
word_array = [
"0x00000000000000000000000000000000000000000000000000000000000004d2", # 1234
"0x000000000000000000000000000000000000000000000000000000000000162e", # 5678
]

[optimism.bytes]
b = "0xdcba"
b_array = ["0xc0ffee", "0xbabe"]

[optimism.string]
str = "alice"
str_array = ["bob", "charlie"]
Copy link
Member

@zerosnacks zerosnacks Sep 3, 2025

Choose a reason for hiding this comment

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

Note, it may be useful to display an alternative style either here or in the documentation that TOML allows for:

dotted keys

[optimism]
endpoint_url = "${OPTIMISM_RPC}"
bool.is_live = false
bool.bool_array = [false, true]
address.weth = "${WETH_OPTIMISM}"
address.deps = [
    "0x2222222222222222222222222222222222222222",
    "0x3333333333333333333333333333333333333333",
]
uint.number = 9999
uint.number_array = [1234, 5678]
int.signed_number = 9999
int.signed_number_array = [-1234, -5678]
bytes32.word = "0x000000000000000000000000000000000000000000000000000000000000270f" # 9999
bytes32.word_array = [
    "0x00000000000000000000000000000000000000000000000000000000000004d2", # 1234
    "0x000000000000000000000000000000000000000000000000000000000000162e", # 5678
]
bytes.b = "0xdcba"
bytes.b_array = ["0xc0ffee", "0xbabe"]
string.str = "alice"
string.str_array = ["bob", "charlie"]

or as inline table

[optimism]
endpoint_url = "${OPTIMISM_RPC}"
bool = { is_live = false, bool_array = [false, true] }
address = { weth = "${WETH_OPTIMISM}", deps = [
    "0x2222222222222222222222222222222222222222",
    "0x3333333333333333333333333333333333333333",
] }
uint = { number = 9999, number_array = [1234, 5678] }
int = { signed_number = 9999, signed_number_array = [-1234, -5678] }
bytes32 = { word = "0x000000000000000000000000000000000000000000000000000000000000270f", word_array = [
    "0x00000000000000000000000000000000000000000000000000000000000004d2",
    "0x000000000000000000000000000000000000000000000000000000000000162e",
] }
bytes = { b = "0xdcba", b_array = ["0xc0ffee", "0xbabe"] }
string = { str = "alice", str_array = ["bob", "charlie"] }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that's great feedback, will make sure to add it to the docs

do we have any preference?

Copy link
Member

Choose a reason for hiding this comment

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

I think for compactness I think user may prefer the inline table style but it has some limitations (no inline comments for example) and some limitations around multiline.

Either way I think both are valid to show in documentation so users are aware.

Copy link
Member

@zerosnacks zerosnacks left a comment

Choose a reason for hiding this comment

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

Looks good! 👍

@grandizzy grandizzy merged commit c2cf701 into master Sep 5, 2025
23 checks passed
@grandizzy grandizzy deleted the rusowsky/config-helper branch September 5, 2025 13:48
@github-project-automation github-project-automation bot moved this from Ready For Review to Done in Foundry Sep 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

6 participants