Skip to content

Commit c6aecac

Browse files
authored
better security and UX with LibVariable (#716)
1 parent 6a53426 commit c6aecac

File tree

7 files changed

+904
-503
lines changed

7 files changed

+904
-503
lines changed

src/Base.sol

Lines changed: 2 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity >=0.6.2 <0.9.0;
3-
pragma experimental ABIEncoderV2;
43

5-
import {console} from "./console.sol";
64
import {StdStorage} from "./StdStorage.sol";
7-
import {StdConfig} from "./StdConfig.sol";
85
import {Vm, VmSafe} from "./Vm.sol";
96

107
abstract contract CommonBase {
@@ -44,55 +41,8 @@ abstract contract CommonBase {
4441
StdStorage internal stdstore;
4542
}
4643

47-
/// @notice Boilerplate to streamline the setup of multi-chain testing environments.
48-
abstract contract CommonConfig is CommonBase {
49-
// -- STORAGE (CONFIG + CHAINS + FORKS) ------------------------------------
44+
abstract contract TestBase is CommonBase {}
5045

51-
StdConfig internal config;
52-
uint256[] internal chainIds;
53-
mapping(uint256 => uint256) internal forkOf; // [chainId -> forkId]
54-
55-
// -- HELPER FUNCTIONS -----------------------------------------------------
56-
57-
/// @notice Loads configuration from a file.
58-
///
59-
/// @dev This function instantiates a `Config` contract, caching all its config variables.
60-
///
61-
/// @param filePath: the path to the TOML configuration file.
62-
function _loadConfig(string memory filePath) internal {
63-
console.log("----------");
64-
console.log(string(abi.encodePacked("Loading config from '", filePath, "'")));
65-
config = new StdConfig(filePath);
66-
vm.makePersistent(address(config));
67-
console.log("Config successfully loaded");
68-
console.log("----------");
69-
}
70-
71-
/// @notice Loads configuration from a file and creates forks for each specified chain.
72-
///
73-
/// @dev This function instantiates a `Config` contract, caching all its config variables,
74-
/// reads the configured chain ids, and iterates through them to create a fork for each one.
75-
/// It also creates a map `forkOf[chainId] -> forkId` to easily switch between forks.
76-
///
77-
/// @param filePath: the path to the TOML configuration file.
78-
function _loadConfigAndForks(string memory filePath) internal {
79-
_loadConfig(filePath);
80-
81-
console.log("Setting up forks for the configured chains...");
82-
uint256[] memory chains = config.getChainIds();
83-
for (uint256 i = 0; i < chains.length; i++) {
84-
uint256 chainId = chains[i];
85-
uint256 forkId = vm.createFork(config.getRpcUrl(chainId));
86-
forkOf[chainId] = forkId;
87-
chainIds.push(chainId);
88-
}
89-
console.log("Forks successfully created");
90-
console.log("----------");
91-
}
92-
}
93-
94-
abstract contract TestBase is CommonConfig {}
95-
96-
abstract contract ScriptBase is CommonConfig {
46+
abstract contract ScriptBase is CommonBase {
9747
VmSafe internal constant vmSafe = VmSafe(VM_ADDRESS);
9848
}

src/Config.sol

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity >=0.6.2 <0.9.0;
3+
pragma experimental ABIEncoderV2;
4+
5+
import {console} from "./console.sol";
6+
import {StdConfig} from "./StdConfig.sol";
7+
import {CommonBase} from "./Base.sol";
8+
9+
/// @notice Boilerplate to streamline the setup of multi-chain environments.
10+
abstract contract Config is CommonBase {
11+
// -- STORAGE (CONFIG + CHAINS + FORKS) ------------------------------------
12+
13+
/// @notice Contract instance holding the data from the TOML config file.
14+
StdConfig internal config;
15+
16+
/// @notice Array of chain IDs for which forks have been created.
17+
uint256[] internal chainIds;
18+
19+
/// @notice A mapping from a chain ID to its initialized fork ID.
20+
mapping(uint256 => uint256) internal forkOf;
21+
22+
// -- HELPER FUNCTIONS -----------------------------------------------------
23+
24+
/// @notice Loads configuration from a file.
25+
///
26+
/// @dev This function instantiates a `Config` contract, caching all its config variables.
27+
///
28+
/// @param filePath: the path to the TOML configuration file.
29+
function _loadConfig(string memory filePath) internal {
30+
console.log("----------");
31+
console.log(string(abi.encodePacked("Loading config from '", filePath, "'")));
32+
config = new StdConfig(filePath);
33+
vm.makePersistent(address(config));
34+
console.log("Config successfully loaded");
35+
console.log("----------");
36+
}
37+
38+
/// @notice Loads configuration from a file and creates forks for each specified chain.
39+
///
40+
/// @dev This function instantiates a `Config` contract, caching all its config variables,
41+
/// reads the configured chain ids, and iterates through them to create a fork for each one.
42+
/// It also creates a map `forkOf[chainId] -> forkId` to easily switch between forks.
43+
///
44+
/// @param filePath: the path to the TOML configuration file.
45+
function _loadConfigAndForks(string memory filePath) internal {
46+
_loadConfig(filePath);
47+
48+
console.log("Setting up forks for the configured chains...");
49+
uint256[] memory chains = config.getChainIds();
50+
for (uint256 i = 0; i < chains.length; i++) {
51+
uint256 chainId = chains[i];
52+
uint256 forkId = vm.createFork(config.getRpcUrl(chainId));
53+
forkOf[chainId] = forkId;
54+
chainIds.push(chainId);
55+
}
56+
console.log("Forks successfully created");
57+
console.log("----------");
58+
}
59+
}

src/LibVariable.sol

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity >=0.6.2 <0.9.0;
3+
4+
using LibVariable for Variable global;
5+
6+
struct Variable {
7+
Type ty;
8+
bytes data;
9+
}
10+
11+
struct Type {
12+
TypeKind kind;
13+
bool isArray;
14+
}
15+
16+
enum TypeKind {
17+
None,
18+
Bool,
19+
Address,
20+
Uint256,
21+
Bytes32,
22+
String,
23+
Bytes
24+
}
25+
26+
/// @notice Library for type-safe coercion of the `Variable` struct to concrete types.
27+
///
28+
/// @dev Ensures that when a `Variable` is cast to a concrete Solidity type, the operation is safe and the
29+
/// underlying type matches what is expected.
30+
/// Provides functions to check types, convert them to strings, and coerce `Variable` instances into
31+
/// both single values and arrays of various types.
32+
///
33+
/// Usage example:
34+
/// ```solidity
35+
/// import {LibVariable} from "./LibVariable.sol";
36+
///
37+
/// contract MyContract {
38+
/// using LibVariable for Variable;
39+
/// StdConfig config; // Assume 'config' is an instance of `StdConfig` and has already been loaded.
40+
///
41+
/// function readValues() public {
42+
/// // Retrieve a 'uint256' value from the config.
43+
/// uint256 myNumber = config.get("important_number").toUint();
44+
///
45+
/// // Would revert with `TypeMismatch` as 'important_number' isn't a `uint256` in the config file.
46+
/// // string memory notANumber = config.get("important_number").toString();
47+
///
48+
/// // Retrieve a address array from the config.
49+
/// string[] memory admins = config.get("whitelisted_admins").toAddressArray();
50+
/// }
51+
/// }
52+
/// ```
53+
library LibVariable {
54+
error NotInitialized();
55+
error TypeMismatch(string expected, string actual);
56+
57+
// -- TYPE HELPERS ----------------------------------------------------
58+
59+
/// @notice Compares two Type instances for equality.
60+
function isEqual(Type memory self, Type memory other) internal pure returns (bool) {
61+
return self.kind == other.kind && self.isArray == other.isArray;
62+
}
63+
64+
/// @notice Compares two Type instances for equality. Reverts if they are not equal.
65+
function assertEq(Type memory self, Type memory other) internal pure {
66+
if (!isEqual(self, other)) {
67+
revert TypeMismatch(toString(other), toString(self));
68+
}
69+
}
70+
71+
/// @notice Converts a Type struct to its full string representation (i.e. "uint256[]").
72+
function toString(Type memory self) internal pure returns (string memory) {
73+
string memory tyStr = toString(self.kind);
74+
if (!self.isArray || self.kind == TypeKind.None) {
75+
return tyStr;
76+
} else {
77+
return string(abi.encodePacked(tyStr, "[]"));
78+
}
79+
}
80+
81+
/// @dev Converts a `TypeKind` enum to its base string representation.
82+
function toString(TypeKind self) internal pure returns (string memory) {
83+
if (self == TypeKind.Bool) return "bool";
84+
if (self == TypeKind.Address) return "address";
85+
if (self == TypeKind.Uint256) return "uint256";
86+
if (self == TypeKind.Bytes32) return "bytes32";
87+
if (self == TypeKind.String) return "string";
88+
if (self == TypeKind.Bytes) return "bytes";
89+
return "none";
90+
}
91+
92+
/// @dev Converts a `TypeKind` enum to its base string representation.
93+
function toTomlKey(TypeKind self) internal pure returns (string memory) {
94+
if (self == TypeKind.Bool) return "bool";
95+
if (self == TypeKind.Address) return "address";
96+
if (self == TypeKind.Uint256) return "uint";
97+
if (self == TypeKind.Bytes32) return "bytes32";
98+
if (self == TypeKind.String) return "string";
99+
if (self == TypeKind.Bytes) return "bytes";
100+
return "none";
101+
}
102+
103+
// -- VARIABLE HELPERS ----------------------------------------------------
104+
105+
/// @dev Checks if a `Variable` has been initialized and matches the expected type reverting if not.
106+
modifier check(Variable memory self, Type memory expected) {
107+
assertExists(self);
108+
assertEq(self.ty, expected);
109+
_;
110+
}
111+
112+
/// @dev Checks if a `Variable` has been initialized, reverting if not.
113+
function assertExists(Variable memory self) public pure {
114+
if (self.ty.kind == TypeKind.None) {
115+
revert NotInitialized();
116+
}
117+
}
118+
119+
// -- VARIABLE COERCION FUNCTIONS (SINGLE VALUES) --------------------------
120+
121+
/// @notice Coerces a `Variable` to a `bool` value.
122+
function toBool(Variable memory self) internal pure check(self, Type(TypeKind.Bool, false)) returns (bool) {
123+
return abi.decode(self.data, (bool));
124+
}
125+
126+
/// @notice Coerces a `Variable` to a `uint256` value.
127+
function toUint(Variable memory self) internal pure check(self, Type(TypeKind.Uint256, false)) returns (uint256) {
128+
return abi.decode(self.data, (uint256));
129+
}
130+
131+
/// @notice Coerces a `Variable` to an `address` value.
132+
function toAddress(Variable memory self)
133+
internal
134+
pure
135+
check(self, Type(TypeKind.Address, false))
136+
returns (address)
137+
{
138+
return abi.decode(self.data, (address));
139+
}
140+
141+
/// @notice Coerces a `Variable` to a `bytes32` value.
142+
function toBytes32(Variable memory self)
143+
internal
144+
pure
145+
check(self, Type(TypeKind.Bytes32, false))
146+
returns (bytes32)
147+
{
148+
return abi.decode(self.data, (bytes32));
149+
}
150+
151+
/// @notice Coerces a `Variable` to a `string` value.
152+
function toString(Variable memory self)
153+
internal
154+
pure
155+
check(self, Type(TypeKind.String, false))
156+
returns (string memory)
157+
{
158+
return abi.decode(self.data, (string));
159+
}
160+
161+
/// @notice Coerces a `Variable` to a `bytes` value.
162+
function toBytes(Variable memory self)
163+
internal
164+
pure
165+
check(self, Type(TypeKind.Bytes, false))
166+
returns (bytes memory)
167+
{
168+
return abi.decode(self.data, (bytes));
169+
}
170+
171+
// -- VARIABLE COERCION FUNCTIONS (ARRAYS) ---------------------------------
172+
173+
/// @notice Coerces a `Variable` to a `bool` array.
174+
function toBoolArray(Variable memory self)
175+
internal
176+
pure
177+
check(self, Type(TypeKind.Bool, true))
178+
returns (bool[] memory)
179+
{
180+
return abi.decode(self.data, (bool[]));
181+
}
182+
183+
/// @notice Coerces a `Variable` to a `uint256` array.
184+
function toUintArray(Variable memory self)
185+
internal
186+
pure
187+
check(self, Type(TypeKind.Uint256, true))
188+
returns (uint256[] memory)
189+
{
190+
return abi.decode(self.data, (uint256[]));
191+
}
192+
193+
/// @notice Coerces a `Variable` to an `address` array.
194+
function toAddressArray(Variable memory self)
195+
internal
196+
pure
197+
check(self, Type(TypeKind.Address, true))
198+
returns (address[] memory)
199+
{
200+
return abi.decode(self.data, (address[]));
201+
}
202+
203+
/// @notice Coerces a `Variable` to a `bytes32` array.
204+
function toBytes32Array(Variable memory self)
205+
internal
206+
pure
207+
check(self, Type(TypeKind.Bytes32, true))
208+
returns (bytes32[] memory)
209+
{
210+
return abi.decode(self.data, (bytes32[]));
211+
}
212+
213+
/// @notice Coerces a `Variable` to a `string` array.
214+
function toStringArray(Variable memory self)
215+
internal
216+
pure
217+
check(self, Type(TypeKind.String, true))
218+
returns (string[] memory)
219+
{
220+
return abi.decode(self.data, (string[]));
221+
}
222+
223+
/// @notice Coerces a `Variable` to a `bytes` array.
224+
function toBytesArray(Variable memory self)
225+
internal
226+
pure
227+
check(self, Type(TypeKind.Bytes, true))
228+
returns (bytes[] memory)
229+
{
230+
return abi.decode(self.data, (bytes[]));
231+
}
232+
}

0 commit comments

Comments
 (0)