Description
Abstract
Add native support for dynamic immutable
arrays, with a length defined at construction time (up to a maximum) and automatic bounds checks.
Motivation
immutable
is a fantastic feature, allowing for highly efficient configuration access without forcing developers to use hardcoded constants, making testing and deployment much easier and enjoyable. However, there is zero support for any kind of arrays.
What I describe can be manually implemented today in solc >0.7, but it leads to a very large amount of error-prone boilerplate. No alternative exists for gas-efficient access to these values. You can look at production code using this pattern here. Using the proposed features in such a contract would reduce sour code size to less than a third of the original length, while also increasing auditability and maintainability.
Specification
'Dynamic' may be a confusing term. What I mean is that the contents and length would be dynamically defined at construction time. Read access with bounds check using the []
operator and .length
would both be supported. This feature set is essentially the same as that of memory arrays, except memory arrays can have their contents mutated.
Truly dynamic immutable arrays would be quite an undertaking as the length of the runtime code would be dependent on construction arguments, but I'd argue that going that far is unnecessary. In all applications I've seen, there is a compile-time known maximum array length.
The proposed feature could therefore work as follows:
- the user declares an immutable array with a maximum length N (e.g.
uint256[] private immutable(50) foo
) - the compiler allocates runtime code equivalent to N+1 immutable values, for each element plus the total length
- during construction, both
foo.length
andfoo[i]
can be used to write (not read) to the array. For simplicity, we could allow for each value to be written multiple times and perform no bounds check (other than.length <= N
andi < N
). - during runtime,
foo.length
returns the array's length andfoo[i]
performs bounds check withfoo.length
.
Backwards Compatibility
This is a new feature using brand new (as of today invalid) syntax, so there should be no backwards compatibility implications.
Extra
Code samples
As mentioned above, this can be implemented today using standard Solidity. See for example how one might declare and use an up to 10 tokens array:
uint256 private immutable _totalTokens;
IERC20 internal immutable _token0;
IERC20 internal immutable _token1;
IERC20 internal immutable _token2;
IERC20 internal immutable _token3;
IERC20 internal immutable _token4;
IERC20 internal immutable _token5;
IERC20 internal immutable _token6;
IERC20 internal immutable _token7;
IERC20 internal immutable _token8;
IERC20 internal immutable _token9;
constructor(IERC20[] memory tokens) {
uint256 numTokens = tokens.length;
require(numTokens < 10);
_totalTokens = numTokens;
_token0 = numTokens > 0 ? tokens[0] : IERC20(0);
_token1 = numTokens > 1 ? tokens[1] : IERC20(0);
_token2 = numTokens > 2 ? tokens[2] : IERC20(0);
_token3 = numTokens > 3 ? tokens[3] : IERC20(0);
_token4 = numTokens > 4 ? tokens[4] : IERC20(0);
_token5 = numTokens > 5 ? tokens[5] : IERC20(0);
_token6 = numTokens > 6 ? tokens[6] : IERC20(0);
_token7 = numTokens > 7 ? tokens[7] : IERC20(0);
_token8 = numTokens > 8 ? tokens[8] : IERC20(0);
_token9 = numTokens > 9 ? tokens[9] : IERC20(0);
}
function getTokenIndex(IERC20 token) public view returns (uint256) {
if (token == _token0) { return 0; }
else if (token == _token1) { return 1; }
else if (token == _token2) { return 2; }
else if (token == _token3) { return 3; }
else if (token == _token4) { return 4; }
else if (token == _token5) { return 5; }
else if (token == _token6) { return 6; }
else if (token == _token7) { return 7; }
else if (token == _token8) { return 8; }
else if (token == _token9) { return 9; }
else {
revert("Invalid token");
}
}
This is of course very error-prone and hard to maintain, but it is the best way to get the desired behavior. With the suggested feature, we get the exact same bytecode size and performance using the following code:
IERC20[] internal immutable(10) _tokens;
constructor(IERC20[] memory tokens) {
_tokens.length = tokens.length;
for (uint256 i = 0; i < tokens.length; ++i) {
_tokens[i] = tokens[i];
}
}
function getTokenIndex(IERC20 token) public view returns (uint256) {
for (uint256 i = 0; i < _tokens.length; ++i) {
if (_tokens[i] == token) {
return i;
}
}
revert("invalid token");
}