-
Notifications
You must be signed in to change notification settings - Fork 6.1k
ABIEncoderV2: Implement calldata structs without dynamically encoded members. #5936
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
Conversation
Codecov Report
@@ Coverage Diff @@
## develop #5936 +/- ##
==========================================
Coverage ? 88.37%
==========================================
Files ? 359
Lines ? 34309
Branches ? 4064
==========================================
Hits ? 30320
Misses ? 2616
Partials ? 1373
|
libsolidity/ast/Types.h
Outdated
@@ -827,6 +827,8 @@ class StructType: public ReferenceType | |||
std::string richIdentifier() const override; | |||
bool operator==(Type const& _other) const override; | |||
unsigned calldataEncodedSize(bool _padded) const override; | |||
// Offset of member in calldata. Does not work for recursive or dynamically encoded types. | |||
unsigned calldataOffsetOfMember(std::string const& _name) const; |
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.
Wouldn't this better fit closer to memoryOffsetOfMember
and storageOffsetOfMember
?
{ | ||
if (structType->dataStoredIn(DataLocation::CallData)) | ||
{ | ||
solAssert(!_fromMemory, ""); |
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.
Can it happen that dataStoredIn(DataLocation::Memory)
is true, but _fromMemory
is false?
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.
I think so - if we want to decode something in calldata into memory, I think.
m_context << Instruction::DUP1; | ||
m_context << typeOnStack.calldataEncodedSize(true); | ||
m_context << Instruction::ADD; | ||
abiDecode({targetType.shared_from_this()}, false); |
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.
Please check that this properly allocates memory.
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.
Ah, but this might need calldata size checks, at least for dynamically-sized structs.
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.
Yep - adjusted this (and the same for the other nested abiDecode call) in a new commit - should be cheaper with CALLDATASIZE
and SUB
anyways.
{ | ||
solUnimplementedAssert(!type.isDynamicallyEncoded(), ""); | ||
m_context << type.calldataOffsetOfMember(member) << Instruction::ADD; | ||
if (_memberAccess.annotation().type->isValueType()) |
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.
Does this work for arrays?
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.
Please also add explicit assertions for every non-value type this is designed to work with.
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.
For every non-value type it should be fine to just return the calldata offset - I think the TypeChecker shouldn't allow anything that will actually try to further decode, if this is an array and if so the the actual decoding of the calldata array should have the unimplemented assertion.
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.
Still better to assert all types we know are working, so that it automatically breaks if we introduce new types.
d25da98
to
3c7816d
Compare
m_context << Instruction::DUP1; | ||
m_context << type.calldataEncodedSize(true); | ||
m_context << Instruction::ADD; | ||
CompilerUtils(m_context).abiDecode({_memberAccess.annotation().type}, false); |
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.
For uint256, this will turn into an assembly function call where the function only consists of a single calldataload
, which cannot be inlined by the current optimizer. This is quite inefficient. Should we make a small exception for types where storageBytes is 32 and call loadFromMemoryDynamic
as above? Actually loadFromMemoryDynamic
also performs range checks. So another solution would be to update CompilerUtils::convertType(
together with #5815
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.
storageBytes is probably right, but feels a bit off... calldataEncodedSize(false)
should do it as well, shouldn't it? And I'm wondering whether this actually needs to range-check that we're inside calldatasize or whether we can just use calldataload
directly if calldataEncodedSize(false)==32
... probably enough for the statically encoded case, right?
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.
We do not need any calldatasize-range checks here for any value type. Using calldataEncodedSize is better, yes. And you can just use calldataload
, yes :)
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.
Or... we could just always call CompilerUtils(m_context).loadFromMemoryDynamic(*_memberAccess.annotation().type, true, false, false)
here, that should do it and degenerate to a single calldataload
whenever possible - I guess that's what you meant with updating CompilerUtils::convertType(
together with #5815, so that (at least in this case) loadFromMemoryDynamic
will validate instead of clean?
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.
Yes, exactly.
Are calldata size checks properly enforced here? I think you already answered that question when I meant something else ;) |
@chriseth Actually I thought they were, but now I'm not sure anymore, I'll recheck and make sure! |
If we limit the scope of this PR to statically encoded structs, the only things left to do in my opinion are:
|
if (_memberAccess.annotation().type->isValueType()) | ||
{ | ||
solAssert(_memberAccess.annotation().type->calldataEncodedSize(false) > 0, ""); | ||
CompilerUtils(m_context).loadFromMemoryDynamic(*_memberAccess.annotation().type, true, false, false); |
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.
CompilerUtils(m_context).loadFromMemoryDynamic(*_memberAccess.annotation().type, true, false, false); | |
CompilerUtils(m_context).loadFromMemoryDynamic(*_memberAccess.annotation().type, true, true, false); |
(struct members are always padded)
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.
I don't think so - this will end up in CompilerUtils.cpp#L1261
and will make sure everything with calldataEncodedSize(false) != 32
will be cleaned (or at some point validated I guess).
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.
I was in fact wrong, it will trigger the shifting in loadFromMemoryHelper
- which we may in fact not want here. I'll check and recheck with some more tests, but I think you're right in that it should be true
here.
CompilerUtils(m_context).loadFromMemoryDynamic(*_memberAccess.annotation().type, true, false, false); | ||
} | ||
else | ||
solUnimplementedAssert( |
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.
solUnimplementedAssert( | |
solAssert( |
Are you planning on implemented anything else apart from arrays and structs? :)
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 can only happen for something that doesn't exist yet anyways and if we add something that could fail here, then handling that is unimplemented, right :-)? At least that was the idea - I can change to an assert, though, I don't mind either :-).
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 tests for calldata structs with short calldata and dirty higher order bits.
b92b120
to
b2169e2
Compare
// double check that the valid case goes through | ||
ABI_CHECK(callContractFunction("f((uint256,uint256))", u256(1), u256(2)), encodeArgs(0x44)); | ||
|
||
ABI_CHECK(callContractFunctionNoEncoding("f((uint256,uint256))", bytes(63,0)), encodeArgs()); |
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.
@chriseth Sorry, I force pushed your comment away - but this is missing one byte, that's why it should revert.
Looks good, please squash! |
b2169e2
to
ed256af
Compare
Still needs more tests, I think. |
Hm, perhaps a struct containing an external function pointer - those are always a little nasty. |
Also |
ed256af
to
c5d6a86
Compare
6b04f98
to
962fd4c
Compare
Added another test case for function pointers. I think it makes more sense to keep |
962fd4c
to
0e4912a
Compare
|
Part of #1603.