Skip to content

Commit c2cf701

Browse files
feat: config helper contract (#715)
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 with`Test` 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`. ```toml # 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: ```solidity /// @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: ```solidity /// @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. ```solidity // 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. ```solidity // 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. ```solidity 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); } } ``` --------- Co-authored-by: zerosnacks <[email protected]>
1 parent 6bce154 commit c2cf701

File tree

13 files changed

+2075
-16
lines changed

13 files changed

+2075
-16
lines changed

.github/workflows/ci.yml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,15 @@ jobs:
3030
- uses: actions/checkout@v4
3131
- uses: foundry-rs/foundry-toolchain@v1
3232
- run: forge --version
33-
- run: forge build --skip test --deny-warnings ${{ matrix.flags }}
33+
- run: |
34+
case "${{ matrix.flags }}" in
35+
*"solc:0.8.0"* | *"solc:0.7"* | *"solc:0.6"*)
36+
forge build --skip test --skip Config --skip StdConfig --skip LibVariable --deny-warnings ${{ matrix.flags }}
37+
;;
38+
*)
39+
forge build --skip test --deny-warnings ${{ matrix.flags }}
40+
;;
41+
esac
3442
# via-ir compilation time checks.
3543
- if: contains(matrix.flags, '--via-ir')
3644
run: forge build --skip test --deny-warnings ${{ matrix.flags }} --contracts 'test/compilation/*'
@@ -48,7 +56,12 @@ jobs:
4856
with:
4957
version: ${{ matrix.toolchain }}
5058
- run: forge --version
51-
- run: forge test -vvv
59+
- run: |
60+
if [ "${{ matrix.toolchain }}" = "stable" ]; then
61+
forge test -vvv --no-match-path "test/Config.t.sol"
62+
else
63+
forge test -vvv
64+
fi
5265
5366
fmt:
5467
runs-on: ubuntu-latest

foundry.toml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
[profile.default]
2-
fs_permissions = [{ access = "read-write", path = "./"}]
2+
fs_permissions = [{ access = "read-write", path = "./" }]
33
optimizer = true
44
optimizer_runs = 200
55

6+
# A list of solidity error codes to ignore.
7+
# 3860 = init-code-size
8+
ignored_error_codes = [3860]
9+
610
[rpc_endpoints]
711
# The RPC URLs are modified versions of the default for testing initialization.
8-
mainnet = "https://eth.merkle.io" # Different API key.
9-
optimism_sepolia = "https://sepolia.optimism.io/" # Adds a trailing slash.
12+
mainnet = "https://eth.merkle.io" # Different API key.
13+
optimism_sepolia = "https://sepolia.optimism.io/" # Adds a trailing slash.
1014
arbitrum_one_sepolia = "https://sepolia-rollup.arbitrum.io/rpc/" # Adds a trailing slash.
1115
needs_undefined_env_var = "${UNDEFINED_RPC_URL_PLACEHOLDER}"
1216

@@ -20,4 +24,4 @@ multiline_func_header = 'attributes_first'
2024
quote_style = 'double'
2125
number_underscore = 'preserve'
2226
single_line_statement_blocks = 'preserve'
23-
ignore = ["src/console.sol", "src/console2.sol"]
27+
ignore = ["src/console.sol", "src/console2.sol"]

src/Base.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,28 @@ abstract contract CommonBase {
88
/// @dev Cheat code address.
99
/// Calculated as `address(uint160(uint256(keccak256("hevm cheat code"))))`.
1010
address internal constant VM_ADDRESS = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D;
11+
1112
/// @dev console.sol and console2.sol work by executing a staticcall to this address.
1213
/// Calculated as `address(uint160(uint88(bytes11("console.log"))))`.
1314
address internal constant CONSOLE = 0x000000000000000000636F6e736F6c652e6c6f67;
15+
1416
/// @dev Used when deploying with create2.
1517
/// Taken from https://github.com/Arachnid/deterministic-deployment-proxy.
1618
address internal constant CREATE2_FACTORY = 0x4e59b44847b379578588920cA78FbF26c0B4956C;
19+
1720
/// @dev The default address for tx.origin and msg.sender.
1821
/// Calculated as `address(uint160(uint256(keccak256("foundry default caller"))))`.
1922
address internal constant DEFAULT_SENDER = 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38;
23+
2024
/// @dev The address of the first contract `CREATE`d by a running test contract.
2125
/// When running tests, each test contract is `CREATE`d by `DEFAULT_SENDER` with nonce 1.
2226
/// Calculated as `VM.computeCreateAddress(VM.computeCreateAddress(DEFAULT_SENDER, 1), 1)`.
2327
address internal constant DEFAULT_TEST_CONTRACT = 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f;
28+
2429
/// @dev Deterministic deployment address of the Multicall3 contract.
2530
/// Taken from https://www.multicall3.com.
2631
address internal constant MULTICALL3_ADDRESS = 0xcA11bde05977b3631167028862bE2a173976CA11;
32+
2733
/// @dev The order of the secp256k1 curve.
2834
uint256 internal constant SECP256K1_ORDER =
2935
115792089237316195423570985008687907852837564279074904382605163141518161494337;

src/Config.sol

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.13;
3+
4+
import {console} from "./console.sol";
5+
import {StdConfig} from "./StdConfig.sol";
6+
import {CommonBase} from "./Base.sol";
7+
8+
/// @notice Boilerplate to streamline the setup of multi-chain environments.
9+
abstract contract Config is CommonBase {
10+
// -- STORAGE (CONFIG + CHAINS + FORKS) ------------------------------------
11+
12+
/// @dev Contract instance holding the data from the TOML config file.
13+
StdConfig internal config;
14+
15+
/// @dev Array of chain IDs for which forks have been created.
16+
uint256[] internal chainIds;
17+
18+
/// @dev A mapping from a chain ID to its initialized fork ID.
19+
mapping(uint256 => uint256) internal forkOf;
20+
21+
// -- HELPER FUNCTIONS -----------------------------------------------------
22+
23+
/// @notice Loads configuration from a file.
24+
///
25+
/// @dev This function instantiates a `Config` contract, caching all its config variables.
26+
///
27+
/// @param filePath: the path to the TOML configuration file.
28+
/// @param writeToFile: whether updates are written back to the TOML file.
29+
function _loadConfig(string memory filePath, bool writeToFile) internal {
30+
console.log("----------");
31+
console.log(string.concat("Loading config from '", filePath, "'"));
32+
config = new StdConfig(filePath, writeToFile);
33+
vm.makePersistent(address(config));
34+
console.log("Config successfully loaded");
35+
console.log("----------");
36+
}
37+
38+
/// @notice Loads configuration from a file and creates forks for each specified chain.
39+
///
40+
/// @dev This function instantiates a `Config` contract, caching all its config variables,
41+
/// reads the configured chain ids, and iterates through them to create a fork for each one.
42+
/// It also creates a map `forkOf[chainId] -> forkId` to easily switch between forks.
43+
///
44+
/// @param filePath: the path to the TOML configuration file.
45+
/// @param writeToFile: whether updates are written back to the TOML file.
46+
function _loadConfigAndForks(string memory filePath, bool writeToFile) internal {
47+
_loadConfig(filePath, writeToFile);
48+
49+
console.log("Setting up forks for the configured chains...");
50+
uint256[] memory chains = config.getChainIds();
51+
for (uint256 i = 0; i < chains.length; i++) {
52+
uint256 chainId = chains[i];
53+
uint256 forkId = vm.createFork(config.getRpcUrl(chainId));
54+
forkOf[chainId] = forkId;
55+
chainIds.push(chainId);
56+
}
57+
console.log("Forks successfully created");
58+
console.log("----------");
59+
}
60+
}

0 commit comments

Comments
 (0)