-
Notifications
You must be signed in to change notification settings - Fork 6k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Type checking for contract explicit storage base location #15528
base: storageLocationsParserSupport
Are you sure you want to change the base?
Type checking for contract explicit storage base location #15528
Conversation
matheusaaguiar
commented
Oct 21, 2024
- depends on Parser support for explicit storage locations #15463.
bf640be
to
666b499
Compare
d3cd13a
to
5645314
Compare
fb75168
to
2579486
Compare
038b819
to
7f4eb69
Compare
07f289c
to
6b34e90
Compare
82b5fec
to
54451ed
Compare
6b34e90
to
f0f7997
Compare
ff0c30b
to
f7f90be
Compare
f0f7997
to
2839eed
Compare
f7f90be
to
7473a01
Compare
7a42733
to
c2d5d9a
Compare
6257c02
to
9acfb2d
Compare
68f5886
to
abca45e
Compare
7a1d6b2
to
6f29b3c
Compare
e4e8e8e
to
32daf59
Compare
e409300
to
7c76c68
Compare
32daf59
to
24a63fc
Compare
7c76c68
to
a3e17ed
Compare
24a63fc
to
f4ddc29
Compare
a3e17ed
to
3396801
Compare
Co-authored-by: Kamil Śliwak <kamil.sliwak@codepoets.it>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two main issues: layout on interfaces is not disallowed and also the issue unnecessarily restricts that a contract can only be inherited from once when there's a layout.
I also think that it would be good to move checks to parts of analysis other than TypeChecker
. It seems perfectly feasible for this particular feature and TypeChecker
is already a beast.
Other than that the usual stuff. Especially tests.
struct StorageLayoutSpecifierAnnotation: ASTAnnotation | ||
{ | ||
// The evaluated value of the expression specifying the contract storage layout base | ||
solidity::u256 value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a pretty bad name :)
Also we're inside the solidity
namespace here.
solidity::u256 value; | |
u256 baseSlot; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, you should make it SetOnce
.
@@ -234,6 +236,99 @@ bool TypeChecker::visit(ImportDirective const&) | |||
return false; | |||
} | |||
|
|||
void TypeChecker::endVisit(ContractDefinition const& _contract) | |||
{ | |||
if ((_contract.isLibrary() || _contract.abstract()) && _contract.storageLayoutSpecifier()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about interfaces?
contract A {} | ||
contract B is A layout at 64 {} | ||
contract C is A layout at 42 {} | ||
// ---- | ||
// TypeError 2031: (46-77): Conflicting storage layout specifications:Storage layout for base contract 'A' was also specified by another contract which derives from it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a perfectly valid case.
The derivedContractSpecifyingStorageLayout
annotation seems to have been added just to check for it. You don't need it or the check. A contract that's not at the top-level of the hierarchy can be a part of multiple layouts.
m_errorReporter.typeError( | ||
1139_error, | ||
baseLocation->location(), | ||
"The storage layout must be specified by an expression that can be evaluated at compilation time." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"The storage layout must be specified by an expression that can be evaluated at compilation time." | |
"The base slot of the storage layout must be a compile-time constant expression." |
auto const* expressionType = type(*baseLocation); | ||
BoolResult result = expressionType->isImplicitlyConvertibleTo(*TypeProvider::uint256()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
auto const* expressionType = type(*baseLocation); | |
BoolResult result = expressionType->isImplicitlyConvertibleTo(*TypeProvider::uint256()); | |
auto const* baseSlotExpressionType = type(*baseLocation); | |
BoolResult convertibleToUint = expressionType->isImplicitlyConvertibleTo(*TypeProvider::uint256()); |
return; | ||
} | ||
|
||
if (auto const* rationalType = dynamic_cast<RationalNumberType const*>(expressionType)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO you should check this first, then fractionals, then range. Finally just assert that the value is convertible to uint
and evaluate the value. This will make the message in most weird cases very clear (it's not a number literal). Currently you get a message about exceeding uint
range in many weird cases, which suboptimal.
Later, when we open the functionality up to non-literals, we'll replace the assert with a message saying that type X is not implicitly convertible, which should also be clear.
if (auto const* rationalType = dynamic_cast<RationalNumberType const*>(expressionType)) | ||
{ | ||
solAssert(!rationalType->isFractional()); | ||
_storageLayoutSpecifier.annotation().value = u256(rationalType->value().numerator()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that u256()
conversion just truncates and does not throw when the value is out of range. You only indirectly check that it's in range, so you should also assert that it really is.
Also assert that denominator is in fact 1.
{ | ||
ASTPointer<Expression> const baseLocation = _storageLayoutSpecifier.expression(); | ||
|
||
if (!*baseLocation->annotation().isPure) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These checks would probably be better off in PostTypeChecker
/PostTypeContractLevelChecker
. TypeChecker
is a beast already (4k+ lines) and we seem to be dumping everything in it :)
The checks and the annotation do not seem to be things that other checks will need to depend on so it should be fairly straightforward to move them. To the contrary - they use isPure
annotation, which is fully filled out only after TypeChecker
. We're in endVisit()
so it should already be set on the expression, but still, it would be cleaner if all of it was not crammed into the same analysis pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, verifying that base contracts do not have layout specifiers seems like something that can be checked already by ContractLevelChecker
and does not have to wait until we know the types.
contract A { } | ||
contract B { } | ||
contract Z { } | ||
contract C is A, B layout at 0x1234 { } | ||
contract D layout at 0xABCD is A, B { } | ||
contract D layout at 0xABCD is Z { } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be changed back.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs more tests. There's quite a few, but note that many of these will be simple one-liners and you can usually drop everything from one bullet point into one file because errors are not fatal.
The most necessary minimum:
- Numbers with denominations (
42 weeks
,666 gwei
) - Rational numbers with zero fractional part (
42.0
,2.5e10
) - Interface with a layout
layout at 1 / 0
layout at -1
Scoping
- Constants defined in inherited contracts
- State variables defined in inherited contracts
- Things defined in foreign contracts
Inheritance
- Contract with a layout inheriting from an abstract contract/interface
- Contract with a layout inheriting from itself
- Inheriting from multiple contracts where first/last already has layout
Things that are now rejected but will have to work eventually so we may as well cover them already:
type(uint).max
uint(42)
uint40(bytes5(hex"0011223344"))
~uint(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
[1, 2, 3][0]
string("ABCD").length
abi.decode(abi.encode(42), (uint))
uint(bytes32(type(I).interfaceId))
uint32(f.selector)
uint64(bytes8(bytes.concat("ABCD", "EFGH")))
uint(keccak256(bytes.concat("ABCD"))
uint8(b[1])
, whereb
isbytes32 constant b
N / ~N
whereN
is a constant.- Ternary operator
T.unwrap()
Wild things that should not work:
N / 0
whereN
is a constant- Expressions that would overflow in checked arithmetic
- Expressions using custom operators defined on
uint
- Various non-
uint
literals: address, hex string, string, boolean, enum,[1, 2, 3]
,(1, 2, 3)
++2
,1 = 2
,delete 2
T.wrap()
f.selector
,f.address
- Builtins that depend on state:
blockhash()
,block.chainid
,block.number
,msg.value
,tx.gasprice
,<address>.balance
, etc. Especially those that returnuint
. - Precompiles returning
uint
:addmod()
etc. - Bound pure library function called on a literal (
1.f()
) - Weird stuff: error, event, module, contract, type, magic, etc.
address(new C())
uint160(address(this))