Commit c2cf701
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
13 files changed
Lines changed: 2075 additions & 16 deletions
File tree
- .github/workflows
- src
- test
- fixtures
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
33 | | - | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
34 | 42 | | |
35 | 43 | | |
36 | 44 | | |
| |||
48 | 56 | | |
49 | 57 | | |
50 | 58 | | |
51 | | - | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
52 | 65 | | |
53 | 66 | | |
54 | 67 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
6 | 10 | | |
7 | 11 | | |
8 | | - | |
9 | | - | |
| 12 | + | |
| 13 | + | |
10 | 14 | | |
11 | 15 | | |
12 | 16 | | |
| |||
20 | 24 | | |
21 | 25 | | |
22 | 26 | | |
23 | | - | |
| 27 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| 11 | + | |
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
| 15 | + | |
14 | 16 | | |
15 | 17 | | |
16 | 18 | | |
| 19 | + | |
17 | 20 | | |
18 | 21 | | |
19 | 22 | | |
| 23 | + | |
20 | 24 | | |
21 | 25 | | |
22 | 26 | | |
23 | 27 | | |
| 28 | + | |
24 | 29 | | |
25 | 30 | | |
26 | 31 | | |
| 32 | + | |
27 | 33 | | |
28 | 34 | | |
29 | 35 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
0 commit comments