Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: superchain address registry #703

Merged
merged 7 commits into from
Mar 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

import {Vm} from "forge-std/Vm.sol";
import {StdChains} from "forge-std/StdChains.sol";
import {stdToml} from "forge-std/StdToml.sol";
import {GameTypes, GameType} from "@eth-optimism-bedrock/src/dispute/lib/Types.sol";

/// @notice Contains getters for arbitrary methods from all L1 contracts, including legacy getters
Expand Down Expand Up @@ -40,108 +39,146 @@ interface IFetcher {
/// @notice This contract provides a single source of truth for storing and retrieving addresses
/// across multiple networks. It handles addresses for contracts and externally owned accounts
/// (EOAs) while ensuring correctness and uniqueness.
contract AddressRegistry is StdChains {
using EnumerableSet for EnumerableSet.UintSet;
contract SuperchainAddressRegistry is StdChains {
using stdToml for string;

address private constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code"))));
Vm private constant vm = Vm(VM_ADDRESS);

/// @dev Structure for storing address details in the contract.
struct RegistryEntry {
address addr;
/// Address (contract or EOA)
/// Indicates if the address is a contract
bool isContract;
}

/// @dev Structure for reading chain list details from toml file
/// @notice Structure for reading chain list details from toml file
struct ChainInfo {
uint256 chainId;
string name;
}

/// @dev Structure for storing address info for a given address.
/// @notice Structure for storing address info for a given address.
struct AddressInfo {
string identifier;
ChainInfo chainInfo;
}

/// @notice Structure used to read in the following hardcoded addresses:
/// - Foundation Upgrade Safe
/// - Foundation Operation Safe
/// - Security Council
struct HardcodedAddress {
address addr;
string identifier;
}

/// @notice Maps an identifier and l2 instance chain ID to a stored address entry.
/// All addresses will live on the same chain.
mapping(string => mapping(uint256 => RegistryEntry)) private registry;
/// @notice Maps a contract identifier to an L2 chain ID to an address.
mapping(string => mapping(uint256 => address)) private registry;

/// @notice Supported L2 chain IDs for this Address Registry instance.
mapping(uint256 => bool) public supportedL2ChainIds;
/// @notice During constructions, tracks if we've seen a chain ID to avoid duplicates and misuse.
mapping(uint256 => bool) public seenL2ChainIds;

/// @notice Maps an address to its identifier and chain info.
mapping(address => AddressInfo) public addressInfo;

/// @notice Array of supported chains and their configurations
ChainInfo[] public chains;

/// @notice loads hardcoded foundation and security council addresses from
/// addresses.toml file
function _loadHardcodedAddresses(string memory chainKey, ChainInfo memory chain) internal {
HardcodedAddress[] memory hardcodedAddresses =
abi.decode(vm.parseToml(vm.readFile("./src/improvements/addresses.toml"), chainKey), (HardcodedAddress[]));
for (uint256 i = 0; i < hardcodedAddresses.length; i++) {
saveAddress(hardcodedAddresses[i].identifier, chain, hardcodedAddresses[i].addr);
}
}

/// @notice Initializes the contract by loading addresses from TOML files
/// and configuring the supported L2 chains.
/// @param networkConfigFilePath the path to the TOML file containing the network configuration(s)
constructor(string memory networkConfigFilePath) {
/// @param configPath the path to the TOML file containing the network configuration(s)
constructor(string memory configPath) {
require(
block.chainid == getChain("mainnet").chainId || block.chainid == getChain("sepolia").chainId,
string.concat("AddressRegistry: Unsupported task chain ID ", vm.toString(block.chainid))
string.concat("SuperchainAddressRegistry: Unsupported task chain ID ", vm.toString(block.chainid))
);

bytes memory chainListContent;
try vm.parseToml(vm.readFile(networkConfigFilePath), ".l2chains") returns (bytes memory parsedChainListContent)
{
require(gasleft() > 500_000, "insufficient gas for readFile() call"); // EIP-150 safe.
try vm.parseToml(vm.readFile(configPath), ".l2chains") returns (bytes memory parsedChainListContent) {
chainListContent = parsedChainListContent;
} catch {
revert(string.concat("AddressRegistry: Failed to parse network config file path ", networkConfigFilePath));
revert(string.concat("SuperchainAddressRegistry: Failed to parse network config file path ", configPath));
}

// Read in the list of OP chains from the config file.
// Cannot assign the abi.decode result to `chains` directly because it's a storage array, so
// compiling without via-ir will fail with:
// Unimplemented feature (/solidity/libsolidity/codegen/ArrayUtils.cpp:228):Copying of type struct AddressRegistry.ChainInfo memory[] memory to storage not yet supported.
ChainInfo[] memory _chains = abi.decode(chainListContent, (ChainInfo[]));
require(_chains.length > 0, "SuperchainAddressRegistry: no chains found");
for (uint256 i = 0; i < _chains.length; i++) {
require(_chains[i].chainId != 0, "SuperchainAddressRegistry: Invalid chain ID in config");
require(bytes(_chains[i].name).length > 0, "SuperchainAddressRegistry: Empty name in config");
require(!seenL2ChainIds[_chains[i].chainId], "SuperchainAddressRegistry: Duplicate chain ID");

seenL2ChainIds[_chains[i].chainId] = true;
chains.push(_chains[i]);
}
require(chains.length > 0, "AddressRegistry: no chains found");

string memory chainAddressesContent =
vm.readFile("lib/superchain-registry/superchain/extra/addresses/addresses.json");
// For each OP chain, read in all addresses for that OP Chain.
string memory chainAddrs = vm.readFile("lib/superchain-registry/superchain/extra/addresses/addresses.json");

for (uint256 i = 0; i < chains.length; i++) {
require(!supportedL2ChainIds[chains[i].chainId], "Duplicate chain ID in chain config");
require(chains[i].chainId != 0, "Invalid chain ID in config");
require(bytes(chains[i].name).length > 0, "Empty name in config");
_processAddresses(chains[i], chainAddrs);

supportedL2ChainIds[chains[i].chainId] = true;
string memory chainKey;
if (block.chainid == getChain("mainnet").chainId) chainKey = ".eth";
else if (block.chainid == getChain("sepolia").chainId) chainKey = ".sep";
else revert(string.concat("SuperchainAddressRegistry: Unknown task chain ID ", vm.toString(block.chainid)));

_processAddresses(chains[i], chainAddressesContent);
_loadHardcodedAddresses(chainKey, chains[i]);
}
}

_loadHardcodedAddresses(
block.chainid == getChain("mainnet").chainId ? ".mainnetAddresses" : ".testnetAddresses", chains[i]
/// @notice Reads in hardcoded addresses from the addresses.toml file.
function _loadHardcodedAddresses(string memory chainKey, ChainInfo memory chain) internal {
string memory toml = vm.readFile("./src/improvements/addresses.toml");
string[] memory keys = vm.parseTomlKeys(toml, chainKey);
require(keys.length > 0, string.concat("SuperchainAddressRegistry: no keys found for ", chainKey));

for (uint256 i = 0; i < keys.length; i++) {
string memory identifier = keys[i];
address addr = toml.readAddress(string.concat(chainKey, ".", identifier));

require(addr != address(0), string.concat("SuperchainAddressRegistry: zero address for ", identifier));
require(
registry[identifier][chain.chainId] == address(0),
string.concat("SuperchainAddressRegistry: address already registered for ", identifier)
);

saveAddress(identifier, chain, addr);
}
}

function saveAddress(string memory identifier, ChainInfo memory chain, address addr) internal {
require(addr != address(0), "Address cannot be zero");
require(registry[identifier][chain.chainId] == address(0), "Address already registered");

registry[identifier][chain.chainId] = addr;
addressInfo[addr] = AddressInfo(identifier, chain);

// Format the chain name: uppercase it and replace spaces with underscores,
// then concatenate with the identifier to form a readable label.
string memory formattedChain = vm.replace(vm.toUppercase(chain.name), " ", "_");
string memory label = string.concat(formattedChain, "_", identifier);
vm.label(addr, label);
}

/// @notice Retrieves an address by its identifier for a specified L2 chain
function getAddress(string memory identifier, uint256 l2ChainId) public view returns (address who_) {
who_ = registry[identifier][l2ChainId];
require(
who_ != address(0),
string.concat(
"SuperchainAddressRegistry: Address not found for ", identifier, " on chain ", vm.toString(l2ChainId)
)
);
}

/// @notice Retrieves the identifier and chain info for a given address.
function getAddressInfo(address addr) public view returns (AddressInfo memory) {
require(
bytes(addressInfo[addr].identifier).length != 0,
string.concat("SuperchainAddressRegistry: Address Info not found for ", vm.toString(addr))
);
return addressInfo[addr];
}

/// @notice Returns the list of supported chains
function getChains() public view returns (ChainInfo[] memory) {
return chains;
}

// ========================================================
// ======== Superchain address discovery functions ========
// ========================================================

/// @dev Processes all configurations for a given chain.
function _processAddresses(ChainInfo memory chain, string memory chainAddressesContent) internal {
uint256 chainId = chain.chainId; // L2 chain ID.
Expand Down Expand Up @@ -213,103 +250,6 @@ contract AddressRegistry is StdChains {
saveAddress("ProxyAdminOwner", chain, proxyAdminOwner);
}

function saveAddress(string memory identifier, ChainInfo memory chain, address addr) internal {
require(addr != address(0), "Address cannot be zero");
require(registry[identifier][chain.chainId].addr == address(0), "Address already registered");

registry[identifier][chain.chainId] = RegistryEntry(addr, addr.code.length > 0);
addressInfo[addr] = AddressInfo(identifier, chain);

// Format the chain name: uppercase it and replace spaces with underscores,
// then concatenate with the identifier to form a readable label.
string memory formattedChain = vm.replace(vm.toUppercase(chain.name), " ", "_");
string memory label = string.concat(formattedChain, "_", identifier);
vm.label(addr, label);
}

/// @notice Retrieves an address by its identifier for a specified L2 chain
/// @param identifier The unique identifier associated with the address
/// @param l2ChainId The chain ID of the L2 network
/// @return The address associated with the given identifier on the specified chain
function getAddress(string memory identifier, uint256 l2ChainId) public view returns (address) {
_l2ChainIdSupported(l2ChainId);

// Fetch the stored registry entry
RegistryEntry memory entry = registry[identifier][l2ChainId];
address resolvedAddress = entry.addr;

require(resolvedAddress != address(0), "Address not found");

return resolvedAddress;
}

/// @notice Retrieves the identifier and chain info for a given address.
/// @param addr The address to retrieve info for.
/// @return The identifier and chain info for the given address.
function getAddressInfo(address addr) public view returns (AddressInfo memory) {
require(bytes(addressInfo[addr].identifier).length != 0, "Address Info not found");
return addressInfo[addr];
}

/// @notice Checks if an address is a contract for a given identifier and L2 chain
/// @param identifier The unique identifier associated with the address
/// @param l2ChainId The chain ID of the L2 network
/// @return True if the address is a contract, false otherwise
function isAddressContract(string memory identifier, uint256 l2ChainId) public view returns (bool) {
_l2ChainIdSupported(l2ChainId);
_checkAddressRegistered(identifier, l2ChainId);

return registry[identifier][l2ChainId].isContract;
}

/// @notice Checks if an address exists for a specified identifier and L2 chain
/// @param identifier The unique identifier associated with the address
/// @param l2ChainId The chain ID of the L2 network
/// @return True if the address exists, false otherwise
function isAddressRegistered(string memory identifier, uint256 l2ChainId) public view returns (bool) {
return registry[identifier][l2ChainId].addr != address(0);
}

/// @notice Verifies that an address is registered for a given identifier and chain
/// @dev Reverts if the address is not registered
/// @param identifier The unique identifier associated with the address
/// @param l2ChainId The chain ID of the L2 network
function _checkAddressRegistered(string memory identifier, uint256 l2ChainId) private view {
require(
isAddressRegistered(identifier, l2ChainId),
string(
abi.encodePacked("Address not found for identifier ", identifier, " on chain ", vm.toString(l2ChainId))
)
);
}

/// @notice Returns the list of supported chains
/// @return An array of ChainInfo structs representing the supported chains
function getChains() public view returns (ChainInfo[] memory) {
return chains;
}

/// @notice Verifies that the given L2 chain ID is supported
/// @param l2ChainId The chain ID of the L2 network to verify
function _l2ChainIdSupported(uint256 l2ChainId) private view {
require(
supportedL2ChainIds[l2ChainId],
string(abi.encodePacked("L2 Chain ID ", vm.toString(l2ChainId), " not supported"))
);
}

/// @notice Validates whether an address matches its expected type (contract or EOA)
/// @dev Reverts if the address type does not match the expected type
/// @param addr The address to validate
/// @param isContract True if the address should be a contract, false if it should be an EOA
function _typeCheckAddress(address addr, bool isContract) private view {
if (isContract) {
require(addr.code.length > 0, "Address must contain code");
} else {
require(addr.code.length == 0, "Address must not contain code");
}
}

/// @dev Saves all dispute game related registry entries.
function _saveDisputeGameEntries(ChainInfo memory chain, address disputeGameFactoryProxy) internal {
saveAddress("DisputeGameFactoryProxy", chain, disputeGameFactoryProxy);
Expand Down
37 changes: 10 additions & 27 deletions src/improvements/addresses.toml
Original file line number Diff line number Diff line change
@@ -1,27 +1,10 @@
[[mainnetAddresses]]
identifier = "FoundationUpgradeSafe"
addr = "0x847B5c174615B1B7fDF770882256e2D3E95b9D92"

[[mainnetAddresses]]
identifier = "FoundationOperationSafe"
addr = "0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A"

[[mainnetAddresses]]
identifier = "SecurityCouncil"
addr = "0xc2819DC788505Aac350142A7A707BF9D03E3Bd03"

[[mainnetAddresses]]
identifier = "ChainGovernorSafe"
addr = "0xb0c4C487C5cf6d67807Bc2008c66fa7e2cE744EC"

[[testnetAddresses]]
identifier = "FoundationUpgradeSafe"
addr = "0xDEe57160aAfCF04c34C887B5962D0a69676d3C8B"

[[testnetAddresses]]
identifier = "FoundationOperationSafe"
addr = "0x837DE453AD5F21E89771e3c06239d8236c0EFd5E"

[[testnetAddresses]]
identifier = "SecurityCouncil"
addr = "0xf64bc17485f0B4Ea5F06A96514182FC4cB561977"
[eth]
ChainGovernorSafe = "0xb0c4C487C5cf6d67807Bc2008c66fa7e2cE744EC"
FoundationOperationSafe = "0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A"
FoundationUpgradeSafe = "0x847B5c174615B1B7fDF770882256e2D3E95b9D92"
SecurityCouncil = "0xc2819DC788505Aac350142A7A707BF9D03E3Bd03"

[sep]
FoundationOperationSafe = "0x837DE453AD5F21E89771e3c06239d8236c0EFd5E"
FoundationUpgradeSafe = "0xDEe57160aAfCF04c34C887B5962D0a69676d3C8B"
SecurityCouncil = "0xf64bc17485f0B4Ea5F06A96514182FC4cB561977"
10 changes: 8 additions & 2 deletions src/improvements/script/get-safe.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,22 @@ fi

# Check if the path contains eth/ or sep/
if [[ "$TASK_PATH" == *"/eth/"* ]]; then
safe=$(yq '.mainnetAddresses[] | select(.identifier == "'"$SAFE_NAME"'") | .addr' "${TOML_PATH}"/addresses.toml)
safe=$(yq ".eth.\"$SAFE_NAME\"" "${TOML_PATH}/addresses.toml")
elif [[ "$TASK_PATH" == *"/sep/"* ]]; then
if [[ "$SAFE_NAME" == "ChainGovernorSafe" ]]; then
echo "Error: chain-governor does not exist on sepolia" >&2
exit 1
fi
safe=$(yq '.testnetAddresses[] | select(.identifier == "'"$SAFE_NAME"'") | .addr' "${TOML_PATH}"/addresses.toml)
safe=$(yq ".sep.\"$SAFE_NAME\"" "${TOML_PATH}/addresses.toml")
else
echo "Error: Task path must contain either /eth/ or /sep/" >&2
exit 1
fi

# Ensure a value was found for the safe
if [[ -z "$safe" || "$safe" == "null" ]]; then
echo "Error: SAFE_NAME '$SAFE_NAME' not found in ${TOML_PATH}/addresses.toml" >&2
exit 1
fi

echo "$safe"
Loading