Skip to content

Commit c12c6ad

Browse files
adhussonZeroEkkusumds1
authored
feat: improve StdChains (#247)
* feat: improve StdChains * fix: faster compile times through initialize fn * Update src/StdChains.sol Co-authored-by: Zero Ekkusu <[email protected]> * Update src/StdChains.sol Co-authored-by: Zero Ekkusu <[email protected]> * Update src/StdChains.sol Co-authored-by: Zero Ekkusu <[email protected]> * Update src/StdChains.sol Co-authored-by: Zero Ekkusu <[email protected]> * Update src/StdChains.sol Co-authored-by: Zero Ekkusu <[email protected]> * fix: cleanup error messages & tests * apply fixes * uint -> uint256 * _* internal mappings * initialize on setChain * prevent initialize reentrancy * forge fmt * fix: correct visibility * chore: use selective import * chore: add clarifying comments * docs: add comments explaining how StdChains works and how to use it * test: validate all default RPCs * refactor: update withRpcUrl * style: forge fmt Co-authored-by: Zero Ekkusu <[email protected]> Co-authored-by: Matt Solomon <[email protected]>
1 parent a414a03 commit c12c6ad

File tree

3 files changed

+219
-57
lines changed

3 files changed

+219
-57
lines changed

foundry.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ fs_permissions = [{ access = "read-write", path = "./"}]
66
mainnet = "https://mainnet.infura.io/v3/7a8769b798b642f6933f2ed52042bd70" # Different API key.
77
optimism_goerli = "https://goerli.optimism.io/" # Adds a trailing slash.
88
arbitrum_one_goerli = "https://goerli-rollup.arbitrum.io/rpc/" # Adds a trailing slash.
9+
needs_undefined_env_var = "${UNDEFINED_RPC_URL_PLACEHOLDER}"
910

1011
[fmt]
1112
# These are all the `forge fmt` defaults.

src/StdChains.sol

Lines changed: 142 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,42 @@ pragma solidity >=0.6.2 <0.9.0;
33

44
pragma experimental ABIEncoderV2;
55

6-
import "./Vm.sol";
6+
import {VmSafe} from "./Vm.sol";
77

8+
/**
9+
* StdChains provides information about EVM compatible chains that can be used in scripts/tests.
10+
* For each chain, the chain's name, chain ID, and a default RPC URL are provided. Chains are
11+
* identified by their alias, which is the same as the alias in the `[rpc_endpoints]` section of
12+
* the `foundry.toml` file. For best UX, ensure the alias in the `foundry.toml` file match the
13+
* alias used in this contract, which can be found as the first argument to the
14+
* `setChainWithDefaultRpcUrl` call in the `initialize` function.
15+
*
16+
* There are two main ways to use this contract:
17+
* 1. Set a chain with `setChain(string memory chainAlias, Chain memory chain)`
18+
* 2. Get a chain with `getChain(string memory chainAlias)` or `getChain(uint256 chainId)`.
19+
*
20+
* The first time either of those are used, chains are initialized with the default set of RPC URLs.
21+
* This is done in `initialize`, which uses `setChainWithDefaultRpcUrl`. Defaults are recorded in
22+
* `defaultRpcUrls`.
23+
*
24+
* The `setChain` function is straightforward, and it simply saves off the given chain data.
25+
*
26+
* The `getChain` methods use `getChainWithUpdatedRpcUrl` to return a chain. For example, let's say
27+
* we want to retrieve `mainnet`'s RPC URL:
28+
* - If you haven't set any mainnet chain info with `setChain` and you haven't specified that
29+
* chain in `foundry.toml`, the default data and RPC URL will be returned.
30+
* - If you have set a mainnet RPC URL in `foundry.toml` it will return that, if valid (e.g. if
31+
* a URL is given or if an environment variable is given and that environment variable exists).
32+
* Otherwise, the default data is returned.
33+
* - If you specified data with `setChain` it will return that.
34+
*
35+
* Summarizing the above, the prioritization hierarchy is `setChain` -> `foundry.toml` -> defaults.
36+
*/
837
abstract contract StdChains {
938
VmSafe private constant vm = VmSafe(address(uint160(uint256(keccak256("hevm cheat code")))));
1039

40+
bool private initialized;
41+
1142
struct Chain {
1243
// The chain name.
1344
string name;
@@ -20,71 +51,139 @@ abstract contract StdChains {
2051
string rpcUrl;
2152
}
2253

23-
bool private initialized;
24-
25-
// Maps from a chain's key (matching the alias in the `foundry.toml` file) to chain data.
54+
// Maps from the chain's alias (matching the alias in the `foundry.toml` file) to chain data.
2655
mapping(string => Chain) private chains;
27-
// Maps from a chain ID to that chain's key name
28-
mapping(uint256 => string) private idToKey;
56+
// Maps from the chain's alias to it's default RPC URL.
57+
mapping(string => string) private defaultRpcUrls;
58+
// Maps from a chain ID to it's alias.
59+
mapping(uint256 => string) private idToAlias;
60+
61+
// The RPC URL will be fetched from config or defaultRpcUrls if possible.
62+
function getChain(string memory chainAlias) internal virtual returns (Chain memory chain) {
63+
require(bytes(chainAlias).length != 0, "StdChains getChain(string): Chain alias cannot be the empty string.");
2964

30-
function getChain(string memory key) internal virtual returns (Chain memory) {
3165
initialize();
32-
return chains[key];
66+
chain = chains[chainAlias];
67+
require(
68+
chain.chainId != 0,
69+
string(abi.encodePacked("StdChains getChain(string): Chain with alias \"", chainAlias, "\" not found."))
70+
);
71+
72+
chain = getChainWithUpdatedRpcUrl(chainAlias, chain);
3373
}
3474

35-
function getChain(uint256 chainId) internal virtual returns (Chain memory) {
75+
function getChain(uint256 chainId) internal virtual returns (Chain memory chain) {
76+
require(chainId != 0, "StdChains getChain(uint256): Chain ID cannot be 0.");
3677
initialize();
37-
return chains[idToKey[chainId]];
78+
string memory chainAlias = idToAlias[chainId];
79+
80+
chain = chains[chainAlias];
81+
82+
require(
83+
chain.chainId != 0,
84+
string(abi.encodePacked("StdChains getChain(uint256): Chain with ID ", vm.toString(chainId), " not found."))
85+
);
86+
87+
chain = getChainWithUpdatedRpcUrl(chainAlias, chain);
3888
}
3989

40-
function setChain(string memory key, string memory name, uint256 chainId, string memory rpcUrl) internal virtual {
90+
// set chain info, with priority to argument's rpcUrl field.
91+
function setChain(string memory chainAlias, Chain memory chain) internal virtual {
92+
require(
93+
bytes(chainAlias).length != 0, "StdChains setChain(string,Chain): Chain alias cannot be the empty string."
94+
);
95+
96+
require(chain.chainId != 0, "StdChains setChain(string,Chain): Chain ID cannot be 0.");
97+
98+
initialize();
99+
string memory foundAlias = idToAlias[chain.chainId];
100+
41101
require(
42-
keccak256(bytes(idToKey[chainId])) == keccak256(bytes(""))
43-
|| keccak256(bytes(idToKey[chainId])) == keccak256(bytes(key)),
102+
bytes(foundAlias).length == 0 || keccak256(bytes(foundAlias)) == keccak256(bytes(chainAlias)),
44103
string(
45104
abi.encodePacked(
46-
"StdChains setChain(string,string,uint256,string): Chain ID ",
47-
vm.toString(chainId),
105+
"StdChains setChain(string,Chain): Chain ID ",
106+
vm.toString(chain.chainId),
48107
" already used by \"",
49-
idToKey[chainId],
108+
foundAlias,
50109
"\"."
51110
)
52111
)
53112
);
54113

55-
uint256 oldChainId = chains[key].chainId;
56-
delete idToKey[oldChainId];
114+
uint256 oldChainId = chains[chainAlias].chainId;
115+
delete idToAlias[oldChainId];
116+
117+
chains[chainAlias] = chain;
118+
idToAlias[chain.chainId] = chainAlias;
119+
}
57120

58-
chains[key] = Chain(name, chainId, rpcUrl);
59-
idToKey[chainId] = key;
121+
// lookup rpcUrl, in descending order of priority:
122+
// current -> config (foundry.toml) -> default
123+
function getChainWithUpdatedRpcUrl(string memory chainAlias, Chain memory chain)
124+
private
125+
view
126+
returns (Chain memory)
127+
{
128+
if (bytes(chain.rpcUrl).length == 0) {
129+
try vm.rpcUrl(chainAlias) returns (string memory configRpcUrl) {
130+
chain.rpcUrl = configRpcUrl;
131+
} catch (bytes memory err) {
132+
chain.rpcUrl = defaultRpcUrls[chainAlias];
133+
// distinguish 'not found' from 'cannot read'
134+
bytes memory notFoundError =
135+
abi.encodeWithSignature("CheatCodeError", string(abi.encodePacked("invalid rpc url ", chainAlias)));
136+
if (keccak256(notFoundError) != keccak256(err) || bytes(chain.rpcUrl).length == 0) {
137+
/// @solidity memory-safe-assembly
138+
assembly {
139+
revert(add(32, err), mload(err))
140+
}
141+
}
142+
}
143+
}
144+
return chain;
60145
}
61146

62147
function initialize() private {
63148
if (initialized) return;
64149

65-
setChain("anvil", "Anvil", 31337, "http://127.0.0.1:8545");
66-
setChain("mainnet", "Mainnet", 1, "https://mainnet.infura.io/v3/6770454bc6ea42c58aac12978531b93f");
67-
setChain("goerli", "Goerli", 5, "https://goerli.infura.io/v3/6770454bc6ea42c58aac12978531b93f");
68-
setChain("sepolia", "Sepolia", 11155111, "https://rpc.sepolia.dev");
69-
setChain("optimism", "Optimism", 10, "https://mainnet.optimism.io");
70-
setChain("optimism_goerli", "Optimism Goerli", 420, "https://goerli.optimism.io");
71-
setChain("arbitrum_one", "Arbitrum One", 42161, "https://arb1.arbitrum.io/rpc");
72-
setChain("arbitrum_one_goerli", "Arbitrum One Goerli", 421613, "https://goerli-rollup.arbitrum.io/rpc");
73-
setChain("arbitrum_nova", "Arbitrum Nova", 42170, "https://nova.arbitrum.io/rpc");
74-
setChain("polygon", "Polygon", 137, "https://polygon-rpc.com");
75-
setChain("polygon_mumbai", "Polygon Mumbai", 80001, "https://rpc-mumbai.matic.today");
76-
setChain("avalanche", "Avalanche", 43114, "https://api.avax.network/ext/bc/C/rpc");
77-
setChain("avalanche_fuji", "Avalanche Fuji", 43113, "https://api.avax-test.network/ext/bc/C/rpc");
78-
setChain("bnb_smart_chain", "BNB Smart Chain", 56, "https://bsc-dataseed1.binance.org");
79-
setChain("bnb_smart_chain_testnet", "BNB Smart Chain Testnet", 97, "https://data-seed-prebsc-1-s1.binance.org:8545");// forgefmt: disable-line
80-
setChain("gnosis_chain", "Gnosis Chain", 100, "https://rpc.gnosischain.com");
81-
82-
// Loop over RPC URLs in the config file to replace the default RPC URLs
83-
Vm.Rpc[] memory rpcs = vm.rpcUrlStructs();
84-
for (uint256 i = 0; i < rpcs.length; i++) {
85-
chains[rpcs[i].key].rpcUrl = rpcs[i].url;
86-
}
87-
88150
initialized = true;
151+
152+
// If adding an RPC here, make sure to test the default RPC URL in `testRpcs`
153+
setChainWithDefaultRpcUrl("anvil", Chain("Anvil", 31337, "http://127.0.0.1:8545"));
154+
setChainWithDefaultRpcUrl(
155+
"mainnet", Chain("Mainnet", 1, "https://mainnet.infura.io/v3/6770454bc6ea42c58aac12978531b93f")
156+
);
157+
setChainWithDefaultRpcUrl(
158+
"goerli", Chain("Goerli", 5, "https://goerli.infura.io/v3/6770454bc6ea42c58aac12978531b93f")
159+
);
160+
setChainWithDefaultRpcUrl(
161+
"sepolia", Chain("Sepolia", 11155111, "https://sepolia.infura.io/v3/6770454bc6ea42c58aac12978531b93f")
162+
);
163+
setChainWithDefaultRpcUrl("optimism", Chain("Optimism", 10, "https://mainnet.optimism.io"));
164+
setChainWithDefaultRpcUrl("optimism_goerli", Chain("Optimism Goerli", 420, "https://goerli.optimism.io"));
165+
setChainWithDefaultRpcUrl("arbitrum_one", Chain("Arbitrum One", 42161, "https://arb1.arbitrum.io/rpc"));
166+
setChainWithDefaultRpcUrl(
167+
"arbitrum_one_goerli", Chain("Arbitrum One Goerli", 421613, "https://goerli-rollup.arbitrum.io/rpc")
168+
);
169+
setChainWithDefaultRpcUrl("arbitrum_nova", Chain("Arbitrum Nova", 42170, "https://nova.arbitrum.io/rpc"));
170+
setChainWithDefaultRpcUrl("polygon", Chain("Polygon", 137, "https://polygon-rpc.com"));
171+
setChainWithDefaultRpcUrl("polygon_mumbai", Chain("Polygon Mumbai", 80001, "https://rpc-mumbai.maticvigil.com"));
172+
setChainWithDefaultRpcUrl("avalanche", Chain("Avalanche", 43114, "https://api.avax.network/ext/bc/C/rpc"));
173+
setChainWithDefaultRpcUrl(
174+
"avalanche_fuji", Chain("Avalanche Fuji", 43113, "https://api.avax-test.network/ext/bc/C/rpc")
175+
);
176+
setChainWithDefaultRpcUrl("bnb_smart_chain", Chain("BNB Smart Chain", 56, "https://bsc-dataseed1.binance.org"));
177+
setChainWithDefaultRpcUrl("bnb_smart_chain_testnet", Chain("BNB Smart Chain Testnet", 97, "https://data-seed-prebsc-1-s1.binance.org:8545"));// forgefmt: disable-line
178+
setChainWithDefaultRpcUrl("gnosis_chain", Chain("Gnosis Chain", 100, "https://rpc.gnosischain.com"));
179+
}
180+
181+
// set chain info, with priority to chainAlias' rpc url in foundry.toml
182+
function setChainWithDefaultRpcUrl(string memory chainAlias, Chain memory chain) private {
183+
string memory rpcUrl = chain.rpcUrl;
184+
defaultRpcUrls[chainAlias] = rpcUrl;
185+
chain.rpcUrl = "";
186+
setChain(chainAlias, chain);
187+
chain.rpcUrl = rpcUrl; // restore argument
89188
}
90189
}

test/StdChains.t.sol

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,61 @@ contract StdChainsTest is Test {
1212

1313
// Other RPCs should remain unchanged.
1414
assertEq(getChain(31337).rpcUrl, "http://127.0.0.1:8545");
15-
assertEq(getChain("sepolia").rpcUrl, "https://rpc.sepolia.dev");
15+
assertEq(getChain("sepolia").rpcUrl, "https://sepolia.infura.io/v3/6770454bc6ea42c58aac12978531b93f");
16+
}
17+
18+
function testRpc(string memory rpcAlias) internal {
19+
string memory rpcUrl = getChain(rpcAlias).rpcUrl;
20+
vm.createSelectFork(rpcUrl);
1621
}
1722

1823
// Ensure we can connect to the default RPC URL for each chain.
1924
function testRpcs() public {
20-
(string[2][] memory rpcs) = vm.rpcUrls();
21-
for (uint256 i = 0; i < rpcs.length; i++) {
22-
( /* string memory name */ , string memory rpcUrl) = (rpcs[i][0], rpcs[i][1]);
23-
vm.createSelectFork(rpcUrl);
24-
}
25+
testRpc("mainnet");
26+
testRpc("goerli");
27+
testRpc("sepolia");
28+
testRpc("optimism");
29+
testRpc("optimism_goerli");
30+
testRpc("arbitrum_one");
31+
testRpc("arbitrum_one_goerli");
32+
testRpc("arbitrum_nova");
33+
testRpc("polygon");
34+
testRpc("polygon_mumbai");
35+
testRpc("avalanche");
36+
testRpc("avalanche_fuji");
37+
testRpc("bnb_smart_chain");
38+
testRpc("bnb_smart_chain_testnet");
39+
testRpc("gnosis_chain");
2540
}
2641

27-
function testCannotSetChain_ChainIdExists() public {
28-
setChain({key: "custom_chain", name: "Custom Chain", chainId: 123456789, rpcUrl: "https://custom.chain/"});
42+
function testChainNoDefault() public {
43+
vm.expectRevert("StdChains getChain(string): Chain with alias \"does_not_exist\" not found.");
44+
getChain("does_not_exist");
45+
}
46+
47+
function testSetChainFirstFails() public {
48+
vm.expectRevert("StdChains setChain(string,Chain): Chain ID 31337 already used by \"anvil\".");
49+
setChain("anvil2", Chain("Anvil", 31337, "URL"));
50+
}
2951

52+
function testChainBubbleUp() public {
53+
setChain("needs_undefined_env_var", Chain("", 123456789, ""));
3054
vm.expectRevert(
31-
'StdChains setChain(string,string,uint256,string): Chain ID 123456789 already used by "custom_chain".'
55+
"Failed to resolve env var `UNDEFINED_RPC_URL_PLACEHOLDER` in `${UNDEFINED_RPC_URL_PLACEHOLDER}`: environment variable not found"
3256
);
57+
getChain("needs_undefined_env_var");
58+
}
59+
60+
function testCannotSetChain_ChainIdExists() public {
61+
setChain("custom_chain", Chain("Custom Chain", 123456789, "https://custom.chain/"));
3362

34-
setChain({key: "another_custom_chain", name: "", chainId: 123456789, rpcUrl: ""});
63+
vm.expectRevert('StdChains setChain(string,Chain): Chain ID 123456789 already used by "custom_chain".');
64+
65+
setChain("another_custom_chain", Chain("", 123456789, ""));
3566
}
3667

3768
function testSetChain() public {
38-
setChain({key: "custom_chain", name: "Custom Chain", chainId: 123456789, rpcUrl: "https://custom.chain/"});
69+
setChain("custom_chain", Chain("Custom Chain", 123456789, "https://custom.chain/"));
3970
Chain memory customChain = getChain("custom_chain");
4071
assertEq(customChain.name, "Custom Chain");
4172
assertEq(customChain.chainId, 123456789);
@@ -46,12 +77,43 @@ contract StdChainsTest is Test {
4677
assertEq(chainById.rpcUrl, customChain.rpcUrl);
4778
}
4879

80+
function testSetNoEmptyAlias() public {
81+
vm.expectRevert("StdChains setChain(string,Chain): Chain alias cannot be the empty string.");
82+
setChain("", Chain("", 123456789, ""));
83+
}
84+
85+
function testSetNoChainId0() public {
86+
vm.expectRevert("StdChains setChain(string,Chain): Chain ID cannot be 0.");
87+
setChain("alias", Chain("", 0, ""));
88+
}
89+
90+
function testGetNoChainId0() public {
91+
vm.expectRevert("StdChains getChain(uint256): Chain ID cannot be 0.");
92+
getChain(0);
93+
}
94+
95+
function testGetNoEmptyAlias() public {
96+
vm.expectRevert("StdChains getChain(string): Chain alias cannot be the empty string.");
97+
getChain("");
98+
}
99+
100+
function testChainIdNotFound() public {
101+
vm.expectRevert("StdChains getChain(string): Chain with alias \"no_such_alias\" not found.");
102+
getChain("no_such_alias");
103+
}
104+
105+
function testChainAliasNotFound() public {
106+
vm.expectRevert("StdChains getChain(uint256): Chain with ID 321 not found.");
107+
getChain(321);
108+
}
109+
49110
function testSetChain_ExistingOne() public {
50-
setChain({key: "custom_chain", name: "Custom Chain", chainId: 123456789, rpcUrl: "https://custom.chain/"});
111+
setChain("custom_chain", Chain("Custom Chain", 123456789, "https://custom.chain/"));
51112
assertEq(getChain(123456789).chainId, 123456789);
52113

53-
setChain({key: "custom_chain", name: "Modified Chain", chainId: 999999999, rpcUrl: "https://modified.chain/"});
54-
assertEq(getChain(123456789).chainId, 0);
114+
setChain("custom_chain", Chain("Modified Chain", 999999999, "https://modified.chain/"));
115+
vm.expectRevert("StdChains getChain(uint256): Chain with ID 123456789 not found.");
116+
getChain(123456789);
55117

56118
Chain memory modifiedChain = getChain(999999999);
57119
assertEq(modifiedChain.name, "Modified Chain");

0 commit comments

Comments
 (0)