From bdcb2a462fccd484a52c4df6d4edd8ca8eaae5da Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Mon, 20 Oct 2025 21:10:04 +0200 Subject: [PATCH 1/7] feat: multi-evm support for multi-chain scripts --- .gitignore | 2 + foundry.toml | 16 + src/Config.sol | 426 ++++++++++++++- src/LibConfigView.sol | 100 ++++ src/StdConfig.sol | 121 +++-- src/Vm.sol | 11 + test/Config.t.sol | 622 ++++++++++++++++------ test/StdCheats.t.sol | 12 +- test/fixtures/config.toml | 2 + test/fixtures/config_uncompiled.toml | 6 + test/fixtures/config_write_forbidden.toml | 6 + test/mocks/MockCounter.sol | 19 + 12 files changed, 1131 insertions(+), 212 deletions(-) create mode 100644 src/LibConfigView.sol create mode 100644 test/fixtures/config_uncompiled.toml create mode 100644 test/fixtures/config_write_forbidden.toml create mode 100644 test/mocks/MockCounter.sol diff --git a/.gitignore b/.gitignore index 756106d3..54411aeb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ cache/ out/ +out-cancun/ +out-shanghai/ .vscode .idea diff --git a/foundry.toml b/foundry.toml index b68bd546..680a1b2a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,6 +7,22 @@ optimizer_runs = 200 # 3860 = init-code-size ignored_error_codes = [3860] +[profile.shanghai] +fs_permissions = [{ access = "read-write", path = "./" }] +optimizer = true +optimizer_runs = 200 +evm_version = "shanghai" +out = "out-shanghai" +ignored_error_codes = [3860] + +[profile.cancun] +fs_permissions = [{ access = "read-write", path = "./" }] +optimizer = true +optimizer_runs = 200 +evm_version = "cancun" +out = "out-cancun" +ignored_error_codes = [3860] + [rpc_endpoints] # The RPC URLs are modified versions of the default for testing initialization. mainnet = "https://eth.merkle.io" # Different API key. diff --git a/src/Config.sol b/src/Config.sol index 1c63c872..b6886edd 100644 --- a/src/Config.sol +++ b/src/Config.sol @@ -3,58 +3,450 @@ pragma solidity ^0.8.13; import {console} from "./console.sol"; import {StdConfig} from "./StdConfig.sol"; +import {ConfigView, LibConfigView} from "./LibConfigView.sol"; import {CommonBase} from "./Base.sol"; +import {VmSafe} from "./Vm.sol"; /// @notice Boilerplate to streamline the setup of multi-chain environments. abstract contract Config is CommonBase { - // -- STORAGE (CONFIG + CHAINS + FORKS) ------------------------------------ + using LibConfigView for ConfigView; + + // -- ERRORS --------------------------------------------------------------- + + error ForkNotLoaded(uint256 chainId); + error ForkNotActive(uint256 chainId); + error ProfileArtifactsNotFound(string profileName, string expectedPath); + error MultiChainConfig(uint256 numChains); - /// @dev Contract instance holding the data from the TOML config file. - StdConfig internal config; + // -- STORAGE (CONFIG + CHAINS + FORKS) ------------------------------------ /// @dev Array of chain IDs for which forks have been created. uint256[] internal chainIds; + /// @dev StdConfig instances for each chain, deployed with profile-specific evm versions. + /// Multiple chains may point to the same StdConfig instance if they share an EVM version. + mapping(uint256 => StdConfig) internal _chainConfig; + /// @dev A mapping from a chain ID to its initialized fork ID. mapping(uint256 => uint256) internal forkOf; - // -- HELPER FUNCTIONS ----------------------------------------------------- + /// @dev A mapping from a chain ID to its profile metadata. + mapping(uint256 => VmSafe.ProfileMetadata) internal profile; + + /// @dev Track which StdConfig was deployed for each EVM version (for deduplication). + mapping(string => StdConfig) internal std_configOf; + + // -- UTILITY FUNCTIONS ----------------------------------------------------- + + /// @notice Creates a ConfigView bound to a specific chain ID. + /// + /// @dev Use this to access configuration variables with a cleaner API. + /// Example: `configOf(chainId).get("my_key").toUint256()` + /// instead of: `_chainConfig[chainId].get(chainId, "my_key").toUint256()` + /// + /// @param chainId: the chain ID. + /// @return ConfigView struct bound to the chain's StdConfig instance. + function configOf(uint256 chainId) internal view isCached(chainId) returns (ConfigView memory) { + return ConfigView(_chainConfig[chainId], chainId); + } /// @notice Loads configuration from a file. /// - /// @dev This function instantiates a `Config` contract, caching all its config variables. + /// @dev This function instantiates a `StdConfig` contract from the default profile. + /// Only supports single-chain configurations. For multi-chain setups, use loadConfigAndForks. /// /// @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 { + function loadConfig(string memory filePath, bool writeToFile) internal { console.log("----------"); console.log(string.concat("Loading config from '", filePath, "'")); - config = new StdConfig(filePath, writeToFile); - vm.makePersistent(address(config)); + + // Parse TOML to get all chain keys + string memory tomlContent = vm.resolveEnv(vm.readFile(filePath)); + string[] memory chainKeys = vm.parseTomlKeys(tomlContent, "$"); + require(chainKeys.length > 0, "Config: no chains found in TOML"); + + // Filter out non-table keys and count actual chains + uint256 numChains = 0; + uint256[] memory chainIdList = new uint256[](chainKeys.length); + string[] memory chainKeyList = new string[](chainKeys.length); + + for (uint256 i = 0; i < chainKeys.length; i++) { + if (vm.parseTomlKeys(tomlContent, string.concat("$.", chainKeys[i])).length > 0) { + chainKeyList[numChains] = chainKeys[i]; + chainIdList[numChains] = _resolveChainId(chainKeys[i]); + numChains++; + } + } + + // Revert if multiple chains are detected + if (numChains > 1) revert MultiChainConfig(numChains); + + // Load the single chain + string memory chainKey = chainKeyList[0]; + uint256 chainId = chainIdList[0]; + + // Get profile name for the chain + string memory profileName; + try vm.parseTomlString(tomlContent, string.concat("$.", chainKey, ".profile")) returns ( + string memory profileStr + ) { + profileName = profileStr; + } catch { + profileName = "default"; + } + + // Load profile metadata + profile[chainId] = vm.getProfile(profileName); + + // Get EVM version + string memory evmVersion = profile[chainId].evm; + + // Deploy StdConfig from the profile's artifact directory if not already deployed + if (address(std_configOf[evmVersion]) == address(0)) { + string memory artifact = string.concat( + profile[chainId].artifacts, + "/StdConfig.sol/StdConfig.json" + ); + + // Validate artifact exists + try vm.readFile(artifact) {} catch { + revert ProfileArtifactsNotFound(profileName, artifact); + } + + bytes memory constructorArgs = abi.encode(filePath, writeToFile, evmVersion); + address configAddr = vm.deployCode(artifact, constructorArgs); + std_configOf[evmVersion] = StdConfig(configAddr); + vm.makePersistent(configAddr); + } + + // Map this chain to its StdConfig instance + _chainConfig[chainId] = std_configOf[evmVersion]; + console.log("Config successfully loaded"); console.log("----------"); } /// @notice Loads configuration from a file and creates forks for each specified chain. /// - /// @dev This function instantiates a `Config` contract, caching all its config variables, - /// reads the configured chain ids, and iterates through them to create a fork for each one. - /// It also creates a map `forkOf[chainId] -> forkId` to easily switch between forks. + /// @dev This function deploys one StdConfig instance per unique EVM version from the + /// profile-specific artifact directory. Multiple chains sharing the same EVM version + /// will reuse the same StdConfig instance. Each StdConfig bytecode matches the EVM + /// version it will be called from. /// /// @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 { - _loadConfig(filePath, writeToFile); + function loadConfigAndForks(string memory filePath, bool writeToFile) internal { + console.log("----------"); + console.log(string.concat("Loading config from '", filePath, "'")); + + // Parse TOML to get chain keys + string memory tomlContent = vm.resolveEnv(vm.readFile(filePath)); + string[] memory chainKeys = vm.parseTomlKeys(tomlContent, "$"); console.log("Setting up forks for the configured chains..."); - uint256[] memory chains = config.getChainIds(); - for (uint256 i = 0; i < chains.length; i++) { - uint256 chainId = chains[i]; - uint256 forkId = vm.createFork(config.getRpcUrl(chainId)); + + // For each chain, load profile and ensure `StdConfig` is deployed for its EVM version + for (uint256 i = 0; i < chainKeys.length; i++) { + string memory chainKey = chainKeys[i]; + + // Ignore top-level keys that are not tables + if (vm.parseTomlKeys(tomlContent, string.concat("$.", chainKey)).length == 0) { + continue; + } + + // Get chain ID (from alias or parse as number) + uint256 chainId = _resolveChainId(chainKey); + + // Get profile name from TOML (with fallback to default profile) + string memory profileName; + try vm.parseTomlString(tomlContent, string.concat("$.", chainKey, ".profile")) returns ( + string memory profileStr + ) { + profileName = profileStr; + } catch { + // Use default profile if not specified + profileName = "default"; + } + + // Load profile metadata and cache EVM version and artifacts path + profile[chainId] = vm.getProfile(profileName); + + // Get EVM version for this chain + string memory evmVersion = profile[chainId].evm; + + // Deploy or reuse StdConfig based on EVM version + if (address(std_configOf[evmVersion]) == address(0)) { + // First chain with this EVM version - deploy new StdConfig + string memory artifact = string.concat( + profile[chainId].artifacts, + "/StdConfig.sol/StdConfig.json" + ); + + // Validate artifact exists + try vm.readFile(artifact) {} catch { + revert ProfileArtifactsNotFound(profileName, artifact); + } + + bytes memory constructorArgs = abi.encode(filePath, writeToFile, evmVersion); + address configAddr = vm.deployCode(artifact, constructorArgs); + std_configOf[evmVersion] = StdConfig(configAddr); + vm.makePersistent(configAddr); + } + + // Map this chain to its StdConfig instance (may be shared with other chains) + _chainConfig[chainId] = std_configOf[evmVersion]; + + // Create fork using this chain's RPC URL + uint256 forkId = vm.createFork(_chainConfig[chainId].getRpcUrl(chainId)); forkOf[chainId] = forkId; chainIds.push(chainId); } + console.log("Forks successfully created"); console.log("----------"); } + + /// @notice Selects the fork and sets the configured evm version associated with the requested chain ID. + /// + /// @dev This function is a simple wrapper around `vm.selectFork` and `vm.setEvmVersion` with an assertion to + /// make sure that the chain was previously loaded. + /// + /// @param chainId: the chain ID. + function selectFork(uint256 chainId) internal isCached(chainId) { + vm.selectFork(forkOf[chainId]); + vm.setEvmVersion(profile[chainId].evm); + } + + + // -- DEPLOYMENT HELPERS --------------------------------------------------- + + /// @notice Deploys a contract from an artifact file of the configured profile for the input chain. + /// Reverts if unable to find the artifact file that is derived from the inputs. + /// Reverts if the target artifact contains unlinked library placeholders. + /// Reverts if the fork of chain ID is not active. + /// + /// @param chainId: the chain ID. + /// @param contractFile: the file that contains the contract's source code (i.e. "Counter.sol"). + /// @param contractName: the name of the contract (i.e. "Counter"). + function deployCode( + uint256 chainId, + string memory contractFile, + string memory contractName + ) internal returns (address) { + string memory artifactPath = _getArtifactPath(chainId, contractFile, contractName); + return vm.deployCode(artifactPath); + } + + /// @notice Deploys a contract from an artifact file of the configured profile for the input chain. + /// Reverts if unable to find the artifact file that is derived from the inputs. + /// Reverts if the target artifact contains unlinked library placeholders. + /// Reverts if the fork of chain ID is not active. + /// + /// @param chainId: the chain ID. + /// @param contractFile: the file that contains the contract's source code (i.e. "Counter.sol"). + /// @param contractName: the name of the contract (i.e. "Counter"). + /// @param constructorArgs: abi-encoded constructor arguments. + function deployCode( + uint256 chainId, + string memory contractFile, + string memory contractName, + bytes memory constructorArgs + ) internal returns (address) { + string memory artifactPath = _getArtifactPath(chainId, contractFile, contractName); + return vm.deployCode(artifactPath, constructorArgs); + } + + /// @notice Deploys a contract from an artifact file of the configured profile for the input chain. + /// Reverts if unable to find the artifact file that is derived from the inputs. + /// Reverts if the target artifact contains unlinked library placeholders. + /// Reverts if the fork of chain ID is not active. + /// + /// @param chainId: the chain ID. + /// @param contractFile: the file that contains the contract's source code (i.e. "Counter.sol"). + /// @param contractName: the name of the contract (i.e. "Counter"). + /// @param value: `msg.value` + function deployCode( + uint256 chainId, + string memory contractFile, + string memory contractName, + uint256 value + ) internal returns (address) { + string memory artifactPath = _getArtifactPath(chainId, contractFile, contractName); + return vm.deployCode(artifactPath, value); + } + + /// @notice Deploys a contract from an artifact file of the configured profile for the input chain. + /// Reverts if unable to find the artifact file that is derived from the inputs. + /// Reverts if the target artifact contains unlinked library placeholders. + /// Reverts if the fork of chain ID is not active. + /// + /// @param chainId: the chain ID. + /// @param contractFile: the file that contains the contract's source code (i.e. "Counter.sol"). + /// @param contractName: the name of the contract (i.e. "Counter"). + /// @param constructorArgs: abi-encoded constructor arguments. + /// @param value: `msg.value` + function deployCode( + uint256 chainId, + string memory contractFile, + string memory contractName, + bytes memory constructorArgs, + uint256 value + ) internal returns (address) { + string memory artifactPath = _getArtifactPath(chainId, contractFile, contractName); + return vm.deployCode(artifactPath, constructorArgs, value); + } + + /// @notice Deploys a contract, using CREATE2, from an artifact file of the configured profile for the input chain. + /// Reverts if unable to find the artifact file that is derived from the inputs. + /// Reverts if the target artifact contains unlinked library placeholders. + /// Reverts if the fork of chain ID is not active. + /// + /// @param chainId: the chain ID. + /// @param contractFile: the file that contains the contract's source code (i.e. "Counter.sol"). + /// @param contractName: the name of the contract (i.e. "Counter"). + /// @param salt: the salt used in CREATE2. + function deployCode( + uint256 chainId, + string memory contractFile, + string memory contractName, + bytes32 salt + ) internal returns (address) { + string memory artifactPath = _getArtifactPath(chainId, contractFile, contractName); + return vm.deployCode(artifactPath, salt); + } + + /// @notice Deploys a contract, using CREATE2, from an artifact file of the configured profile for the input chain. + /// Reverts if unable to find the artifact file that is derived from the inputs. + /// Reverts if the target artifact contains unlinked library placeholders. + /// Reverts if the fork of chain ID is not active. + /// + /// @param chainId: the chain ID. + /// @param contractFile: the file that contains the contract's source code (i.e. "Counter.sol"). + /// @param contractName: the name of the contract (i.e. "Counter"). + /// @param constructorArgs: abi-encoded constructor arguments. + /// @param salt: the salt used in CREATE2. + function deployCode( + uint256 chainId, + string memory contractFile, + string memory contractName, + bytes memory constructorArgs, + bytes32 salt + ) internal returns (address) { + string memory artifactPath = _getArtifactPath(chainId, contractFile, contractName); + return vm.deployCode(artifactPath, constructorArgs, salt); + } + + /// @notice Deploys a contract, using CREATE2, from an artifact file of the configured profile for the input chain. + /// Reverts if unable to find the artifact file that is derived from the inputs. + /// Reverts if the target artifact contains unlinked library placeholders. + /// Reverts if the fork of chain ID is not active. + /// + /// @param chainId: the chain ID. + /// @param contractFile: the file that contains the contract's source code (i.e. "Counter.sol"). + /// @param contractName: the name of the contract (i.e. "Counter"). + /// @param value: `msg.value` + /// @param salt: the salt used in CREATE2. + function deployCode( + uint256 chainId, + string memory contractFile, + string memory contractName, + uint256 value, + bytes32 salt + ) internal returns (address) { + string memory artifactPath = _getArtifactPath(chainId, contractFile, contractName); + return vm.deployCode(artifactPath, value, salt); + } + + /// @notice Deploys a contract, using CREATE2, from an artifact file of the configured profile for the input chain. + /// Reverts if unable to find the artifact file that is derived from the inputs. + /// Reverts if the target artifact contains unlinked library placeholders. + /// Reverts if the fork of chain ID is not active. + /// + /// @param chainId: the chain ID. + /// @param contractFile: the file that contains the contract's source code (i.e. "Counter.sol"). + /// @param contractName: the name of the contract (i.e. "Counter"). + /// @param constructorArgs: abi-encoded constructor arguments. + /// @param value: `msg.value` + /// @param salt: the salt used in CREATE2. + function deployCode( + uint256 chainId, + string memory contractFile, + string memory contractName, + bytes memory constructorArgs, + uint256 value, + bytes32 salt + ) internal returns (address) { + string memory artifactPath = _getArtifactPath(chainId, contractFile, contractName); + return vm.deployCode(artifactPath, constructorArgs, value, salt); + } + + // -- INTERNAL HELPER FUNCTIONS AND MODIFIERS ------------------------------ + + /// @dev Resolves a chain key to a chain ID (handles both numeric IDs and aliases). + function _resolveChainId(string memory chainKey) private view returns (uint256) { + try vm.parseUint(chainKey) returns (uint256 id) { + return id; + } catch { + VmSafe.Chain memory chainInfo = vm.getChain(chainKey); + return chainInfo.chainId; + } + } + + /// @notice Returns the RPC URL for the requested chain ID. + /// + /// @dev This function returns the RPC URL from the chain's StdConfig instance. + /// + /// @param chainId: the chain ID. + /// @return The RPC URL for the chain. + function _getRpcUrl(uint256 chainId) internal view isCached(chainId) returns (string memory) { + return _chainConfig[chainId].getRpcUrl(chainId); + } + + /// @notice Constructs the artifact path for a contract and validates it has no unresolved libraries. + /// Reverts if the fork of chain ID is not active. + /// + /// @param chainId: the chain ID. + /// @param contractFile: the file that contains the contract's source code (i.e. "Counter.sol"). + /// @param contractName: the contract name (i.e. "Counter"). + /// @return The full path to the artifact JSON file. + function _getArtifactPath( + uint256 chainId, + string memory contractFile, + string memory contractName + ) internal view isActive(chainId) returns (string memory) { + return string.concat( + profile[chainId].artifacts, + "/", + contractFile, + "/", + contractName, + ".json" + ); + } + + function _assertCached(uint256 chainId) internal view { + bool found; + for (uint256 i = 0; i < chainIds.length; i++) { + if (chainId == chainIds[i]) { + found = true; + break; + } + } + if (!found) revert ForkNotLoaded(chainId); + } + + function _assertActive(uint256 chainId) internal view { + if (forkOf[chainId] != vm.activeFork()) revert ForkNotActive(chainId); + } + + modifier isCached(uint256 chainId) { + _assertCached(chainId); + _; + } + + modifier isActive(uint256 chainId) { + _assertActive(chainId); + _; + } } diff --git a/src/LibConfigView.sol b/src/LibConfigView.sol new file mode 100644 index 00000000..5a7e2fbc --- /dev/null +++ b/src/LibConfigView.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {StdConfig} from "./StdConfig.sol"; +import {Variable} from "./LibVariable.sol"; + +/// @notice A view into a StdConfig instance bound to a specific chain ID. +/// Provides ergonomic access to configuration variables without repeating the chain ID. +struct ConfigView { + StdConfig stdConfig; + uint256 chainId; +} + +/// @notice Library providing helper methods for ConfigView. +/// All methods delegate to StdConfig, automatically passing the bound chainId. +library LibConfigView { + // -- GETTER --------------------------------------------------------------- + + /// @notice Reads a configuration variable for the bound chain ID. + /// @param self The ConfigView instance. + /// @param key The configuration variable key. + /// @return Variable struct containing the type and ABI-encoded value. + function get(ConfigView memory self, string memory key) internal view returns (Variable memory) { + return self.stdConfig.get(self.chainId, key); + } + + // -- SETTERS (SINGLE VALUES) ---------------------------------------------- + + /// @notice Sets a boolean configuration variable. + function set(ConfigView memory self, string memory key, bool value) internal { + self.stdConfig.set(self.chainId, key, value); + } + + /// @notice Sets an address configuration variable. + function set(ConfigView memory self, string memory key, address value) internal { + self.stdConfig.set(self.chainId, key, value); + } + + /// @notice Sets a bytes32 configuration variable. + function set(ConfigView memory self, string memory key, bytes32 value) internal { + self.stdConfig.set(self.chainId, key, value); + } + + /// @notice Sets a uint256 configuration variable. + function set(ConfigView memory self, string memory key, uint256 value) internal { + self.stdConfig.set(self.chainId, key, value); + } + + /// @notice Sets an int256 configuration variable. + function set(ConfigView memory self, string memory key, int256 value) internal { + self.stdConfig.set(self.chainId, key, value); + } + + /// @notice Sets a string configuration variable. + function set(ConfigView memory self, string memory key, string memory value) internal { + self.stdConfig.set(self.chainId, key, value); + } + + /// @notice Sets a bytes configuration variable. + function set(ConfigView memory self, string memory key, bytes memory value) internal { + self.stdConfig.set(self.chainId, key, value); + } + + // -- SETTERS (ARRAYS) ----------------------------------------------------- + + /// @notice Sets a boolean array configuration variable. + function set(ConfigView memory self, string memory key, bool[] memory value) internal { + self.stdConfig.set(self.chainId, key, value); + } + + /// @notice Sets an address array configuration variable. + function set(ConfigView memory self, string memory key, address[] memory value) internal { + self.stdConfig.set(self.chainId, key, value); + } + + /// @notice Sets a bytes32 array configuration variable. + function set(ConfigView memory self, string memory key, bytes32[] memory value) internal { + self.stdConfig.set(self.chainId, key, value); + } + + /// @notice Sets a uint256 array configuration variable. + function set(ConfigView memory self, string memory key, uint256[] memory value) internal { + self.stdConfig.set(self.chainId, key, value); + } + + /// @notice Sets an int256 array configuration variable. + function set(ConfigView memory self, string memory key, int256[] memory value) internal { + self.stdConfig.set(self.chainId, key, value); + } + + /// @notice Sets a string array configuration variable. + function set(ConfigView memory self, string memory key, string[] memory value) internal { + self.stdConfig.set(self.chainId, key, value); + } + + /// @notice Sets a bytes array configuration variable. + function set(ConfigView memory self, string memory key, bytes[] memory value) internal { + self.stdConfig.set(self.chainId, key, value); + } +} diff --git a/src/StdConfig.sol b/src/StdConfig.sol index 506ac34a..3c7f5949 100644 --- a/src/StdConfig.sol +++ b/src/StdConfig.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; -import {VmSafe} from "./Vm.sol"; import {Variable, Type, TypeKind, LibVariable} from "./LibVariable.sol"; +import {VmSafe} from "./Vm.sol"; /// @notice A contract that parses a toml configuration file and load its /// variables into storage, automatically casting them, on deployment. @@ -53,16 +53,22 @@ contract StdConfig { /// @dev Path to the loaded TOML configuration file. string private _filePath; - /// @dev List of top-level keys found in the TOML file, assumed to be chain names/aliases. - string[] private _chainKeys; + /// @dev List of chain IDs loaded from the TOML file. + uint256[] private _chainIds; + + /// @dev Storage for the TOML key of each chain. + mapping(uint256 => string) private _keyOf; + + /// @dev Storage for the profile metadata necessary for multi-chain deployments. + mapping(uint256 => VmSafe.ProfileMetadata) private _profileOf; - /// @dev Storage for the configured RPC URL for each chain. + /// @dev Storage for the configured RPC URL of each chain. mapping(uint256 => string) private _rpcOf; - /// @dev Storage for values, organized by chain ID and variable key. + /// @dev Storage for values, indexed by chain ID and variable key. mapping(uint256 => mapping(string => bytes)) private _dataOf; - /// @dev Type cache for runtime checking when casting. + /// @dev Type cache for runtime checking when casting, indexed by chain ID and variable key. mapping(uint256 => mapping(string => Type)) private _typeOf; /// @dev When enabled, `set` will always write updates back to the configuration file. @@ -81,9 +87,13 @@ contract StdConfig { /// and if that fails, as an array of that type. If a variable cannot be /// parsed as either, the constructor will revert with an error. /// + /// This `StdConfig` instance will only manage chains that have a profile with + /// an EVM version matching the provided evmVersion parameter. + /// /// @param configFilePath: The local path to the TOML configuration file. /// @param writeToFile: Whether to write updates back to the TOML file. Only for scripts. - constructor(string memory configFilePath, bool writeToFile) { + /// @param evmVersion: The EVM version this StdConfig should manage (e.g., "shanghai", "cancun"). + constructor(string memory configFilePath, bool writeToFile, string memory evmVersion) { if (writeToFile && !vm.isContext(VmSafe.ForgeContext.ScriptGroup)) { revert WriteToFileInForbiddenCtxt(); } @@ -101,9 +111,28 @@ contract StdConfig { continue; } uint256 chainId = resolveChainId(chain_key); - _chainKeys.push(chain_key); - // Cache the configure rpc endpoint for that chain. + // Cache the configured profile metadata for that chain. + // Falls back to the currently active profile. Panics if the profile name doesn't exist. + VmSafe.ProfileMetadata memory chainProfile; + try vm.parseTomlString(content, string.concat("$.", chain_key, ".profile")) returns (string memory profile) + { + chainProfile = vm.getProfile(profile); + } catch { + chainProfile = vm.getProfile(); + } + + // Only load chains that match this StdConfig's EVM version + if (keccak256(bytes(chainProfile.evm)) != keccak256(bytes(evmVersion))) { + continue; + } + + // This chain matches our EVM version, load it + _chainIds.push(chainId); + _profileOf[chainId] = chainProfile; + _keyOf[chainId] = chain_key; + + // Cache the configured rpc endpoint for that chain. // Falls back to `[rpc_endpoints]`. Panics if no rpc endpoint is configured. try vm.parseTomlString(content, string.concat("$.", chain_key, ".endpoint_url")) returns (string memory url) { @@ -261,12 +290,9 @@ contract StdConfig { /// @dev Retrieves the chain key/alias from the configuration based on the chain ID. function _getChainKeyFromId(uint256 chainId) private view returns (string memory) { - for (uint256 i = 0; i < _chainKeys.length; i++) { - if (resolveChainId(_chainKeys[i]) == chainId) { - return _chainKeys[i]; - } - } - revert ChainNotInitialized(chainId); + string memory key = _keyOf[chainId]; + if (bytes(key).length == 0) revert ChainNotInitialized(chainId); + return key; } /// @dev Ensures type consistency when setting a value - prevents changing types unless uninitialized. @@ -297,6 +323,17 @@ contract StdConfig { vm.writeToml(jsonValue, _filePath, valueKey); } + /// @dev Validates that a chain has been initialized in this StdConfig instance. + function _isCached(uint256 chainId) private view { + if (bytes(_keyOf[chainId]).length == 0) revert ChainNotInitialized(chainId); + } + + /// @dev Modifier to ensure chain is initialized before accessing its data. + modifier isCached(uint256 chainId) { + _isCached(chainId); + _; + } + // -- GETTER FUNCTIONS ----------------------------------------------------- /// @dev Reads a variable for a given chain id and key, and returns it in a generic container. @@ -306,7 +343,12 @@ contract StdConfig { /// @param chain_id The chain ID to read from. /// @param key The key of the variable to retrieve. /// @return `Variable` struct containing the type and the ABI-encoded value. - function get(uint256 chain_id, string memory key) public view returns (Variable memory) { + function get(uint256 chain_id, string memory key) + public + view + isCached(chain_id) + returns (Variable memory) + { return Variable(_typeOf[chain_id][key], _dataOf[chain_id][key]); } @@ -322,14 +364,7 @@ contract StdConfig { /// @notice Returns the numerical chain ids for all configured chains. function getChainIds() public view returns (uint256[] memory) { - string[] memory keys = _chainKeys; - - uint256[] memory ids = new uint256[](keys.length); - for (uint256 i = 0; i < keys.length; i++) { - ids[i] = resolveChainId(keys[i]); - } - - return ids; + return _chainIds; } /// @notice Reads the RPC URL for a specific chain id. @@ -342,11 +377,21 @@ contract StdConfig { return _rpcOf[vm.getChainId()]; } + /// @notice Reads the profile metadata for a specific chain id. + function getProfile(uint256 chainId) public view returns (VmSafe.ProfileMetadata memory) { + return _profileOf[chainId]; + } + + /// @notice Reads the profile metadata for the current chain. + function getProfile() public view returns (VmSafe.ProfileMetadata memory) { + return _profileOf[vm.getChainId()]; + } + // -- SETTER FUNCTIONS (SINGLE VALUES) ------------------------------------- /// @notice Sets a boolean value for a given key and chain ID. /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, bool value) public { + function set(uint256 chainId, string memory key, bool value) public isCached(chainId) { Type memory ty = Type(TypeKind.Bool, false); _ensureTypeConsistency(chainId, key, ty); _dataOf[chainId][key] = abi.encode(value); @@ -361,7 +406,7 @@ contract StdConfig { /// @notice Sets an address value for a given key and chain ID. /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, address value) public { + function set(uint256 chainId, string memory key, address value) public isCached(chainId) { Type memory ty = Type(TypeKind.Address, false); _ensureTypeConsistency(chainId, key, ty); _dataOf[chainId][key] = abi.encode(value); @@ -376,7 +421,7 @@ contract StdConfig { /// @notice Sets a bytes32 value for a given key and chain ID. /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, bytes32 value) public { + function set(uint256 chainId, string memory key, bytes32 value) public isCached(chainId) { Type memory ty = Type(TypeKind.Bytes32, false); _ensureTypeConsistency(chainId, key, ty); _dataOf[chainId][key] = abi.encode(value); @@ -391,7 +436,7 @@ contract StdConfig { /// @notice Sets a uint256 value for a given key and chain ID. /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, uint256 value) public { + function set(uint256 chainId, string memory key, uint256 value) public isCached(chainId) { Type memory ty = Type(TypeKind.Uint256, false); _ensureTypeConsistency(chainId, key, ty); _dataOf[chainId][key] = abi.encode(value); @@ -405,7 +450,7 @@ contract StdConfig { } /// @notice Sets an int256 value for a given key and chain ID. - function set(uint256 chainId, string memory key, int256 value) public { + function set(uint256 chainId, string memory key, int256 value) public isCached(chainId) { Type memory ty = Type(TypeKind.Int256, false); _ensureTypeConsistency(chainId, key, ty); _dataOf[chainId][key] = abi.encode(value); @@ -419,7 +464,7 @@ contract StdConfig { /// @notice Sets a string value for a given key and chain ID. /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, string memory value) public { + function set(uint256 chainId, string memory key, string memory value) public isCached(chainId) { Type memory ty = Type(TypeKind.String, false); _ensureTypeConsistency(chainId, key, ty); _dataOf[chainId][key] = abi.encode(value); @@ -434,7 +479,7 @@ contract StdConfig { /// @notice Sets a bytes value for a given key and chain ID. /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, bytes memory value) public { + function set(uint256 chainId, string memory key, bytes memory value) public isCached(chainId) { Type memory ty = Type(TypeKind.Bytes, false); _ensureTypeConsistency(chainId, key, ty); _dataOf[chainId][key] = abi.encode(value); @@ -451,7 +496,7 @@ contract StdConfig { /// @notice Sets a boolean array for a given key and chain ID. /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, bool[] memory value) public { + function set(uint256 chainId, string memory key, bool[] memory value) public isCached(chainId) { Type memory ty = Type(TypeKind.Bool, true); _ensureTypeConsistency(chainId, key, ty); _dataOf[chainId][key] = abi.encode(value); @@ -474,7 +519,7 @@ contract StdConfig { /// @notice Sets an address array for a given key and chain ID. /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, address[] memory value) public { + function set(uint256 chainId, string memory key, address[] memory value) public isCached(chainId) { Type memory ty = Type(TypeKind.Address, true); _ensureTypeConsistency(chainId, key, ty); _dataOf[chainId][key] = abi.encode(value); @@ -497,7 +542,7 @@ contract StdConfig { /// @notice Sets a bytes32 array for a given key and chain ID. /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, bytes32[] memory value) public { + function set(uint256 chainId, string memory key, bytes32[] memory value) public isCached(chainId) { Type memory ty = Type(TypeKind.Bytes32, true); _ensureTypeConsistency(chainId, key, ty); _dataOf[chainId][key] = abi.encode(value); @@ -520,7 +565,7 @@ contract StdConfig { /// @notice Sets a uint256 array for a given key and chain ID. /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, uint256[] memory value) public { + function set(uint256 chainId, string memory key, uint256[] memory value) public isCached(chainId) { Type memory ty = Type(TypeKind.Uint256, true); _ensureTypeConsistency(chainId, key, ty); _dataOf[chainId][key] = abi.encode(value); @@ -543,7 +588,7 @@ contract StdConfig { /// @notice Sets a int256 array for a given key and chain ID. /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, int256[] memory value) public { + function set(uint256 chainId, string memory key, int256[] memory value) public isCached(chainId) { Type memory ty = Type(TypeKind.Int256, true); _ensureTypeConsistency(chainId, key, ty); _dataOf[chainId][key] = abi.encode(value); @@ -566,7 +611,7 @@ contract StdConfig { /// @notice Sets a string array for a given key and chain ID. /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, string[] memory value) public { + function set(uint256 chainId, string memory key, string[] memory value) public isCached(chainId) { Type memory ty = Type(TypeKind.String, true); _ensureTypeConsistency(chainId, key, ty); _dataOf[chainId][key] = abi.encode(value); @@ -589,7 +634,7 @@ contract StdConfig { /// @notice Sets a bytes array for a given key and chain ID. /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, bytes[] memory value) public { + function set(uint256 chainId, string memory key, bytes[] memory value) public isCached(chainId) { Type memory ty = Type(TypeKind.Bytes, true); _ensureTypeConsistency(chainId, key, ty); _dataOf[chainId][key] = abi.encode(value); diff --git a/src/Vm.sol b/src/Vm.sol index c028bc80..ddfb752b 100644 --- a/src/Vm.sol +++ b/src/Vm.sol @@ -341,6 +341,14 @@ interface VmSafe { bytes32[] storageKeys; } + /// Represents metadata derived from `foundry.toml` for a given profile. + struct ProfileMetadata { + /// The output path of the generated artifacts. + string artifacts; + /// The EVM version. + string evm; + } + // ======== Crypto ======== /// Derives a private key from the name, labels the account with that name, and returns the wallet. @@ -2022,6 +2030,9 @@ interface VmSafe { /// Encodes a `string` value to a base64 string. function toBase64(string calldata data) external pure returns (string memory); + + function getProfile() external returns (ProfileMetadata memory); + function getProfile(string calldata profile) external returns (ProfileMetadata memory); } /// The `Vm` interface does allow manipulation of the EVM state. These are all intended to be used diff --git a/test/Config.t.sol b/test/Config.t.sol index 8e2342ca..cf1705e8 100644 --- a/test/Config.t.sol +++ b/test/Config.t.sol @@ -2,10 +2,13 @@ pragma solidity ^0.8.13; import {Test} from "../src/Test.sol"; +import {VmSafe} from "../src/Vm.sol"; import {Config} from "../src/Config.sol"; import {StdConfig} from "../src/StdConfig.sol"; +import {ConfigView, LibConfigView} from "../src/LibConfigView.sol"; contract ConfigTest is Test, Config { + using LibConfigView for ConfigView; function setUp() public { vm.setEnv("MAINNET_RPC", "https://eth.llamarpc.com"); vm.setEnv("WETH_MAINNET", "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); @@ -13,107 +16,178 @@ contract ConfigTest is Test, Config { vm.setEnv("WETH_OPTIMISM", "0x4200000000000000000000000000000000000006"); } + function _endsWith(string memory str, string memory suffix) internal pure returns (bool) { + bytes memory strBytes = bytes(str); + bytes memory suffixBytes = bytes(suffix); + + if (suffixBytes.length > strBytes.length) { + return false; + } + + uint256 offset = strBytes.length - suffixBytes.length; + for (uint256 i = 0; i < suffixBytes.length; i++) { + if (strBytes[offset + i] != suffixBytes[i]) { + return false; + } + } + return true; + } + + function getArtifactPath( + uint256 chainId, + string memory contractFile, + string memory contractName + ) external view returns (string memory) { + return _getArtifactPath(chainId, contractFile, contractName); + } + + function getRpcUrl(uint256 chainId) external view returns (string memory) { + return _getRpcUrl(chainId); + } + + function _createMinimalSingleChainConfig( + string memory path, + string memory chainKey, + string memory profileName + ) internal { + vm.writeFile( + path, + string.concat( + "[", chainKey, "]\n", + "endpoint_url = \"${MAINNET_RPC}\"\n", + "profile = \"", profileName, "\"\n\n", + "[", chainKey, ".bool]\n", + "is_live = true\n" + ) + ); + } + + function _createFullSingleChainConfig( + string memory path, + string memory chainKey, + string memory profileName + ) internal { + vm.writeFile( + path, + string.concat( + "[", chainKey, "]\n", + "endpoint_url = \"${MAINNET_RPC}\"\n", + "profile = \"", profileName, "\"\n\n", + "[", chainKey, ".bool]\n", + "is_live = true\n", + "bool_array = [true, false]\n\n", + "[", chainKey, ".address]\n", + "weth = \"${WETH_MAINNET}\"\n", + "deps = [\n", + " \"0x0000000000000000000000000000000000000000\",\n", + " \"0x1111111111111111111111111111111111111111\",\n", + "]\n\n", + "[", chainKey, ".uint]\n", + "number = 1234\n", + "number_array = [5678, 9999]\n\n", + "[", chainKey, ".int]\n", + "signed_number = -1234\n", + "signed_number_array = [-5678, 9999]\n\n", + "[", chainKey, ".bytes32]\n", + "word = \"0x00000000000000000000000000000000000000000000000000000000000004d2\"\n", + "word_array = [\n", + " \"0x000000000000000000000000000000000000000000000000000000000000162e\",\n", + " \"0x000000000000000000000000000000000000000000000000000000000000270f\",\n", + "]\n\n", + "[", chainKey, ".bytes]\n", + "b = \"0xabcd\"\n", + "b_array = [\"0xdead\", \"0xbeef\"]\n\n", + "[", chainKey, ".string]\n", + "str = \"foo\"\n", + "str_array = [\"bar\", \"baz\"]\n" + ) + ); + } + + function _createInvalidChainConfig(string memory path) internal { + vm.writeFile( + path, + string.concat( + "[mainnet]\n", + "endpoint_url = \"https://eth.llamarpc.com\"\n", + "profile = \"shanghai\"\n", + "\n", + "[mainnet.uint]\n", + "valid_number = 123\n", + "\n", + "# Invalid chain key (not a number and not a valid alias)\n", + "[invalid_chain]\n", + "endpoint_url = \"https://invalid.com\"\n", + "\n", + "[invalid_chain_9999.uint]\n", + "some_value = 456\n" + ) + ); + } + + function _createUnparsableConfig(string memory path) internal { + vm.writeFile( + path, + string.concat( + "[mainnet]\n", + "endpoint_url = \"https://eth.llamarpc.com\"\n", + "profile = \"shanghai\"\n", + "\n", + "[mainnet.uint]\n", + "bad_value = \"not_a_number\"\n" + ) + ); + } + function test_loadConfig() public { - // Deploy the config contract with the test fixture. - _loadConfig("./test/fixtures/config.toml", false); + string memory singleChainConfig = "./test/fixtures/config_single_chain.toml"; + _createFullSingleChainConfig(singleChainConfig, "mainnet", "shanghai"); + loadConfig(singleChainConfig, false); - // -- MAINNET -------------------------------------------------------------- + // -- MAINNET (single chain) ----------------------------------------------- - // Read and assert RPC URL for Mainnet (chain ID 1) - assertEq(config.getRpcUrl(1), "https://eth.llamarpc.com"); + assertEq(_chainConfig[1].getRpcUrl(1), "https://eth.llamarpc.com"); - // Read and assert boolean values - assertTrue(config.get(1, "is_live").toBool()); - bool[] memory bool_array = config.get(1, "bool_array").toBoolArray(); + assertTrue(_chainConfig[1].get(1, "is_live").toBool()); + bool[] memory bool_array = _chainConfig[1].get(1, "bool_array").toBoolArray(); assertTrue(bool_array[0]); assertFalse(bool_array[1]); - // Read and assert address values - assertEq(config.get(1, "weth").toAddress(), 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - address[] memory address_array = config.get(1, "deps").toAddressArray(); + assertEq(_chainConfig[1].get(1, "weth").toAddress(), 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + address[] memory address_array = _chainConfig[1].get(1, "deps").toAddressArray(); assertEq(address_array[0], 0x0000000000000000000000000000000000000000); assertEq(address_array[1], 0x1111111111111111111111111111111111111111); - // Read and assert bytes32 values - assertEq(config.get(1, "word").toBytes32(), bytes32(uint256(1234))); - bytes32[] memory bytes32_array = config.get(1, "word_array").toBytes32Array(); + assertEq(_chainConfig[1].get(1, "word").toBytes32(), bytes32(uint256(1234))); + bytes32[] memory bytes32_array = _chainConfig[1].get(1, "word_array").toBytes32Array(); assertEq(bytes32_array[0], bytes32(uint256(5678))); assertEq(bytes32_array[1], bytes32(uint256(9999))); - // Read and assert uint values - assertEq(config.get(1, "number").toUint256(), 1234); - uint256[] memory uint_array = config.get(1, "number_array").toUint256Array(); + assertEq(_chainConfig[1].get(1, "number").toUint256(), 1234); + uint256[] memory uint_array = _chainConfig[1].get(1, "number_array").toUint256Array(); assertEq(uint_array[0], 5678); assertEq(uint_array[1], 9999); - // Read and assert int values - assertEq(config.get(1, "signed_number").toInt256(), -1234); - int256[] memory int_array = config.get(1, "signed_number_array").toInt256Array(); + assertEq(_chainConfig[1].get(1, "signed_number").toInt256(), -1234); + int256[] memory int_array = _chainConfig[1].get(1, "signed_number_array").toInt256Array(); assertEq(int_array[0], -5678); assertEq(int_array[1], 9999); - // Read and assert bytes values - assertEq(config.get(1, "b").toBytes(), hex"abcd"); - bytes[] memory bytes_array = config.get(1, "b_array").toBytesArray(); + assertEq(_chainConfig[1].get(1, "b").toBytes(), hex"abcd"); + bytes[] memory bytes_array = _chainConfig[1].get(1, "b_array").toBytesArray(); assertEq(bytes_array[0], hex"dead"); assertEq(bytes_array[1], hex"beef"); - // Read and assert string values - assertEq(config.get(1, "str").toString(), "foo"); - string[] memory string_array = config.get(1, "str_array").toStringArray(); + assertEq(_chainConfig[1].get(1, "str").toString(), "foo"); + string[] memory string_array = _chainConfig[1].get(1, "str_array").toStringArray(); assertEq(string_array[0], "bar"); assertEq(string_array[1], "baz"); - // -- OPTIMISM ------------------------------------------------------------ - - // Read and assert RPC URL for Optimism (chain ID 10) - assertEq(config.getRpcUrl(10), "https://mainnet.optimism.io"); - - // Read and assert boolean values - assertFalse(config.get(10, "is_live").toBool()); - bool_array = config.get(10, "bool_array").toBoolArray(); - assertFalse(bool_array[0]); - assertTrue(bool_array[1]); - - // Read and assert address values - assertEq(config.get(10, "weth").toAddress(), 0x4200000000000000000000000000000000000006); - address_array = config.get(10, "deps").toAddressArray(); - assertEq(address_array[0], 0x2222222222222222222222222222222222222222); - assertEq(address_array[1], 0x3333333333333333333333333333333333333333); - - // Read and assert bytes32 values - assertEq(config.get(10, "word").toBytes32(), bytes32(uint256(9999))); - bytes32_array = config.get(10, "word_array").toBytes32Array(); - assertEq(bytes32_array[0], bytes32(uint256(1234))); - assertEq(bytes32_array[1], bytes32(uint256(5678))); - - // Read and assert uint values - assertEq(config.get(10, "number").toUint256(), 9999); - uint_array = config.get(10, "number_array").toUint256Array(); - assertEq(uint_array[0], 1234); - assertEq(uint_array[1], 5678); - - // Read and assert int values - assertEq(config.get(10, "signed_number").toInt256(), 9999); - int_array = config.get(10, "signed_number_array").toInt256Array(); - assertEq(int_array[0], -1234); - assertEq(int_array[1], -5678); - - // Read and assert bytes values - assertEq(config.get(10, "b").toBytes(), hex"dcba"); - bytes_array = config.get(10, "b_array").toBytesArray(); - assertEq(bytes_array[0], hex"c0ffee"); - assertEq(bytes_array[1], hex"babe"); - - // Read and assert string values - assertEq(config.get(10, "str").toString(), "alice"); - string_array = config.get(10, "str_array").toStringArray(); - assertEq(string_array[0], "bob"); - assertEq(string_array[1], "charlie"); + vm.removeFile(singleChainConfig); } function test_loadConfigAndForks() public { - _loadConfigAndForks("./test/fixtures/config.toml", false); + loadConfigAndForks("./test/fixtures/config.toml", false); // assert that the map of chain id and fork ids is created and that the chain ids actually match assertEq(forkOf[1], 0); @@ -132,37 +206,36 @@ contract ConfigTest is Test, Config { vm.copyFile(originalConfig, testConfig); // Deploy the config contract with the temporary fixture. - _loadConfig(testConfig, false); + // Use loadConfigAndForks since this test accesses both chains + loadConfigAndForks(testConfig, false); - // Enable writing to file bypassing the context check. - vm.store(address(config), bytes32(uint256(5)), bytes32(uint256(1))); + // Enable writing to file bypassing the context check for both StdConfig instances. + vm.store(address(_chainConfig[1]), bytes32(uint256(7)), bytes32(uint256(1))); // Shanghai StdConfig + vm.store(address(_chainConfig[10]), bytes32(uint256(7)), bytes32(uint256(1))); // Cancun StdConfig { - // Update a single boolean value and verify the change. - config.set(1, "is_live", false); + _chainConfig[1].set(1, "is_live", false); - assertFalse(config.get(1, "is_live").toBool()); + assertFalse(_chainConfig[1].get(1, "is_live").toBool()); string memory content = vm.readFile(testConfig); assertFalse(vm.parseTomlBool(content, "$.mainnet.bool.is_live")); - // Update a single address value and verify the change. address new_addr = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; - config.set(1, "weth", new_addr); + _chainConfig[1].set(1, "weth", new_addr); - assertEq(config.get(1, "weth").toAddress(), new_addr); + assertEq(_chainConfig[1].get(1, "weth").toAddress(), new_addr); content = vm.readFile(testConfig); assertEq(vm.parseTomlAddress(content, "$.mainnet.address.weth"), new_addr); - // Update a uint array and verify the change. uint256[] memory new_numbers = new uint256[](3); new_numbers[0] = 1; new_numbers[1] = 2; new_numbers[2] = 3; - config.set(10, "number_array", new_numbers); + _chainConfig[10].set(10, "number_array", new_numbers); - uint256[] memory updated_numbers_mem = config.get(10, "number_array").toUint256Array(); + uint256[] memory updated_numbers_mem = _chainConfig[10].get(10, "number_array").toUint256Array(); assertEq(updated_numbers_mem.length, 3); assertEq(updated_numbers_mem[0], 1); assertEq(updated_numbers_mem[1], 2); @@ -175,13 +248,12 @@ contract ConfigTest is Test, Config { assertEq(updated_numbers_disk[1], 2); assertEq(updated_numbers_disk[2], 3); - // Update a string array and verify the change. string[] memory new_strings = new string[](2); new_strings[0] = "hello"; new_strings[1] = "world"; - config.set(1, "str_array", new_strings); + _chainConfig[1].set(1, "str_array", new_strings); - string[] memory updated_strings_mem = config.get(1, "str_array").toStringArray(); + string[] memory updated_strings_mem = _chainConfig[1].get(1, "str_array").toStringArray(); assertEq(updated_strings_mem.length, 2); assertEq(updated_strings_mem[0], "hello"); assertEq(updated_strings_mem[1], "world"); @@ -192,29 +264,26 @@ contract ConfigTest is Test, Config { assertEq(updated_strings_disk[0], "hello"); assertEq(updated_strings_disk[1], "world"); - // Create a new uint variable and verify the change. - config.set(1, "new_uint", uint256(42)); + _chainConfig[1].set(1, "new_uint", uint256(42)); - assertEq(config.get(1, "new_uint").toUint256(), 42); + assertEq(_chainConfig[1].get(1, "new_uint").toUint256(), 42); content = vm.readFile(testConfig); assertEq(vm.parseTomlUint(content, "$.mainnet.uint.new_uint"), 42); - // Create a new int variable and verify the change. - config.set(1, "new_int", int256(-42)); + _chainConfig[1].set(1, "new_int", int256(-42)); - assertEq(config.get(1, "new_int").toInt256(), -42); + assertEq(_chainConfig[1].get(1, "new_int").toInt256(), -42); content = vm.readFile(testConfig); assertEq(vm.parseTomlInt(content, "$.mainnet.int.new_int"), -42); - // Create a new int array and verify the change. int256[] memory new_ints = new int256[](2); new_ints[0] = -100; new_ints[1] = 200; - config.set(10, "new_ints", new_ints); + _chainConfig[10].set(10, "new_ints", new_ints); - int256[] memory updated_ints_mem = config.get(10, "new_ints").toInt256Array(); + int256[] memory updated_ints_mem = _chainConfig[10].get(10, "new_ints").toInt256Array(); assertEq(updated_ints_mem.length, 2); assertEq(updated_ints_mem[0], -100); assertEq(updated_ints_mem[1], 200); @@ -225,13 +294,12 @@ contract ConfigTest is Test, Config { assertEq(updated_ints_disk[0], -100); assertEq(updated_ints_disk[1], 200); - // Create a new bytes32 array and verify the change. bytes32[] memory new_words = new bytes32[](2); new_words[0] = bytes32(uint256(0xDEAD)); new_words[1] = bytes32(uint256(0xBEEF)); - config.set(10, "new_words", new_words); + _chainConfig[10].set(10, "new_words", new_words); - bytes32[] memory updated_words_mem = config.get(10, "new_words").toBytes32Array(); + bytes32[] memory updated_words_mem = _chainConfig[10].get(10, "new_words").toBytes32Array(); assertEq(updated_words_mem.length, 2); assertEq(updated_words_mem[0], new_words[0]); assertEq(updated_words_mem[1], new_words[1]); @@ -243,110 +311,362 @@ contract ConfigTest is Test, Config { assertEq(vm.toString(updated_words_disk[1]), vm.toString(new_words[1])); } - // Clean up the temporary file. vm.removeFile(testConfig); } function test_writeUpdatesBackToFile() public { - // Create a temporary copy of the config file to avoid modifying the original. - string memory originalConfig = "./test/fixtures/config.toml"; string memory testConfig = "./test/fixtures/write_config.t.toml"; - vm.copyFile(originalConfig, testConfig); - - // Deploy the config contract with `writeToFile = false` (disabled). - _loadConfig(testConfig, false); + _createMinimalSingleChainConfig(testConfig, "mainnet", "shanghai"); + loadConfig(testConfig, false); // Update a single boolean value and verify the file is NOT changed. - config.set(1, "is_live", false); + _chainConfig[1].set(1, "is_live", false); string memory content = vm.readFile(testConfig); assertTrue(vm.parseTomlBool(content, "$.mainnet.bool.is_live"), "File should not be updated yet"); // Enable writing to file bypassing the context check. - vm.store(address(config), bytes32(uint256(5)), bytes32(uint256(1))); + vm.store(address(_chainConfig[1]), bytes32(uint256(7)), bytes32(uint256(1))); // Update the value again and verify the file IS changed. - config.set(1, "is_live", false); + _chainConfig[1].set(1, "is_live", false); content = vm.readFile(testConfig); assertFalse(vm.parseTomlBool(content, "$.mainnet.bool.is_live"), "File should be updated now"); // Disable writing to file. - config.writeUpdatesBackToFile(false); + _chainConfig[1].writeUpdatesBackToFile(false); // Update the value again and verify the file is NOT changed. - config.set(1, "is_live", true); + _chainConfig[1].set(1, "is_live", true); content = vm.readFile(testConfig); assertFalse(vm.parseTomlBool(content, "$.mainnet.bool.is_live"), "File should not be updated again"); - // Clean up the temporary file. vm.removeFile(testConfig); } function testRevert_WriteToFileInForbiddenCtxt() public { + string memory singleChainConfig = "./test/fixtures/config_write_forbidden.toml"; + _createMinimalSingleChainConfig(singleChainConfig, "mainnet", "shanghai"); + // Cannot initialize enabling writing to file unless we are in SCRIPT mode. vm.expectRevert(StdConfig.WriteToFileInForbiddenCtxt.selector); - _loadConfig("./test/fixtures/config.toml", true); + loadConfig(singleChainConfig, true); // Initialize with `writeToFile = false`. - _loadConfig("./test/fixtures/config.toml", false); + loadConfig(singleChainConfig, false); // Cannot enable writing to file unless we are in SCRIPT mode. vm.expectRevert(StdConfig.WriteToFileInForbiddenCtxt.selector); - config.writeUpdatesBackToFile(true); + _chainConfig[1].writeUpdatesBackToFile(true); + + vm.removeFile(singleChainConfig); } function testRevert_InvalidChainKey() public { - // Create a fixture with an invalid chain key string memory invalidChainConfig = "./test/fixtures/config_invalid_chain.toml"; - vm.writeFile( - invalidChainConfig, - string.concat( - "[mainnet]\n", - "endpoint_url = \"https://eth.llamarpc.com\"\n", - "\n", - "[mainnet.uint]\n", - "valid_number = 123\n", - "\n", - "# Invalid chain key (not a number and not a valid alias)\n", - "[invalid_chain]\n", - "endpoint_url = \"https://invalid.com\"\n", - "\n", - "[invalid_chain_9999.uint]\n", - "some_value = 456\n" - ) - ); + _createInvalidChainConfig(invalidChainConfig); vm.expectRevert(abi.encodeWithSelector(StdConfig.InvalidChainKey.selector, "invalid_chain")); - new StdConfig(invalidChainConfig, false); + new StdConfig(invalidChainConfig, false, "shanghai"); vm.removeFile(invalidChainConfig); } function testRevert_ChainNotInitialized() public { - _loadConfig("./test/fixtures/config.toml", false); + string memory singleChainConfig = "./test/fixtures/config_chain_init.toml"; + _createMinimalSingleChainConfig(singleChainConfig, "mainnet", "shanghai"); + loadConfig(singleChainConfig, false); // Enable writing to file bypassing the context check. - vm.store(address(config), bytes32(uint256(5)), bytes32(uint256(1))); + vm.store(address(_chainConfig[1]), bytes32(uint256(7)), bytes32(uint256(1))); - // Try to write a value for a non-existent chain ID + // Try to write a value for a non-existent chain ID through the shanghai StdConfig + // This should fail because the shanghai StdConfig only manages chain 1, not chain 999999 vm.expectRevert(abi.encodeWithSelector(StdConfig.ChainNotInitialized.selector, uint256(999999))); - config.set(999999, "some_key", uint256(123)); + _chainConfig[1].set(999999, "some_key", uint256(123)); + + vm.removeFile(singleChainConfig); } function testRevert_UnableToParseVariable() public { - // Create a temporary fixture with an unparsable variable string memory badParseConfig = "./test/fixtures/config_bad_parse.toml"; - vm.writeFile( - badParseConfig, - string.concat( - "[mainnet]\n", - "endpoint_url = \"https://eth.llamarpc.com\"\n", - "\n", - "[mainnet.uint]\n", - "bad_value = \"not_a_number\"\n" - ) - ); + _createUnparsableConfig(badParseConfig); vm.expectRevert(abi.encodeWithSelector(StdConfig.UnableToParseVariable.selector, "bad_value")); - new StdConfig(badParseConfig, false); + new StdConfig(badParseConfig, false, "shanghai"); vm.removeFile(badParseConfig); } + + // ========================================================================= + // MULTI-EVM PROFILE TESTS + // ========================================================================= + + function test_chainFilteringByEvmVersion() public { + loadConfigAndForks("./test/fixtures/config.toml", false); + + // Verify shanghai StdConfig only contains mainnet (chain 1) + uint256[] memory shanghaiChains = _chainConfig[1].getChainIds(); + assertEq(shanghaiChains.length, 1); + assertEq(shanghaiChains[0], 1); + + // Verify cancun StdConfig only contains optimism (chain 10) + uint256[] memory cancunChains = _chainConfig[10].getChainIds(); + assertEq(cancunChains.length, 1); + assertEq(cancunChains[0], 10); + + // Verify that the two StdConfig instances are different + assertTrue( + address(_chainConfig[1]) != address(_chainConfig[10]), + "Different EVM versions should have different StdConfig instances" + ); + } + + function test_configViewCleanAPI() public { + loadConfigAndForks("./test/fixtures/config.toml", false); + + bool isLiveOldWay = _chainConfig[1].get(1, "is_live").toBool(); + bool isLiveNewWay = configOf(1).get("is_live").toBool(); + address wethOldWay = _chainConfig[10].get(10, "weth").toAddress(); + address wethNewWay = configOf(10).get("weth").toAddress(); + + // Both ways should return the same values + assertEq(isLiveOldWay, isLiveNewWay); + assertEq(wethOldWay, wethNewWay); + + assertTrue(isLiveNewWay); + assertEq(wethNewWay, 0x4200000000000000000000000000000000000006); + } + + function testRevert_crossChainAccessDifferentEvmVersions() public { + loadConfigAndForks("./test/fixtures/config.toml", false); + + // Try to access chain 10 (cancun) through shanghai StdConfig (chain 1's instance) + // This should revert with ChainNotInitialized because shanghai StdConfig doesn't manage chain 10 + vm.expectRevert(abi.encodeWithSelector(StdConfig.ChainNotInitialized.selector, uint256(10))); + _chainConfig[1].get(10, "is_live"); + + // Try to access chain 1 (shanghai) through cancun StdConfig (chain 10's instance) + // This should revert with ChainNotInitialized because cancun StdConfig doesn't manage chain 1 + vm.expectRevert(abi.encodeWithSelector(StdConfig.ChainNotInitialized.selector, uint256(1))); + _chainConfig[10].get(1, "is_live"); + } + + function test_loadConfigWithProfiles() public { + // Use loadConfigAndForks since the fixture has multiple chains + loadConfigAndForks("./test/fixtures/config.toml", false); + + // Verify profiles are cached correctly for each chain + VmSafe.ProfileMetadata memory mainnetProfile = profile[1]; + assertEq(mainnetProfile.evm, "shanghai"); + assertTrue(_endsWith(mainnetProfile.artifacts, "out-shanghai")); + + VmSafe.ProfileMetadata memory optimismProfile = profile[10]; + assertEq(optimismProfile.evm, "cancun"); + assertTrue(_endsWith(optimismProfile.artifacts, "out-cancun")); + } + + function test_getProfileMetadata() public { + loadConfigAndForks("./test/fixtures/config.toml", false); + + // Test getting profile by chain ID + VmSafe.ProfileMetadata memory mainnetProfile = profile[1]; + assertEq(mainnetProfile.evm, "shanghai"); + + // Select fork and test getProfile() without chainId (uses active fork) + selectFork(10); + VmSafe.ProfileMetadata memory activeProfile = profile[vm.getChainId()]; + assertEq(activeProfile.evm, "cancun"); + } + + function test_selectFork() public { + loadConfigAndForks("./test/fixtures/config.toml", false); + + // Test selecting mainnet fork + selectFork(1); + assertEq(vm.activeFork(), forkOf[1]); + assertEq(vm.getChainId(), 1); + + // Test selecting optimism fork + selectFork(10); + assertEq(vm.activeFork(), forkOf[10]); + assertEq(vm.getChainId(), 10); + } + + function test_getRpcUrl() public { + loadConfigAndForks("./test/fixtures/config.toml", false); + + // Verify cached RPC URLs match the environment variables + assertEq(_getRpcUrl(1), "https://eth.llamarpc.com"); + assertEq(_getRpcUrl(10), "https://mainnet.optimism.io"); + + // Verify it works after switching forks and EVM versions + selectFork(1); + assertEq(_getRpcUrl(1), "https://eth.llamarpc.com"); + + selectFork(10); + assertEq(_getRpcUrl(10), "https://mainnet.optimism.io"); + } + + // Nececssary to assert the revert + function _selectFork(uint256 chainId) external { + selectFork(chainId); + } + + function testRevert_selectForkNotLoaded() public { + loadConfigAndForks("./test/fixtures/config.toml", false); + + // Try to select a fork that was not loaded + vm.expectRevert(abi.encodeWithSelector(Config.ForkNotLoaded.selector, uint256(999))); + this._selectFork(999); + } + + function testRevert_getRpcUrlNotLoaded() public { + loadConfigAndForks("./test/fixtures/config.toml", false); + + // Try to get RPC URL for a chain that was not loaded + vm.expectRevert(abi.encodeWithSelector(Config.ForkNotLoaded.selector, uint256(999))); + this.getRpcUrl(999); + } + + function test_getArtifactPath() public { + loadConfigAndForks("./test/fixtures/config.toml", false); + + // Select mainnet fork and get artifact path + selectFork(1); + string memory artifactPath = _getArtifactPath(1, "MockCounter.sol", "MockCounter"); + assertTrue(_endsWith(artifactPath, "out-shanghai/MockCounter.sol/MockCounter.json")); + + // Select optimism fork and get artifact path + selectFork(10); + artifactPath = _getArtifactPath(10, "MockCounter.sol", "MockCounter"); + assertTrue(_endsWith(artifactPath, "out-cancun/MockCounter.sol/MockCounter.json")); + } + + function testRevert_getArtifactPathForkNotActive() public { + loadConfigAndForks("./test/fixtures/config.toml", false); + + // Select mainnet fork + selectFork(1); + + // Try to get artifact path for optimism fork while mainnet is active + vm.expectRevert(abi.encodeWithSelector(Config.ForkNotActive.selector, uint256(10))); + this.getArtifactPath(10, "MockCounter.sol", "MockCounter"); + } + + function test_deployCodeBasic() public { + loadConfigAndForks("./test/fixtures/config.toml", false); + + // Deploy to mainnet fork + selectFork(1); + address counterMainnet = deployCode(1, "MockCounter.sol", "MockCounter"); + assertTrue(counterMainnet != address(0)); + + // Deploy to optimism fork + selectFork(10); + address counterOptimism = deployCode(10, "MockCounter.sol", "MockCounter"); + assertTrue(counterOptimism != address(0)); + + // Verify deployments are different addresses + assertTrue(counterMainnet != counterOptimism); + } + + function test_deployCodeWithConstructorArgs() public { + loadConfigAndForks("./test/fixtures/config.toml", false); + + // Deploy MockCounter (has no constructor args but tests the interface) + selectFork(1); + bytes memory constructorArgs = ""; + address counter = deployCode(1, "MockCounter.sol", "MockCounter", constructorArgs); + + assertTrue(counter != address(0)); + (bool success, bytes memory data) = counter.call(abi.encodeWithSignature("count()")); + assertTrue(success); + assertEq(abi.decode(data, (uint256)), 0); + } + + function test_deployCodeWithValue() public { + loadConfigAndForks("./test/fixtures/config.toml", false); + + // Deploy with msg.value + selectFork(1); + uint256 value = 1 ether; + vm.deal(address(this), value); + address counter = deployCode(1, "MockCounter.sol", "MockCounter", value); + + assertTrue(counter != address(0)); + assertGe(counter.balance, value); + } + + function test_deployCodeCreate2() public { + loadConfigAndForks("./test/fixtures/config.toml", false); + + // Deploy with CREATE2 using salt + selectFork(1); + bytes32 salt = bytes32(uint256(12345)); + address counter = deployCode(1, "MockCounter.sol", "MockCounter", salt); + + assertTrue(counter != address(0)); + + // Verify same salt produces different addresses due to different bytecode (different EVM versions) + selectFork(10); + address counter2 = deployCode(10, "MockCounter.sol", "MockCounter", salt); + + // Note: Addresses will be different because bytecode is different (shanghai vs cancun) + assertTrue(counter != counter2, "Addresses should differ due to different EVM versions"); + } + + function test_deployCodeCrossChain() public { + loadConfigAndForks("./test/fixtures/config.toml", false); + + // Deploy MockCounter to mainnet (shanghai profile) + selectFork(1); + address mainnetCounter = deployCode(1, "MockCounter.sol", "MockCounter"); + assertTrue(mainnetCounter != address(0)); + + // Verify it's using shanghai artifacts + string memory actualMainnetPath = _getArtifactPath(1, "MockCounter.sol", "MockCounter"); + assertTrue(_endsWith(actualMainnetPath, "out-shanghai/MockCounter.sol/MockCounter.json")); + + // Deploy MockCounter to optimism (cancun profile) + selectFork(10); + address optimismCounter = deployCode(10, "MockCounter.sol", "MockCounter"); + assertTrue(optimismCounter != address(0)); + + // Verify it's using cancun artifacts + string memory actualOptimismPath = _getArtifactPath(10, "MockCounter.sol", "MockCounter"); + assertTrue(_endsWith(actualOptimismPath, "out-cancun/MockCounter.sol/MockCounter.json")); + } + + function test_multiChainDeploymentWorkflow() public { + loadConfigAndForks("./test/fixtures/config.toml", false); + + selectFork(1); + assertEq(vm.getChainId(), 1); + + address mainnetCounter = deployCode(1, "MockCounter.sol", "MockCounter"); + assertTrue(mainnetCounter != address(0)); + + (bool success1, bytes memory data1) = mainnetCounter.call(abi.encodeWithSignature("count()")); + assertTrue(success1); + assertEq(abi.decode(data1, (uint256)), 0); + + (bool success1b,) = mainnetCounter.call(abi.encodeWithSignature("increment()")); + assertTrue(success1b); + + selectFork(10); + assertEq(vm.getChainId(), 10); + + address optimismCounter = deployCode(10, "MockCounter.sol", "MockCounter"); + assertTrue(optimismCounter != address(0)); + + (bool success2, bytes memory data2) = optimismCounter.call(abi.encodeWithSignature("count()")); + assertTrue(success2); + assertEq(abi.decode(data2, (uint256)), 0); + + assertTrue(mainnetCounter != optimismCounter); + + // Switch back to mainnet and verify state is preserved + selectFork(1); + (bool success3, bytes memory data3) = mainnetCounter.call(abi.encodeWithSignature("count()")); + assertTrue(success3); + assertEq(abi.decode(data3, (uint256)), 1); + } } diff --git a/test/StdCheats.t.sol b/test/StdCheats.t.sol index 57dbcc29..ca0bb2df 100644 --- a/test/StdCheats.t.sol +++ b/test/StdCheats.t.sol @@ -151,7 +151,7 @@ contract StdCheatsTest is Test { assertEq(barToken.balanceOf(bar), 1); } - function test_DeployCode() public { + function test_deployCode() public { address deployed = deployCode("StdCheats.t.sol:Bar", bytes("")); assertEq(string(getCode(deployed)), string(getCode(address(test)))); } @@ -183,18 +183,18 @@ contract StdCheatsTest is Test { assertEq(bar.balance, 0); } - function test_DeployCodeNoArgs() public { + function test_deployCodeNoArgs() public { address deployed = deployCode("StdCheats.t.sol:Bar"); assertEq(string(getCode(deployed)), string(getCode(address(test)))); } - function test_DeployCodeVal() public { + function test_deployCodeVal() public { address deployed = deployCode("StdCheats.t.sol:Bar", bytes(""), 1 ether); assertEq(string(getCode(deployed)), string(getCode(address(test)))); assertEq(deployed.balance, 1 ether); } - function test_DeployCodeValNoArgs() public { + function test_deployCodeValNoArgs() public { address deployed = deployCode("StdCheats.t.sol:Bar", 1 ether); assertEq(string(getCode(deployed)), string(getCode(address(test)))); assertEq(deployed.balance, 1 ether); @@ -205,7 +205,7 @@ contract StdCheatsTest is Test { deployCode(what); } - function test_RevertIf_DeployCodeFail() public { + function test_RevertIf_deployCodeFails() public { vm.expectRevert(bytes("StdCheats deployCode(string): Deployment failed.")); this.deployCodeHelper("StdCheats.t.sol:RevertingContract"); } @@ -424,7 +424,7 @@ contract StdCheatsTest is Test { deployCodeTo("StdCheats.t.sol:RevertingContract", address(0)); } - function test_DeployCodeTo() external { + function test_deployCodeTo() external { address arbitraryAddress = makeAddr("arbitraryAddress"); deployCodeTo( diff --git a/test/fixtures/config.toml b/test/fixtures/config.toml index e6dcccca..17127ef9 100644 --- a/test/fixtures/config.toml +++ b/test/fixtures/config.toml @@ -6,6 +6,7 @@ [mainnet] endpoint_url = "${MAINNET_RPC}" +profile = "shanghai" [mainnet.bool] is_live = true @@ -45,6 +46,7 @@ str_array = ["bar", "baz"] [optimism] endpoint_url = "${OPTIMISM_RPC}" +profile = "cancun" [optimism.bool] is_live = false diff --git a/test/fixtures/config_uncompiled.toml b/test/fixtures/config_uncompiled.toml new file mode 100644 index 00000000..72fadf75 --- /dev/null +++ b/test/fixtures/config_uncompiled.toml @@ -0,0 +1,6 @@ +[mainnet] +endpoint_url = "https://eth.llamarpc.com" +profile = "uncompiled" + +[mainnet.uint] +number = 123 diff --git a/test/fixtures/config_write_forbidden.toml b/test/fixtures/config_write_forbidden.toml new file mode 100644 index 00000000..82673d00 --- /dev/null +++ b/test/fixtures/config_write_forbidden.toml @@ -0,0 +1,6 @@ +[mainnet] +endpoint_url = "${MAINNET_RPC}" +profile = "shanghai" + +[mainnet.bool] +is_live = true diff --git a/test/mocks/MockCounter.sol b/test/mocks/MockCounter.sol new file mode 100644 index 00000000..a74acf48 --- /dev/null +++ b/test/mocks/MockCounter.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +/// @notice Simple counter contract for testing multi-EVM deployments. +contract MockCounter { + uint256 public count; + + constructor() payable { + count = 0; + } + + function increment() public { + count++; + } + + function reset() public { + count = 0; + } +} From 875b0a30bf73e78ccb6a489f1121174eb7288c2f Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Mon, 27 Oct 2025 09:11:33 +0100 Subject: [PATCH 2/7] fix: mock counter version --- test/mocks/MockCounter.sol | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/mocks/MockCounter.sol b/test/mocks/MockCounter.sol index a74acf48..71510978 100644 --- a/test/mocks/MockCounter.sol +++ b/test/mocks/MockCounter.sol @@ -1,14 +1,10 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.13; +pragma solidity >=0.6.0 <0.9.0; /// @notice Simple counter contract for testing multi-EVM deployments. contract MockCounter { uint256 public count; - constructor() payable { - count = 0; - } - function increment() public { count++; } From adefbf86015f680e25f67edb1f6c08947a3f9b2f Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Mon, 27 Oct 2025 09:19:37 +0100 Subject: [PATCH 3/7] style: fmt --- src/Vm.sol | 4 ++-- test/Config.t.sol | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Vm.sol b/src/Vm.sol index ddfb752b..0e5f16e1 100644 --- a/src/Vm.sol +++ b/src/Vm.sol @@ -343,9 +343,9 @@ interface VmSafe { /// Represents metadata derived from `foundry.toml` for a given profile. struct ProfileMetadata { - /// The output path of the generated artifacts. + // The output path of the generated artifacts. string artifacts; - /// The EVM version. + // The EVM version. string evm; } diff --git a/test/Config.t.sol b/test/Config.t.sol index cf1705e8..3dc49781 100644 --- a/test/Config.t.sol +++ b/test/Config.t.sol @@ -7,6 +7,7 @@ import {Config} from "../src/Config.sol"; import {StdConfig} from "../src/StdConfig.sol"; import {ConfigView, LibConfigView} from "../src/LibConfigView.sol"; +// forgefmt: disable-start contract ConfigTest is Test, Config { using LibConfigView for ConfigView; function setUp() public { From c8c10205adbe97fd75f89627afb7369c5071ecbe Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Mon, 27 Oct 2025 09:26:40 +0100 Subject: [PATCH 4/7] style: fmt --- src/Config.sol | 66 ++++++++++++-------------------- src/StdAssertions.sol | 36 +++++++++++++----- src/StdConfig.sol | 25 +++++------- src/StdJson.sol | 10 +---- src/StdToml.sol | 10 +---- src/Vm.sol | 69 ++++++++++++---------------------- src/interfaces/IERC7540.sol | 10 +---- src/interfaces/IMulticall3.sol | 5 +-- test/StdStorage.t.sol | 44 +++++++++++----------- 9 files changed, 112 insertions(+), 163 deletions(-) diff --git a/src/Config.sol b/src/Config.sol index b6886edd..60ffdf59 100644 --- a/src/Config.sol +++ b/src/Config.sol @@ -104,13 +104,11 @@ abstract contract Config is CommonBase { // Deploy StdConfig from the profile's artifact directory if not already deployed if (address(std_configOf[evmVersion]) == address(0)) { - string memory artifact = string.concat( - profile[chainId].artifacts, - "/StdConfig.sol/StdConfig.json" - ); + string memory artifact = string.concat(profile[chainId].artifacts, "/StdConfig.sol/StdConfig.json"); // Validate artifact exists - try vm.readFile(artifact) {} catch { + try vm.readFile(artifact) {} + catch { revert ProfileArtifactsNotFound(profileName, artifact); } @@ -178,13 +176,11 @@ abstract contract Config is CommonBase { // Deploy or reuse StdConfig based on EVM version if (address(std_configOf[evmVersion]) == address(0)) { // First chain with this EVM version - deploy new StdConfig - string memory artifact = string.concat( - profile[chainId].artifacts, - "/StdConfig.sol/StdConfig.json" - ); + string memory artifact = string.concat(profile[chainId].artifacts, "/StdConfig.sol/StdConfig.json"); // Validate artifact exists - try vm.readFile(artifact) {} catch { + try vm.readFile(artifact) {} + catch { revert ProfileArtifactsNotFound(profileName, artifact); } @@ -218,7 +214,6 @@ abstract contract Config is CommonBase { vm.setEvmVersion(profile[chainId].evm); } - // -- DEPLOYMENT HELPERS --------------------------------------------------- /// @notice Deploys a contract from an artifact file of the configured profile for the input chain. @@ -229,11 +224,10 @@ abstract contract Config is CommonBase { /// @param chainId: the chain ID. /// @param contractFile: the file that contains the contract's source code (i.e. "Counter.sol"). /// @param contractName: the name of the contract (i.e. "Counter"). - function deployCode( - uint256 chainId, - string memory contractFile, - string memory contractName - ) internal returns (address) { + function deployCode(uint256 chainId, string memory contractFile, string memory contractName) + internal + returns (address) + { string memory artifactPath = _getArtifactPath(chainId, contractFile, contractName); return vm.deployCode(artifactPath); } @@ -266,12 +260,10 @@ abstract contract Config is CommonBase { /// @param contractFile: the file that contains the contract's source code (i.e. "Counter.sol"). /// @param contractName: the name of the contract (i.e. "Counter"). /// @param value: `msg.value` - function deployCode( - uint256 chainId, - string memory contractFile, - string memory contractName, - uint256 value - ) internal returns (address) { + function deployCode(uint256 chainId, string memory contractFile, string memory contractName, uint256 value) + internal + returns (address) + { string memory artifactPath = _getArtifactPath(chainId, contractFile, contractName); return vm.deployCode(artifactPath, value); } @@ -306,12 +298,10 @@ abstract contract Config is CommonBase { /// @param contractFile: the file that contains the contract's source code (i.e. "Counter.sol"). /// @param contractName: the name of the contract (i.e. "Counter"). /// @param salt: the salt used in CREATE2. - function deployCode( - uint256 chainId, - string memory contractFile, - string memory contractName, - bytes32 salt - ) internal returns (address) { + function deployCode(uint256 chainId, string memory contractFile, string memory contractName, bytes32 salt) + internal + returns (address) + { string memory artifactPath = _getArtifactPath(chainId, contractFile, contractName); return vm.deployCode(artifactPath, salt); } @@ -410,19 +400,13 @@ abstract contract Config is CommonBase { /// @param contractFile: the file that contains the contract's source code (i.e. "Counter.sol"). /// @param contractName: the contract name (i.e. "Counter"). /// @return The full path to the artifact JSON file. - function _getArtifactPath( - uint256 chainId, - string memory contractFile, - string memory contractName - ) internal view isActive(chainId) returns (string memory) { - return string.concat( - profile[chainId].artifacts, - "/", - contractFile, - "/", - contractName, - ".json" - ); + function _getArtifactPath(uint256 chainId, string memory contractFile, string memory contractName) + internal + view + isActive(chainId) + returns (string memory) + { + return string.concat(profile[chainId].artifacts, "/", contractFile, "/", contractName, ".json"); } function _assertCached(uint256 chainId) internal view { diff --git a/src/StdAssertions.sol b/src/StdAssertions.sol index 4248170d..45418f2c 100644 --- a/src/StdAssertions.sol +++ b/src/StdAssertions.sol @@ -572,11 +572,7 @@ abstract contract StdAssertions { vm.assertApproxEqAbs(left, right, maxDelta); } - function assertApproxEqAbs(uint256 left, uint256 right, uint256 maxDelta, string memory err) - internal - pure - virtual - { + function assertApproxEqAbs(uint256 left, uint256 right, uint256 maxDelta, string memory err) internal pure virtual { vm.assertApproxEqAbs(left, right, maxDelta, err); } @@ -626,7 +622,11 @@ abstract contract StdAssertions { uint256 left, uint256 right, uint256 maxPercentDelta // An 18 decimal fixed point number, where 1e18 == 100% - ) internal pure virtual { + ) + internal + pure + virtual + { vm.assertApproxEqRel(left, right, maxPercentDelta); } @@ -635,7 +635,11 @@ abstract contract StdAssertions { uint256 right, uint256 maxPercentDelta, // An 18 decimal fixed point number, where 1e18 == 100% string memory err - ) internal pure virtual { + ) + internal + pure + virtual + { vm.assertApproxEqRel(left, right, maxPercentDelta, err); } @@ -644,7 +648,11 @@ abstract contract StdAssertions { uint256 right, uint256 maxPercentDelta, // An 18 decimal fixed point number, where 1e18 == 100% uint256 decimals - ) internal pure virtual { + ) + internal + pure + virtual + { vm.assertApproxEqRelDecimal(left, right, maxPercentDelta, decimals); } @@ -667,7 +675,11 @@ abstract contract StdAssertions { int256 right, uint256 maxPercentDelta, // An 18 decimal fixed point number, where 1e18 == 100% string memory err - ) internal pure virtual { + ) + internal + pure + virtual + { vm.assertApproxEqRel(left, right, maxPercentDelta, err); } @@ -676,7 +688,11 @@ abstract contract StdAssertions { int256 right, uint256 maxPercentDelta, // An 18 decimal fixed point number, where 1e18 == 100% uint256 decimals - ) internal pure virtual { + ) + internal + pure + virtual + { vm.assertApproxEqRelDecimal(left, right, maxPercentDelta, decimals); } diff --git a/src/StdConfig.sol b/src/StdConfig.sol index 3c7f5949..61472f73 100644 --- a/src/StdConfig.sol +++ b/src/StdConfig.sol @@ -105,18 +105,17 @@ contract StdConfig { // Cache the entire configuration to storage for (uint256 i = 0; i < chain_keys.length; i++) { - string memory chain_key = chain_keys[i]; + string memory key = chain_keys[i]; // Ignore top-level keys that are not tables - if (vm.parseTomlKeys(content, string.concat("$.", chain_key)).length == 0) { + if (vm.parseTomlKeys(content, string.concat("$.", key)).length == 0) { continue; } - uint256 chainId = resolveChainId(chain_key); + uint256 chainId = resolveChainId(key); // Cache the configured profile metadata for that chain. // Falls back to the currently active profile. Panics if the profile name doesn't exist. VmSafe.ProfileMetadata memory chainProfile; - try vm.parseTomlString(content, string.concat("$.", chain_key, ".profile")) returns (string memory profile) - { + try vm.parseTomlString(content, string.concat("$.", key, ".profile")) returns (string memory profile) { chainProfile = vm.getProfile(profile); } catch { chainProfile = vm.getProfile(); @@ -130,21 +129,20 @@ contract StdConfig { // This chain matches our EVM version, load it _chainIds.push(chainId); _profileOf[chainId] = chainProfile; - _keyOf[chainId] = chain_key; + _keyOf[chainId] = key; // Cache the configured rpc endpoint for that chain. // Falls back to `[rpc_endpoints]`. Panics if no rpc endpoint is configured. - try vm.parseTomlString(content, string.concat("$.", chain_key, ".endpoint_url")) returns (string memory url) - { + try vm.parseTomlString(content, string.concat("$.", key, ".endpoint_url")) returns (string memory url) { _rpcOf[chainId] = vm.resolveEnv(url); } catch { - _rpcOf[chainId] = vm.resolveEnv(vm.rpcUrl(chain_key)); + _rpcOf[chainId] = vm.resolveEnv(vm.rpcUrl(key)); } // Iterate through all the available `TypeKind`s (except `None`) to create the sub-section paths for (uint8 t = 1; t <= NUM_TYPES; t++) { TypeKind ty = TypeKind(t); - string memory typePath = string.concat("$.", chain_key, ".", ty.toTomlKey()); + string memory typePath = string.concat("$.", key, ".", ty.toTomlKey()); try vm.parseTomlKeys(content, typePath) returns (string[] memory keys) { for (uint256 j = 0; j < keys.length; j++) { @@ -343,12 +341,7 @@ contract StdConfig { /// @param chain_id The chain ID to read from. /// @param key The key of the variable to retrieve. /// @return `Variable` struct containing the type and the ABI-encoded value. - function get(uint256 chain_id, string memory key) - public - view - isCached(chain_id) - returns (Variable memory) - { + function get(uint256 chain_id, string memory key) public view isCached(chain_id) returns (Variable memory) { return Variable(_typeOf[chain_id][key], _dataOf[chain_id][key]); } diff --git a/src/StdJson.sol b/src/StdJson.sol index 2a033c03..193f89a6 100644 --- a/src/StdJson.sol +++ b/src/StdJson.sol @@ -197,10 +197,7 @@ library stdJson { return vm.serializeBool(jsonKey, key, value); } - function serialize(string memory jsonKey, string memory key, bool[] memory value) - internal - returns (string memory) - { + function serialize(string memory jsonKey, string memory key, bool[] memory value) internal returns (string memory) { return vm.serializeBool(jsonKey, key, value); } @@ -259,10 +256,7 @@ library stdJson { return vm.serializeBytes(jsonKey, key, value); } - function serialize(string memory jsonKey, string memory key, string memory value) - internal - returns (string memory) - { + function serialize(string memory jsonKey, string memory key, string memory value) internal returns (string memory) { return vm.serializeString(jsonKey, key, value); } diff --git a/src/StdToml.sol b/src/StdToml.sol index 7ad3be2f..7ea92e26 100644 --- a/src/StdToml.sol +++ b/src/StdToml.sol @@ -197,10 +197,7 @@ library stdToml { return vm.serializeBool(jsonKey, key, value); } - function serialize(string memory jsonKey, string memory key, bool[] memory value) - internal - returns (string memory) - { + function serialize(string memory jsonKey, string memory key, bool[] memory value) internal returns (string memory) { return vm.serializeBool(jsonKey, key, value); } @@ -259,10 +256,7 @@ library stdToml { return vm.serializeBytes(jsonKey, key, value); } - function serialize(string memory jsonKey, string memory key, string memory value) - internal - returns (string memory) - { + function serialize(string memory jsonKey, string memory key, string memory value) internal returns (string memory) { return vm.serializeString(jsonKey, key, value); } diff --git a/src/Vm.sol b/src/Vm.sol index 0e5f16e1..3ee950e8 100644 --- a/src/Vm.sol +++ b/src/Vm.sol @@ -380,10 +380,12 @@ interface VmSafe { /// Derive a private key from a provided mnemonic string (or mnemonic file path) in the specified language /// at `{derivationPath}{index}`. - function deriveKey(string calldata mnemonic, string calldata derivationPath, uint32 index, string calldata language) - external - pure - returns (uint256 privateKey); + function deriveKey( + string calldata mnemonic, + string calldata derivationPath, + uint32 index, + string calldata language + ) external pure returns (uint256 privateKey); /// Derives secp256r1 public key from the provided `privateKey`. function publicKeyP256(uint256 privateKey) external pure returns (uint256 publicKeyX, uint256 publicKeyY); @@ -879,10 +881,7 @@ interface VmSafe { function getDeployment(string calldata contractName) external view returns (address deployedAddress); /// Returns the most recent deployment for the given contract on `chainId` - function getDeployment(string calldata contractName, uint64 chainId) - external - view - returns (address deployedAddress); + function getDeployment(string calldata contractName, uint64 chainId) external view returns (address deployedAddress); /// Returns all deployments for the given contract on `chainId` /// Sorted in descending order of deployment time i.e descending order of BroadcastTxSummary.blockNumber. @@ -989,10 +988,7 @@ interface VmSafe { function parseJsonAddress(string calldata json, string calldata key) external pure returns (address); /// Parses a string of JSON data at `key` and coerces it to `address[]`. - function parseJsonAddressArray(string calldata json, string calldata key) - external - pure - returns (address[] memory); + function parseJsonAddressArray(string calldata json, string calldata key) external pure returns (address[] memory); /// Parses a string of JSON data at `key` and coerces it to `bool`. function parseJsonBool(string calldata json, string calldata key) external pure returns (bool); @@ -1007,10 +1003,7 @@ interface VmSafe { function parseJsonBytes32(string calldata json, string calldata key) external pure returns (bytes32); /// Parses a string of JSON data at `key` and coerces it to `bytes32[]`. - function parseJsonBytes32Array(string calldata json, string calldata key) - external - pure - returns (bytes32[] memory); + function parseJsonBytes32Array(string calldata json, string calldata key) external pure returns (bytes32[] memory); /// Parses a string of JSON data at `key` and coerces it to `bytes[]`. function parseJsonBytesArray(string calldata json, string calldata key) external pure returns (bytes[] memory); @@ -1037,10 +1030,7 @@ interface VmSafe { returns (bytes memory); /// Parses a string of JSON data and coerces it to type corresponding to `typeDescription`. - function parseJsonType(string calldata json, string calldata typeDescription) - external - pure - returns (bytes memory); + function parseJsonType(string calldata json, string calldata typeDescription) external pure returns (bytes memory); /// Parses a string of JSON data at `key` and coerces it to type corresponding to `typeDescription`. function parseJsonType(string calldata json, string calldata key, string calldata typeDescription) @@ -1407,9 +1397,7 @@ interface VmSafe { /// Compares two `int256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`. /// `maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100% /// Includes error message into revert string on failure. - function assertApproxEqRel(int256 left, int256 right, uint256 maxPercentDelta, string calldata error) - external - pure; + function assertApproxEqRel(int256 left, int256 right, uint256 maxPercentDelta, string calldata error) external pure; /// Asserts that two `uint256` values are equal, formatting them with decimals in failure message. function assertEqDecimal(uint256 left, uint256 right, uint256 decimals) external pure; @@ -1808,10 +1796,7 @@ interface VmSafe { function parseTomlAddress(string calldata toml, string calldata key) external pure returns (address); /// Parses a string of TOML data at `key` and coerces it to `address[]`. - function parseTomlAddressArray(string calldata toml, string calldata key) - external - pure - returns (address[] memory); + function parseTomlAddressArray(string calldata toml, string calldata key) external pure returns (address[] memory); /// Parses a string of TOML data at `key` and coerces it to `bool`. function parseTomlBool(string calldata toml, string calldata key) external pure returns (bool); @@ -1826,10 +1811,7 @@ interface VmSafe { function parseTomlBytes32(string calldata toml, string calldata key) external pure returns (bytes32); /// Parses a string of TOML data at `key` and coerces it to `bytes32[]`. - function parseTomlBytes32Array(string calldata toml, string calldata key) - external - pure - returns (bytes32[] memory); + function parseTomlBytes32Array(string calldata toml, string calldata key) external pure returns (bytes32[] memory); /// Parses a string of TOML data at `key` and coerces it to `bytes[]`. function parseTomlBytesArray(string calldata toml, string calldata key) external pure returns (bytes[] memory); @@ -1856,10 +1838,7 @@ interface VmSafe { returns (bytes memory); /// Parses a string of TOML data and coerces it to type corresponding to `typeDescription`. - function parseTomlType(string calldata toml, string calldata typeDescription) - external - pure - returns (bytes memory); + function parseTomlType(string calldata toml, string calldata typeDescription) external pure returns (bytes memory); /// Parses a string of TOML data at `key` and coerces it to type corresponding to `typeDescription`. function parseTomlType(string calldata toml, string calldata key, string calldata typeDescription) @@ -1896,10 +1875,7 @@ interface VmSafe { function bound(int256 current, int256 min, int256 max) external view returns (int256); /// Compute the address of a contract created with CREATE2 using the given CREATE2 deployer. - function computeCreate2Address(bytes32 salt, bytes32 initCodeHash, address deployer) - external - pure - returns (address); + function computeCreate2Address(bytes32 salt, bytes32 initCodeHash, address deployer) external pure returns (address); /// Compute the address of a contract created with CREATE2 using the default CREATE2 deployer. function computeCreate2Address(bytes32 salt, bytes32 initCodeHash) external pure returns (address); @@ -2149,8 +2125,7 @@ interface Vm is VmSafe { function mockCallRevert(address callee, bytes calldata data, bytes calldata revertData) external; /// Reverts a call to an address with a specific `msg.value`, with specified revert data. - function mockCallRevert(address callee, uint256 msgValue, bytes calldata data, bytes calldata revertData) - external; + function mockCallRevert(address callee, uint256 msgValue, bytes calldata data, bytes calldata revertData) external; /// Reverts a call to an address with specified revert data. /// Overload to pass the function selector directly `token.approve.selector` instead of `abi.encodeWithSelector(token.approve.selector)`. @@ -2396,8 +2371,13 @@ interface Vm is VmSafe { /// Prepare an expected anonymous log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData.). /// Call this function, then emit an anonymous event, then call a function. Internally after the call, we check if /// logs were emitted in the expected order with the expected topics and data (as specified by the booleans). - function expectEmitAnonymous(bool checkTopic0, bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData) - external; + function expectEmitAnonymous( + bool checkTopic0, + bool checkTopic1, + bool checkTopic2, + bool checkTopic3, + bool checkData + ) external; /// Same as the previous method, but also checks supplied address against emitting contract. function expectEmitAnonymous( @@ -2423,8 +2403,7 @@ interface Vm is VmSafe { function expectEmit(bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData) external; /// Same as the previous method, but also checks supplied address against emitting contract. - function expectEmit(bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData, address emitter) - external; + function expectEmit(bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData, address emitter) external; /// Prepare an expected log with all topic and data checks enabled. /// Call this function, then emit an event, then call a function. Internally after the call, we check if diff --git a/src/interfaces/IERC7540.sol b/src/interfaces/IERC7540.sol index 91a38ca3..9b3ca08b 100644 --- a/src/interfaces/IERC7540.sol +++ b/src/interfaces/IERC7540.sol @@ -64,10 +64,7 @@ interface IERC7540Deposit is IERC7540Operator { * - MUST NOT show any variations depending on the caller. * - MUST NOT revert unless due to integer overflow caused by an unreasonably large input. */ - function pendingDepositRequest(uint256 requestId, address controller) - external - view - returns (uint256 pendingAssets); + function pendingDepositRequest(uint256 requestId, address controller) external view returns (uint256 pendingAssets); /** * @dev Returns the amount of requested assets in Claimable state for the controller to deposit or mint. @@ -127,10 +124,7 @@ interface IERC7540Redeem is IERC7540Operator { * - MUST NOT show any variations depending on the caller. * - MUST NOT revert unless due to integer overflow caused by an unreasonably large input. */ - function pendingRedeemRequest(uint256 requestId, address controller) - external - view - returns (uint256 pendingShares); + function pendingRedeemRequest(uint256 requestId, address controller) external view returns (uint256 pendingShares); /** * @dev Returns the amount of requested shares in Claimable state for the controller to redeem or withdraw. diff --git a/src/interfaces/IMulticall3.sol b/src/interfaces/IMulticall3.sol index 0d031b71..3b79851b 100644 --- a/src/interfaces/IMulticall3.sol +++ b/src/interfaces/IMulticall3.sol @@ -27,10 +27,7 @@ interface IMulticall3 { bytes returnData; } - function aggregate(Call[] calldata calls) - external - payable - returns (uint256 blockNumber, bytes[] memory returnData); + function aggregate(Call[] calldata calls) external payable returns (uint256 blockNumber, bytes[] memory returnData); function aggregate3(Call3[] calldata calls) external payable returns (Result[] memory returnData); diff --git a/test/StdStorage.t.sol b/test/StdStorage.t.sol index 46604f86..d59465b5 100644 --- a/test/StdStorage.t.sol +++ b/test/StdStorage.t.sol @@ -58,9 +58,8 @@ contract StdStorageTest is Test { } function test_StorageDeepMap() public { - uint256 slot = stdstore.target(address(test)).sig(test.deep_map.selector).with_key(address(this)).with_key( - address(this) - ).find(); + uint256 slot = stdstore.target(address(test)).sig(test.deep_map.selector).with_key(address(this)) + .with_key(address(this)).find(); assertEq(uint256(keccak256(abi.encode(address(this), keccak256(abi.encode(address(this), uint256(5)))))), slot); } @@ -74,7 +73,9 @@ contract StdStorageTest is Test { uint256 slot = stdstore.target(address(test)).sig(test.deep_map_struct.selector).with_key(address(this)) .with_key(address(this)).depth(0).find(); assertEq( - bytes32(uint256(keccak256(abi.encode(address(this), keccak256(abi.encode(address(this), uint256(6)))))) + 0), + bytes32( + uint256(keccak256(abi.encode(address(this), keccak256(abi.encode(address(this), uint256(6)))))) + 0 + ), bytes32(slot) ); } @@ -83,24 +84,24 @@ contract StdStorageTest is Test { uint256 slot = stdstore.target(address(test)).sig(test.deep_map_struct.selector).with_key(address(this)) .with_key(address(this)).depth(1).find(); assertEq( - bytes32(uint256(keccak256(abi.encode(address(this), keccak256(abi.encode(address(this), uint256(6)))))) + 1), + bytes32( + uint256(keccak256(abi.encode(address(this), keccak256(abi.encode(address(this), uint256(6)))))) + 1 + ), bytes32(slot) ); } function test_StorageCheckedWriteDeepMapStructA() public { - stdstore.target(address(test)).sig(test.deep_map_struct.selector).with_key(address(this)).with_key( - address(this) - ).depth(0).checked_write(100); + stdstore.target(address(test)).sig(test.deep_map_struct.selector).with_key(address(this)) + .with_key(address(this)).depth(0).checked_write(100); (uint256 a, uint256 b) = test.deep_map_struct(address(this), address(this)); assertEq(100, a); assertEq(0, b); } function test_StorageCheckedWriteDeepMapStructB() public { - stdstore.target(address(test)).sig(test.deep_map_struct.selector).with_key(address(this)).with_key( - address(this) - ).depth(1).checked_write(100); + stdstore.target(address(test)).sig(test.deep_map_struct.selector).with_key(address(this)) + .with_key(address(this)).depth(1).checked_write(100); (uint256 a, uint256 b) = test.deep_map_struct(address(this), address(this)); assertEq(0, a); assertEq(100, b); @@ -192,9 +193,8 @@ contract StdStorageTest is Test { uint256 full = test.map_packed(address(1337)); // keep upper 128, set lower 128 to 1337 full = (full & (uint256((1 << 128) - 1) << 128)) | 1337; - stdstore.target(address(test)).sig(test.map_packed.selector).with_key(address(uint160(1337))).checked_write( - full - ); + stdstore.target(address(test)).sig(test.map_packed.selector).with_key(address(uint160(1337))) + .checked_write(full); assertEq(1337, test.read_struct_lower(address(1337))); } @@ -290,9 +290,9 @@ contract StdStorageTest is Test { // clear left bits, then clear right bits and realign uint256 expectedValToRead = (val << leftBits) >> (leftBits + rightBits); - uint256 readVal = stdstore.target(address(test)).enable_packed_slots().sig( - "getRandomPacked(uint8,uint8[],uint8)" - ).with_calldata(abi.encode(shifts, shiftSizes, elemToGet)).read_uint(); + uint256 readVal = stdstore.target(address(test)).enable_packed_slots() + .sig("getRandomPacked(uint8,uint8[],uint8)").with_calldata(abi.encode(shifts, shiftSizes, elemToGet)) + .read_uint(); assertEq(readVal, expectedValToRead); } @@ -330,16 +330,14 @@ contract StdStorageTest is Test { // Pack all values into the slot. for (uint256 i = 0; i < nvars; i++) { - stdstore.enable_packed_slots().target(address(test)).sig("getRandomPacked(uint256,uint256)").with_key( - sizes[i] - ).with_key(offsets[i]).checked_write(vals[i]); + stdstore.enable_packed_slots().target(address(test)).sig("getRandomPacked(uint256,uint256)") + .with_key(sizes[i]).with_key(offsets[i]).checked_write(vals[i]); } // Verify the read data matches. for (uint256 i = 0; i < nvars; i++) { - uint256 readVal = stdstore.enable_packed_slots().target(address(test)).sig( - "getRandomPacked(uint256,uint256)" - ).with_key(sizes[i]).with_key(offsets[i]).read_uint(); + uint256 readVal = stdstore.enable_packed_slots().target(address(test)) + .sig("getRandomPacked(uint256,uint256)").with_key(sizes[i]).with_key(offsets[i]).read_uint(); uint256 retVal = test.getRandomPacked(sizes[i], offsets[i]); From d943b898db3749667a1af8b7778a91f9da4c6c3c Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Mon, 27 Oct 2025 09:33:28 +0100 Subject: [PATCH 5/7] fix: typo --- src/StdConfig.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/StdConfig.sol b/src/StdConfig.sol index 13df4710..79cb5dde 100644 --- a/src/StdConfig.sol +++ b/src/StdConfig.sol @@ -146,11 +146,11 @@ contract StdConfig { try vm.parseTomlKeys(content, typePath) returns (string[] memory keys) { for (uint256 j = 0; j < keys.length; j++) { - string memory key = keys[j]; - if (_typeOf[chainId][key].kind == TypeKind.None) { - _loadAndCacheValue(content, string.concat(typePath, ".", key), chainId, key, ty); + string memory k = keys[j]; + if (_typeOf[chainId][k].kind == TypeKind.None) { + _loadAndCacheValue(content, string.concat(typePath, ".", k), chainId, k, ty); } else { - revert AlreadyInitialized(key); + revert AlreadyInitialized(k); } } } catch {} From 92d1ec65e578b7660079c2cb8b25eaecc03e967f Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Mon, 27 Oct 2025 12:22:55 +0100 Subject: [PATCH 6/7] chore: refactor to reduce contract size --- src/LibConfigView.sol | 30 +-- src/LibVariable.sol | 74 +++++++ src/StdConfig.sol | 444 +++++++++++++----------------------------- test/Config.t.sol | 25 +-- 4 files changed, 239 insertions(+), 334 deletions(-) diff --git a/src/LibConfigView.sol b/src/LibConfigView.sol index 5a7e2fbc..00fb5a94 100644 --- a/src/LibConfigView.sol +++ b/src/LibConfigView.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.13; import {StdConfig} from "./StdConfig.sol"; -import {Variable} from "./LibVariable.sol"; +import {Variable, LibVariable} from "./LibVariable.sol"; /// @notice A view into a StdConfig instance bound to a specific chain ID. /// Provides ergonomic access to configuration variables without repeating the chain ID. @@ -28,73 +28,73 @@ library LibConfigView { /// @notice Sets a boolean configuration variable. function set(ConfigView memory self, string memory key, bool value) internal { - self.stdConfig.set(self.chainId, key, value); + self.stdConfig.set(self.chainId, key, LibVariable.from(value)); } /// @notice Sets an address configuration variable. function set(ConfigView memory self, string memory key, address value) internal { - self.stdConfig.set(self.chainId, key, value); + self.stdConfig.set(self.chainId, key, LibVariable.from(value)); } /// @notice Sets a bytes32 configuration variable. function set(ConfigView memory self, string memory key, bytes32 value) internal { - self.stdConfig.set(self.chainId, key, value); + self.stdConfig.set(self.chainId, key, LibVariable.from(value)); } /// @notice Sets a uint256 configuration variable. function set(ConfigView memory self, string memory key, uint256 value) internal { - self.stdConfig.set(self.chainId, key, value); + self.stdConfig.set(self.chainId, key, LibVariable.from(value)); } /// @notice Sets an int256 configuration variable. function set(ConfigView memory self, string memory key, int256 value) internal { - self.stdConfig.set(self.chainId, key, value); + self.stdConfig.set(self.chainId, key, LibVariable.from(value)); } /// @notice Sets a string configuration variable. function set(ConfigView memory self, string memory key, string memory value) internal { - self.stdConfig.set(self.chainId, key, value); + self.stdConfig.set(self.chainId, key, LibVariable.from(value)); } /// @notice Sets a bytes configuration variable. function set(ConfigView memory self, string memory key, bytes memory value) internal { - self.stdConfig.set(self.chainId, key, value); + self.stdConfig.set(self.chainId, key, LibVariable.from(value)); } // -- SETTERS (ARRAYS) ----------------------------------------------------- /// @notice Sets a boolean array configuration variable. function set(ConfigView memory self, string memory key, bool[] memory value) internal { - self.stdConfig.set(self.chainId, key, value); + self.stdConfig.set(self.chainId, key, LibVariable.from(value)); } /// @notice Sets an address array configuration variable. function set(ConfigView memory self, string memory key, address[] memory value) internal { - self.stdConfig.set(self.chainId, key, value); + self.stdConfig.set(self.chainId, key, LibVariable.from(value)); } /// @notice Sets a bytes32 array configuration variable. function set(ConfigView memory self, string memory key, bytes32[] memory value) internal { - self.stdConfig.set(self.chainId, key, value); + self.stdConfig.set(self.chainId, key, LibVariable.from(value)); } /// @notice Sets a uint256 array configuration variable. function set(ConfigView memory self, string memory key, uint256[] memory value) internal { - self.stdConfig.set(self.chainId, key, value); + self.stdConfig.set(self.chainId, key, LibVariable.from(value)); } /// @notice Sets an int256 array configuration variable. function set(ConfigView memory self, string memory key, int256[] memory value) internal { - self.stdConfig.set(self.chainId, key, value); + self.stdConfig.set(self.chainId, key, LibVariable.from(value)); } /// @notice Sets a string array configuration variable. function set(ConfigView memory self, string memory key, string[] memory value) internal { - self.stdConfig.set(self.chainId, key, value); + self.stdConfig.set(self.chainId, key, LibVariable.from(value)); } /// @notice Sets a bytes array configuration variable. function set(ConfigView memory self, string memory key, bytes[] memory value) internal { - self.stdConfig.set(self.chainId, key, value); + self.stdConfig.set(self.chainId, key, LibVariable.from(value)); } } diff --git a/src/LibVariable.sol b/src/LibVariable.sol index c46b1532..638a39bc 100644 --- a/src/LibVariable.sol +++ b/src/LibVariable.sol @@ -474,4 +474,78 @@ library LibVariable { { return abi.decode(self.data, (bytes[])); } + + // -- FACTORY FUNCTIONS (SINGLE VALUES) ------------------------------------ + + /// @notice Creates a `Variable` from a `bool` value. + function from(bool value) internal pure returns (Variable memory) { + return Variable(Type(TypeKind.Bool, false), abi.encode(value)); + } + + /// @notice Creates a `Variable` from an `address` value. + function from(address value) internal pure returns (Variable memory) { + return Variable(Type(TypeKind.Address, false), abi.encode(value)); + } + + /// @notice Creates a `Variable` from a `bytes32` value. + function from(bytes32 value) internal pure returns (Variable memory) { + return Variable(Type(TypeKind.Bytes32, false), abi.encode(value)); + } + + /// @notice Creates a `Variable` from a `uint256` value. + function from(uint256 value) internal pure returns (Variable memory) { + return Variable(Type(TypeKind.Uint256, false), abi.encode(value)); + } + + /// @notice Creates a `Variable` from an `int256` value. + function from(int256 value) internal pure returns (Variable memory) { + return Variable(Type(TypeKind.Int256, false), abi.encode(value)); + } + + /// @notice Creates a `Variable` from a `string` value. + function from(string memory value) internal pure returns (Variable memory) { + return Variable(Type(TypeKind.String, false), abi.encode(value)); + } + + /// @notice Creates a `Variable` from a `bytes` value. + function from(bytes memory value) internal pure returns (Variable memory) { + return Variable(Type(TypeKind.Bytes, false), abi.encode(value)); + } + + // -- FACTORY FUNCTIONS (ARRAYS) ------------------------------------------- + + /// @notice Creates a `Variable` from a `bool` array. + function from(bool[] memory value) internal pure returns (Variable memory) { + return Variable(Type(TypeKind.Bool, true), abi.encode(value)); + } + + /// @notice Creates a `Variable` from an `address` array. + function from(address[] memory value) internal pure returns (Variable memory) { + return Variable(Type(TypeKind.Address, true), abi.encode(value)); + } + + /// @notice Creates a `Variable` from a `bytes32` array. + function from(bytes32[] memory value) internal pure returns (Variable memory) { + return Variable(Type(TypeKind.Bytes32, true), abi.encode(value)); + } + + /// @notice Creates a `Variable` from a `uint256` array. + function from(uint256[] memory value) internal pure returns (Variable memory) { + return Variable(Type(TypeKind.Uint256, true), abi.encode(value)); + } + + /// @notice Creates a `Variable` from an `int256` array. + function from(int256[] memory value) internal pure returns (Variable memory) { + return Variable(Type(TypeKind.Int256, true), abi.encode(value)); + } + + /// @notice Creates a `Variable` from a `string` array. + function from(string[] memory value) internal pure returns (Variable memory) { + return Variable(Type(TypeKind.String, true), abi.encode(value)); + } + + /// @notice Creates a `Variable` from a `bytes` array. + function from(bytes[] memory value) internal pure returns (Variable memory) { + return Variable(Type(TypeKind.Bytes, true), abi.encode(value)); + } } diff --git a/src/StdConfig.sol b/src/StdConfig.sol index 79cb5dde..61dc1b35 100644 --- a/src/StdConfig.sol +++ b/src/StdConfig.sol @@ -110,7 +110,7 @@ contract StdConfig { if (vm.parseTomlKeys(content, string.concat("$.", key)).length == 0) { continue; } - uint256 chainId = resolveChainId(key); + uint256 chainId = _resolveChainId(key); // Cache the configured profile metadata for that chain. // Falls back to the currently active profile. Panics if the profile name doesn't exist. @@ -257,81 +257,6 @@ contract StdConfig { } } - // -- HELPER FUNCTIONS ----------------------------------------------------- - - /// @notice Enable or disable automatic writing to the TOML file on `set`. - /// Can only be enabled when scripting. - function writeUpdatesBackToFile(bool enabled) public { - if (enabled && !vm.isContext(VmSafe.ForgeContext.ScriptGroup)) { - revert WriteToFileInForbiddenCtxt(); - } - - _writeToFile = enabled; - } - - /// @notice Resolves a chain alias or a chain id string to its numerical chain id. - /// @param aliasOrId The string representing the chain alias (i.e. "mainnet") or a numerical ID (i.e. "1"). - /// @return The numerical chain ID. - /// @dev It first attempts to parse the input as a number. If that fails, it uses `vm.getChain` to resolve a named alias. - /// Reverts if the alias is not valid or not a number. - function resolveChainId(string memory aliasOrId) public view returns (uint256) { - try vm.parseUint(aliasOrId) returns (uint256 chainId) { - return chainId; - } catch { - try vm.getChain(aliasOrId) returns (VmSafe.Chain memory chainInfo) { - return chainInfo.chainId; - } catch { - revert InvalidChainKey(aliasOrId); - } - } - } - - /// @dev Retrieves the chain key/alias from the configuration based on the chain ID. - function _getChainKeyFromId(uint256 chainId) private view returns (string memory) { - string memory key = _keyOf[chainId]; - if (bytes(key).length == 0) revert ChainNotInitialized(chainId); - return key; - } - - /// @dev Ensures type consistency when setting a value - prevents changing types unless uninitialized. - /// Updates type only when the previous type was `None`. - function _ensureTypeConsistency(uint256 chainId, string memory key, Type memory ty) private { - Type memory current = _typeOf[chainId][key]; - - if (current.kind == TypeKind.None) { - _typeOf[chainId][key] = ty; - } else { - current.assertEq(ty); - } - } - - /// @dev Wraps a string in double quotes for JSON compatibility. - function _quote(string memory s) private pure returns (string memory) { - return string.concat('"', s, '"'); - } - - /// @dev Writes a JSON-formatted value to a specific key in the TOML file. - /// @param chainId The chain id to write under. - /// @param ty The type category ('bool', 'address', 'uint', 'bytes32', 'string', or 'bytes'). - /// @param key The variable key name. - /// @param jsonValue The JSON-formatted value to write. - function _writeToToml(uint256 chainId, string memory ty, string memory key, string memory jsonValue) private { - string memory chainKey = _getChainKeyFromId(chainId); - string memory valueKey = string.concat("$.", chainKey, ".", ty, ".", key); - vm.writeToml(jsonValue, _filePath, valueKey); - } - - /// @dev Validates that a chain has been initialized in this StdConfig instance. - function _isCached(uint256 chainId) private view { - if (bytes(_keyOf[chainId]).length == 0) revert ChainNotInitialized(chainId); - } - - /// @dev Modifier to ensure chain is initialized before accessing its data. - modifier isCached(uint256 chainId) { - _isCached(chainId); - _; - } - // -- GETTER FUNCTIONS ----------------------------------------------------- /// @dev Reads a variable for a given chain id and key, and returns it in a generic container. @@ -380,271 +305,176 @@ contract StdConfig { return _profileOf[vm.getChainId()]; } - // -- SETTER FUNCTIONS (SINGLE VALUES) ------------------------------------- + // -- SETTER FUNCTIONS ----------------------------------------------------- - /// @notice Sets a boolean value for a given key and chain ID. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, bool value) public isCached(chainId) { - Type memory ty = Type(TypeKind.Bool, false); - _ensureTypeConsistency(chainId, key, ty); - _dataOf[chainId][key] = abi.encode(value); - if (_writeToFile) _writeToToml(chainId, ty.kind.toTomlKey(), key, vm.toString(value)); - } + /// @notice Enable or disable automatic writing to the TOML file on `set`. + /// Can only be enabled when scripting. + function writeUpdatesBackToFile(bool enabled) public { + if (enabled && !vm.isContext(VmSafe.ForgeContext.ScriptGroup)) { + revert WriteToFileInForbiddenCtxt(); + } - /// @notice Sets a boolean value for a given key on the current chain. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(string memory key, bool value) public { - set(vm.getChainId(), key, value); + _writeToFile = enabled; } - /// @notice Sets an address value for a given key and chain ID. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, address value) public isCached(chainId) { - Type memory ty = Type(TypeKind.Address, false); - _ensureTypeConsistency(chainId, key, ty); - _dataOf[chainId][key] = abi.encode(value); - if (_writeToFile) _writeToToml(chainId, ty.kind.toTomlKey(), key, _quote(vm.toString(value))); + /// @notice Sets a variable for a given key and chain ID. + /// @dev Sets the cached value in storage and writes the change back to the TOML file if `writeToFile` is enabled. + /// Use `LibVariable.from(...)` to create the Variable from concrete types. + /// @param chainId The chain ID to set the value for. + /// @param key The key of the variable to set. + /// @param value The Variable containing the type and ABI-encoded value. + function set(uint256 chainId, string memory key, Variable memory value) public isCached(chainId) { + _ensureTypeConsistency(chainId, key, value.ty); + _dataOf[chainId][key] = value.data; + if (_writeToFile) { + _writeToToml(chainId, value.ty.kind.toTomlKey(), key, _serializeToJson(value)); + } } - /// @notice Sets an address value for a given key on the current chain. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(string memory key, address value) public { + /// @notice Sets a variable for a given key on the current chain. + /// @dev Sets the cached value in storage and writes the change back to the TOML file if `writeToFile` is enabled. + /// Use `LibVariable.from(...)` to create the Variable from concrete types. + /// @param key The key of the variable to set. + /// @param value The Variable containing the type and ABI-encoded value. + function set(string memory key, Variable memory value) public { set(vm.getChainId(), key, value); } - /// @notice Sets a bytes32 value for a given key and chain ID. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, bytes32 value) public isCached(chainId) { - Type memory ty = Type(TypeKind.Bytes32, false); - _ensureTypeConsistency(chainId, key, ty); - _dataOf[chainId][key] = abi.encode(value); - if (_writeToFile) _writeToToml(chainId, ty.kind.toTomlKey(), key, _quote(vm.toString(value))); - } - - /// @notice Sets a bytes32 value for a given key on the current chain. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(string memory key, bytes32 value) public { - set(vm.getChainId(), key, value); - } + // -- HELPER FUNCTIONS ----------------------------------------------------- - /// @notice Sets a uint256 value for a given key and chain ID. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, uint256 value) public isCached(chainId) { - Type memory ty = Type(TypeKind.Uint256, false); - _ensureTypeConsistency(chainId, key, ty); - _dataOf[chainId][key] = abi.encode(value); - if (_writeToFile) _writeToToml(chainId, ty.kind.toTomlKey(), key, vm.toString(value)); + /// @dev Validates that a chain has been initialized in this StdConfig instance. + function _isCached(uint256 chainId) private view { + if (bytes(_keyOf[chainId]).length == 0) revert ChainNotInitialized(chainId); } - /// @notice Sets a uint256 value for a given key on the current chain. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(string memory key, uint256 value) public { - set(vm.getChainId(), key, value); + /// @dev Modifier to ensure chain is initialized before accessing its data. + modifier isCached(uint256 chainId) { + _isCached(chainId); + _; } - /// @notice Sets an int256 value for a given key and chain ID. - function set(uint256 chainId, string memory key, int256 value) public isCached(chainId) { - Type memory ty = Type(TypeKind.Int256, false); - _ensureTypeConsistency(chainId, key, ty); - _dataOf[chainId][key] = abi.encode(value); - if (_writeToFile) _writeToToml(chainId, ty.kind.toTomlKey(), key, vm.toString(value)); + /// @notice Resolves a chain alias or a chain id string to its numerical chain id. + /// @param aliasOrId The string representing the chain alias (i.e. "mainnet") or a numerical ID (i.e. "1"). + /// @return The numerical chain ID. + /// @dev It first attempts to parse the input as a number. If that fails, it uses `vm.getChain` to resolve a named alias. + /// Reverts if the alias is not valid or not a number. + function _resolveChainId(string memory aliasOrId) public view returns (uint256) { + try vm.parseUint(aliasOrId) returns (uint256 chainId) { + return chainId; + } catch { + try vm.getChain(aliasOrId) returns (VmSafe.Chain memory chainInfo) { + return chainInfo.chainId; + } catch { + revert InvalidChainKey(aliasOrId); + } + } } - /// @notice Sets an int256 value for a given key on the current chain. - function set(string memory key, int256 value) public { - set(vm.getChainId(), key, value); + /// @dev Retrieves the chain key/alias from the configuration based on the chain ID. + function _getChainKeyFromId(uint256 chainId) private view returns (string memory) { + string memory key = _keyOf[chainId]; + if (bytes(key).length == 0) revert ChainNotInitialized(chainId); + return key; } - /// @notice Sets a string value for a given key and chain ID. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, string memory value) public isCached(chainId) { - Type memory ty = Type(TypeKind.String, false); - _ensureTypeConsistency(chainId, key, ty); - _dataOf[chainId][key] = abi.encode(value); - if (_writeToFile) _writeToToml(chainId, ty.kind.toTomlKey(), key, _quote(value)); - } + /// @dev Ensures type consistency when setting a value - prevents changing types unless uninitialized. + /// Updates type only when the previous type was `None`. + function _ensureTypeConsistency(uint256 chainId, string memory key, Type memory ty) private { + Type memory current = _typeOf[chainId][key]; - /// @notice Sets a string value for a given key on the current chain. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(string memory key, string memory value) public { - set(vm.getChainId(), key, value); + if (current.kind == TypeKind.None) { + _typeOf[chainId][key] = ty; + } else { + current.assertEq(ty); + } } - /// @notice Sets a bytes value for a given key and chain ID. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, bytes memory value) public isCached(chainId) { - Type memory ty = Type(TypeKind.Bytes, false); - _ensureTypeConsistency(chainId, key, ty); - _dataOf[chainId][key] = abi.encode(value); - if (_writeToFile) _writeToToml(chainId, ty.kind.toTomlKey(), key, _quote(vm.toString(value))); + /// @dev Wraps a string in double quotes for JSON compatibility. + function _quote(string memory s) private pure returns (string memory) { + return string.concat('"', s, '"'); } - /// @notice Sets a bytes value for a given key on the current chain. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(string memory key, bytes memory value) public { - set(vm.getChainId(), key, value); + /// @dev Writes a JSON-formatted value to a specific key in the TOML file. + /// @param chainId The chain id to write under. + /// @param ty The type category ('bool', 'address', 'uint', 'bytes32', 'string', or 'bytes'). + /// @param key The variable key name. + /// @param jsonValue The JSON-formatted value to write. + function _writeToToml(uint256 chainId, string memory ty, string memory key, string memory jsonValue) private { + string memory chainKey = _getChainKeyFromId(chainId); + string memory valueKey = string.concat("$.", chainKey, ".", ty, ".", key); + vm.writeToml(jsonValue, _filePath, valueKey); } - // -- SETTER FUNCTIONS (ARRAYS) -------------------------------------------- - - /// @notice Sets a boolean array for a given key and chain ID. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, bool[] memory value) public isCached(chainId) { - Type memory ty = Type(TypeKind.Bool, true); - _ensureTypeConsistency(chainId, key, ty); - _dataOf[chainId][key] = abi.encode(value); - if (_writeToFile) { - string memory json = "["; - for (uint256 i = 0; i < value.length; i++) { - json = string.concat(json, vm.toString(value[i])); - if (i < value.length - 1) json = string.concat(json, ","); + /// @dev Serializes a Variable to JSON format for TOML writing. + function _serializeToJson(Variable memory value) private pure returns (string memory) { + TypeKind kind = value.ty.kind; + bool isArray = value.ty.isArray; + + // single values + if (!isArray) { + if (kind == TypeKind.Bool) { + return abi.decode(value.data, (bool)) ? "true" : "false"; + } else if (kind == TypeKind.Address) { + return _quote(vm.toString(abi.decode(value.data, (address)))); + } else if (kind == TypeKind.Bytes32) { + return _quote(vm.toString(abi.decode(value.data, (bytes32)))); + } else if (kind == TypeKind.Uint256) { + return vm.toString(abi.decode(value.data, (uint256))); + } else if (kind == TypeKind.Int256) { + return vm.toString(abi.decode(value.data, (int256))); + } else if (kind == TypeKind.String) { + return _quote(abi.decode(value.data, (string))); + } else if (kind == TypeKind.Bytes) { + return _quote(vm.toString(abi.decode(value.data, (bytes)))); } - json = string.concat(json, "]"); - _writeToToml(chainId, ty.kind.toTomlKey(), key, json); } - } - - /// @notice Sets a boolean array for a given key on the current chain. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(string memory key, bool[] memory value) public { - set(vm.getChainId(), key, value); - } - /// @notice Sets an address array for a given key and chain ID. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, address[] memory value) public isCached(chainId) { - Type memory ty = Type(TypeKind.Address, true); - _ensureTypeConsistency(chainId, key, ty); - _dataOf[chainId][key] = abi.encode(value); - if (_writeToFile) { - string memory json = "["; - for (uint256 i = 0; i < value.length; i++) { - json = string.concat(json, _quote(vm.toString(value[i]))); - if (i < value.length - 1) json = string.concat(json, ","); + // arrays + string memory json = "["; + if (kind == TypeKind.Bool) { + bool[] memory arr = abi.decode(value.data, (bool[])); + for (uint256 i = 0; i < arr.length; i++) { + json = string.concat(json, arr[i] ? "true" : "false"); + if (i < arr.length - 1) json = string.concat(json, ","); } - json = string.concat(json, "]"); - _writeToToml(chainId, ty.kind.toTomlKey(), key, json); - } - } - - /// @notice Sets an address array for a given key on the current chain. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(string memory key, address[] memory value) public { - set(vm.getChainId(), key, value); - } - - /// @notice Sets a bytes32 array for a given key and chain ID. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, bytes32[] memory value) public isCached(chainId) { - Type memory ty = Type(TypeKind.Bytes32, true); - _ensureTypeConsistency(chainId, key, ty); - _dataOf[chainId][key] = abi.encode(value); - if (_writeToFile) { - string memory json = "["; - for (uint256 i = 0; i < value.length; i++) { - json = string.concat(json, _quote(vm.toString(value[i]))); - if (i < value.length - 1) json = string.concat(json, ","); + } else if (kind == TypeKind.Address) { + address[] memory arr = abi.decode(value.data, (address[])); + for (uint256 i = 0; i < arr.length; i++) { + json = string.concat(json, _quote(vm.toString(arr[i]))); + if (i < arr.length - 1) json = string.concat(json, ","); } - json = string.concat(json, "]"); - _writeToToml(chainId, ty.kind.toTomlKey(), key, json); - } - } - - /// @notice Sets a bytes32 array for a given key on the current chain. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(string memory key, bytes32[] memory value) public { - set(vm.getChainId(), key, value); - } - - /// @notice Sets a uint256 array for a given key and chain ID. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, uint256[] memory value) public isCached(chainId) { - Type memory ty = Type(TypeKind.Uint256, true); - _ensureTypeConsistency(chainId, key, ty); - _dataOf[chainId][key] = abi.encode(value); - if (_writeToFile) { - string memory json = "["; - for (uint256 i = 0; i < value.length; i++) { - json = string.concat(json, vm.toString(value[i])); - if (i < value.length - 1) json = string.concat(json, ","); + } else if (kind == TypeKind.Bytes32) { + bytes32[] memory arr = abi.decode(value.data, (bytes32[])); + for (uint256 i = 0; i < arr.length; i++) { + json = string.concat(json, _quote(vm.toString(arr[i]))); + if (i < arr.length - 1) json = string.concat(json, ","); } - json = string.concat(json, "]"); - _writeToToml(chainId, ty.kind.toTomlKey(), key, json); - } - } - - /// @notice Sets a uint256 array for a given key on the current chain. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(string memory key, uint256[] memory value) public { - set(vm.getChainId(), key, value); - } - - /// @notice Sets a int256 array for a given key and chain ID. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, int256[] memory value) public isCached(chainId) { - Type memory ty = Type(TypeKind.Int256, true); - _ensureTypeConsistency(chainId, key, ty); - _dataOf[chainId][key] = abi.encode(value); - if (_writeToFile) { - string memory json = "["; - for (uint256 i = 0; i < value.length; i++) { - json = string.concat(json, vm.toString(value[i])); - if (i < value.length - 1) json = string.concat(json, ","); + } else if (kind == TypeKind.Uint256) { + uint256[] memory arr = abi.decode(value.data, (uint256[])); + for (uint256 i = 0; i < arr.length; i++) { + json = string.concat(json, vm.toString(arr[i])); + if (i < arr.length - 1) json = string.concat(json, ","); } - json = string.concat(json, "]"); - _writeToToml(chainId, ty.kind.toTomlKey(), key, json); - } - } - - /// @notice Sets a int256 array for a given key on the current chain. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(string memory key, int256[] memory value) public { - set(vm.getChainId(), key, value); - } - - /// @notice Sets a string array for a given key and chain ID. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, string[] memory value) public isCached(chainId) { - Type memory ty = Type(TypeKind.String, true); - _ensureTypeConsistency(chainId, key, ty); - _dataOf[chainId][key] = abi.encode(value); - if (_writeToFile) { - string memory json = "["; - for (uint256 i = 0; i < value.length; i++) { - json = string.concat(json, _quote(value[i])); - if (i < value.length - 1) json = string.concat(json, ","); + } else if (kind == TypeKind.Int256) { + int256[] memory arr = abi.decode(value.data, (int256[])); + for (uint256 i = 0; i < arr.length; i++) { + json = string.concat(json, vm.toString(arr[i])); + if (i < arr.length - 1) json = string.concat(json, ","); } - json = string.concat(json, "]"); - _writeToToml(chainId, ty.kind.toTomlKey(), key, json); - } - } - - /// @notice Sets a string array for a given key on the current chain. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(string memory key, string[] memory value) public { - set(vm.getChainId(), key, value); - } - - /// @notice Sets a bytes array for a given key and chain ID. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(uint256 chainId, string memory key, bytes[] memory value) public isCached(chainId) { - Type memory ty = Type(TypeKind.Bytes, true); - _ensureTypeConsistency(chainId, key, ty); - _dataOf[chainId][key] = abi.encode(value); - if (_writeToFile) { - string memory json = "["; - for (uint256 i = 0; i < value.length; i++) { - json = string.concat(json, _quote(vm.toString(value[i]))); - if (i < value.length - 1) json = string.concat(json, ","); + } else if (kind == TypeKind.String) { + string[] memory arr = abi.decode(value.data, (string[])); + for (uint256 i = 0; i < arr.length; i++) { + json = string.concat(json, _quote(arr[i])); + if (i < arr.length - 1) json = string.concat(json, ","); + } + } else if (kind == TypeKind.Bytes) { + bytes[] memory arr = abi.decode(value.data, (bytes[])); + for (uint256 i = 0; i < arr.length; i++) { + json = string.concat(json, _quote(vm.toString(arr[i]))); + if (i < arr.length - 1) json = string.concat(json, ","); } - json = string.concat(json, "]"); - _writeToToml(chainId, ty.kind.toTomlKey(), key, json); } - } - - /// @notice Sets a bytes array for a given key on the current chain. - /// @dev Sets the cached value in storage and writes the change back to the TOML file if `autoWrite` is enabled. - function set(string memory key, bytes[] memory value) public { - set(vm.getChainId(), key, value); + json = string.concat(json, "]"); + return json; } } diff --git a/test/Config.t.sol b/test/Config.t.sol index 3dc49781..1842ac6f 100644 --- a/test/Config.t.sol +++ b/test/Config.t.sol @@ -6,6 +6,7 @@ import {VmSafe} from "../src/Vm.sol"; import {Config} from "../src/Config.sol"; import {StdConfig} from "../src/StdConfig.sol"; import {ConfigView, LibConfigView} from "../src/LibConfigView.sol"; +import {LibVariable} from "../src/LibVariable.sol"; // forgefmt: disable-start contract ConfigTest is Test, Config { @@ -215,7 +216,7 @@ contract ConfigTest is Test, Config { vm.store(address(_chainConfig[10]), bytes32(uint256(7)), bytes32(uint256(1))); // Cancun StdConfig { - _chainConfig[1].set(1, "is_live", false); + _chainConfig[1].set(1, "is_live", LibVariable.from(false)); assertFalse(_chainConfig[1].get(1, "is_live").toBool()); @@ -223,7 +224,7 @@ contract ConfigTest is Test, Config { assertFalse(vm.parseTomlBool(content, "$.mainnet.bool.is_live")); address new_addr = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; - _chainConfig[1].set(1, "weth", new_addr); + _chainConfig[1].set(1, "weth", LibVariable.from(new_addr)); assertEq(_chainConfig[1].get(1, "weth").toAddress(), new_addr); @@ -234,7 +235,7 @@ contract ConfigTest is Test, Config { new_numbers[0] = 1; new_numbers[1] = 2; new_numbers[2] = 3; - _chainConfig[10].set(10, "number_array", new_numbers); + _chainConfig[10].set(10, "number_array", LibVariable.from(new_numbers)); uint256[] memory updated_numbers_mem = _chainConfig[10].get(10, "number_array").toUint256Array(); assertEq(updated_numbers_mem.length, 3); @@ -252,7 +253,7 @@ contract ConfigTest is Test, Config { string[] memory new_strings = new string[](2); new_strings[0] = "hello"; new_strings[1] = "world"; - _chainConfig[1].set(1, "str_array", new_strings); + _chainConfig[1].set(1, "str_array", LibVariable.from(new_strings)); string[] memory updated_strings_mem = _chainConfig[1].get(1, "str_array").toStringArray(); assertEq(updated_strings_mem.length, 2); @@ -265,14 +266,14 @@ contract ConfigTest is Test, Config { assertEq(updated_strings_disk[0], "hello"); assertEq(updated_strings_disk[1], "world"); - _chainConfig[1].set(1, "new_uint", uint256(42)); + _chainConfig[1].set(1, "new_uint", LibVariable.from(uint256(42))); assertEq(_chainConfig[1].get(1, "new_uint").toUint256(), 42); content = vm.readFile(testConfig); assertEq(vm.parseTomlUint(content, "$.mainnet.uint.new_uint"), 42); - _chainConfig[1].set(1, "new_int", int256(-42)); + _chainConfig[1].set(1, "new_int", LibVariable.from(int256(-42))); assertEq(_chainConfig[1].get(1, "new_int").toInt256(), -42); @@ -282,7 +283,7 @@ contract ConfigTest is Test, Config { int256[] memory new_ints = new int256[](2); new_ints[0] = -100; new_ints[1] = 200; - _chainConfig[10].set(10, "new_ints", new_ints); + _chainConfig[10].set(10, "new_ints", LibVariable.from(new_ints)); int256[] memory updated_ints_mem = _chainConfig[10].get(10, "new_ints").toInt256Array(); assertEq(updated_ints_mem.length, 2); @@ -298,7 +299,7 @@ contract ConfigTest is Test, Config { bytes32[] memory new_words = new bytes32[](2); new_words[0] = bytes32(uint256(0xDEAD)); new_words[1] = bytes32(uint256(0xBEEF)); - _chainConfig[10].set(10, "new_words", new_words); + _chainConfig[10].set(10, "new_words", LibVariable.from(new_words)); bytes32[] memory updated_words_mem = _chainConfig[10].get(10, "new_words").toBytes32Array(); assertEq(updated_words_mem.length, 2); @@ -321,7 +322,7 @@ contract ConfigTest is Test, Config { loadConfig(testConfig, false); // Update a single boolean value and verify the file is NOT changed. - _chainConfig[1].set(1, "is_live", false); + _chainConfig[1].set(1, "is_live", LibVariable.from(false)); string memory content = vm.readFile(testConfig); assertTrue(vm.parseTomlBool(content, "$.mainnet.bool.is_live"), "File should not be updated yet"); @@ -329,7 +330,7 @@ contract ConfigTest is Test, Config { vm.store(address(_chainConfig[1]), bytes32(uint256(7)), bytes32(uint256(1))); // Update the value again and verify the file IS changed. - _chainConfig[1].set(1, "is_live", false); + _chainConfig[1].set(1, "is_live", LibVariable.from(false)); content = vm.readFile(testConfig); assertFalse(vm.parseTomlBool(content, "$.mainnet.bool.is_live"), "File should be updated now"); @@ -337,7 +338,7 @@ contract ConfigTest is Test, Config { _chainConfig[1].writeUpdatesBackToFile(false); // Update the value again and verify the file is NOT changed. - _chainConfig[1].set(1, "is_live", true); + _chainConfig[1].set(1, "is_live", LibVariable.from(true)); content = vm.readFile(testConfig); assertFalse(vm.parseTomlBool(content, "$.mainnet.bool.is_live"), "File should not be updated again"); @@ -382,7 +383,7 @@ contract ConfigTest is Test, Config { // Try to write a value for a non-existent chain ID through the shanghai StdConfig // This should fail because the shanghai StdConfig only manages chain 1, not chain 999999 vm.expectRevert(abi.encodeWithSelector(StdConfig.ChainNotInitialized.selector, uint256(999999))); - _chainConfig[1].set(999999, "some_key", uint256(123)); + _chainConfig[1].set(999999, "some_key", LibVariable.from(uint256(123))); vm.removeFile(singleChainConfig); } From 164636de1f7b588737f225a4ea598d5a1c6c16e5 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Mon, 27 Oct 2025 15:20:20 +0100 Subject: [PATCH 7/7] fix: payable constructor --- .github/workflows/ci.yml | 2 +- test/mocks/MockCounter.sol | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b4e2877..ffa5d04f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: - run: | case "${{ matrix.flags }}" in *"solc:0.8.0"* | *"solc:0.7"* | *"solc:0.6"*) - forge build --skip test --skip Config --skip StdConfig --skip LibVariable --deny-warnings ${{ matrix.flags }} + forge build --skip test --skip Config --skip StdConfig --skip LibVariable --skip MockCounter --deny-warnings ${{ matrix.flags }} ;; *) forge build --skip test --deny-warnings ${{ matrix.flags }} diff --git a/test/mocks/MockCounter.sol b/test/mocks/MockCounter.sol index 71510978..0b74ddfe 100644 --- a/test/mocks/MockCounter.sol +++ b/test/mocks/MockCounter.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: MIT -pragma solidity >=0.6.0 <0.9.0; +pragma solidity ^0.8.13; /// @notice Simple counter contract for testing multi-EVM deployments. contract MockCounter { uint256 public count; + constructor() payable {} + function increment() public { count++; }