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/.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 376654da..9c77556a 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://reth-ethereum.ithaca.xyz/rpc" diff --git a/src/Config.sol b/src/Config.sol index 1c63c872..60ffdf59 100644 --- a/src/Config.sol +++ b/src/Config.sol @@ -3,58 +3,434 @@ 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..00fb5a94 --- /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, 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. +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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 161e3171..61dc1b35 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 configured RPC URL for each chain. + /// @dev Storage for the profile metadata necessary for multi-chain deployments. + mapping(uint256 => VmSafe.ProfileMetadata) private _profileOf; + + /// @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(); } @@ -95,36 +105,52 @@ 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(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("$.", 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; } - uint256 chainId = resolveChainId(chain_key); - _chainKeys.push(chain_key); - // Cache the configure rpc endpoint for that chain. + // This chain matches our EVM version, load it + _chainIds.push(chainId); + _profileOf[chainId] = chainProfile; + _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++) { - 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 {} @@ -231,73 +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) { - for (uint256 i = 0; i < _chainKeys.length; i++) { - if (resolveChainId(_chainKeys[i]) == chainId) { - return _chainKeys[i]; - } - } - revert ChainNotInitialized(chainId); - } - - /// @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); - } - // -- GETTER FUNCTIONS ----------------------------------------------------- /// @dev Reads a variable for a given chain id and key, and returns it in a generic container. @@ -307,7 +266,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 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]); } @@ -323,14 +282,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. @@ -343,271 +295,186 @@ contract StdConfig { return _rpcOf[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 { - 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 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); - } - - /// @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 { - 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 Reads the profile metadata for a specific chain id. + function getProfile(uint256 chainId) public view returns (VmSafe.ProfileMetadata memory) { + return _profileOf[chainId]; } - /// @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 { - set(vm.getChainId(), key, value); + /// @notice Reads the profile metadata for the current chain. + function getProfile() public view returns (VmSafe.ProfileMetadata memory) { + return _profileOf[vm.getChainId()]; } - /// @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 { - 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))); - } + // -- SETTER FUNCTIONS ----------------------------------------------------- - /// @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); - } - - /// @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 { - 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)); - } + /// @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 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); + _writeToFile = enabled; } - /// @notice Sets an int256 value for a given key and chain ID. - function set(uint256 chainId, string memory key, int256 value) public { - 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 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 int256 value for a given key on the current chain. - function set(string memory key, int256 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 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 { - 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)); - } - - /// @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); - } + // -- HELPER FUNCTIONS ----------------------------------------------------- - /// @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 { - 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 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 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 Modifier to ensure chain is initialized before accessing its data. + modifier isCached(uint256 chainId) { + _isCached(chainId); + _; } - // -- 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 { - 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, ","); + /// @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); } - 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 { - 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, ","); - } - json = string.concat(json, "]"); - _writeToToml(chainId, ty.kind.toTomlKey(), key, json); - } + /// @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 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); - } + /// @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 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 { - 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, ","); - } - json = string.concat(json, "]"); - _writeToToml(chainId, ty.kind.toTomlKey(), key, json); + if (current.kind == TypeKind.None) { + _typeOf[chainId][key] = ty; + } else { + current.assertEq(ty); } } - /// @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 { - 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, ","); - } - json = string.concat(json, "]"); - _writeToToml(chainId, ty.kind.toTomlKey(), key, json); - } + /// @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 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); + /// @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); } - /// @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 { - 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, ","); + /// @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 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 { - 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, ","); + // 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 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 { - 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.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, ","); + } + } 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, ","); + } + } 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, ","); + } + } 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, ","); + } + } 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/src/Vm.sol b/src/Vm.sol index 20f91a14..3ee950e8 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. @@ -1998,6 +2006,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..1842ac6f 100644 --- a/test/Config.t.sol +++ b/test/Config.t.sol @@ -2,10 +2,15 @@ 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"; +import {LibVariable} from "../src/LibVariable.sol"; +// forgefmt: disable-start 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 +18,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 +208,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", LibVariable.from(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", LibVariable.from(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", LibVariable.from(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 +250,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", LibVariable.from(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 +266,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", LibVariable.from(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", LibVariable.from(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", LibVariable.from(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 +296,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", LibVariable.from(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 +313,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", LibVariable.from(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", LibVariable.from(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", LibVariable.from(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", LibVariable.from(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..0b74ddfe --- /dev/null +++ b/test/mocks/MockCounter.sol @@ -0,0 +1,17 @@ +// 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 {} + + function increment() public { + count++; + } + + function reset() public { + count = 0; + } +}