-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Add mapping slot API to cheatcodes #4710
Conversation
wdyt @mds1 ? |
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.
test and code look great.
only smol nits
issue: more test: |
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.
lgtm, pending @mds1
This is a really interesting idea, thanks @clouds56! Being able to loop over mapping keys is very useful, especially during invariant tests, so people often use arrays to manually track all keys written to a mapping to work around this limitation. I want to think on this API a bit before merging, as I'm not yet convinced it's the best UX. Some scattered initial thoughts for now:
|
I agree with this is not the best UX, but I think it is also important that we could have simple and powerful API in mapping(uint => mapping(uint => uint)) d; how to name Shall we let // d[1][2] = 3
// `read_mapping` would set storage dynamic type to mapping
StdStorage root = stdstore.target(address(test)).sig("d()").read_mapping();
// `getLength` would call `getMappingLength` on correspond slot
for (uint i = 0; i < root.getLength(); i++) {
// `element` would call `getMappingSlotAt`
StdStorage sub = root.element(i).read_mapping();
for (uint j = 0; j < sub.getLength(); j++) {
uint value = root.element(j).read_u256();
}
} inner getMappingLength(address,bytes32)
getMappingSlotAt(address,bytes32,uint256) |
I implemented (uint256 slot, bytes32 key) = stdstore.target(address(test)).sig(test.map_addr.selector).with_key(address(this)).parent();
// or
stdstore.target(address(test)).sig(test.map_addr.selector).with_key(address(this)).root(); to get root slot. |
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.
code lgtm,
pending @mds1
Sorry for the slow response here, busy week. Lots of questions below.
You could do it just like that, that seems like a good convention, i.e.
I do think we should make as much of this native to forge as we can, per #3782, so if we are able to avoid needing a forge-std PR that would be preferable. Rust is a lot more flexible than solidity too, so handling things here seems simpler than having to handle them in solidity. Is there a reason you prefer to split things up into forge-std? Also can you provide some examples of what it would look like to use the full set of cheats + forge-std to get an array of all mapping keys for a regular mapping and nested mapping? Want to make sure I fully understand the UX here, there's a lot of cheats and I'm not sure I fully understand all the nomenclature yet, e.g. parent, root, etc. One thing in particular is, instead of a cheat to return the length, should we additionally have a family of cheats like the below? It feels like that would be better UX since I imagine getting the full array is the most common use case, and of course you can always easily get the length from there.
It's probably keep to keep the length + index versions for better performance in cases where the arrays get really big. Should there be a Lastly, how do the getters behave in terms of their buffers? For example, |
I'm not familiar with UX things, the new adding
could all built on top of current APIs.
I like the idea to get rid of forge-std, while if i read it correctly, the #3782 is just a draft RFC, it even hasn't reached the final design. We could not let existing PR to follow a standard not exists.
I agree with we should add I think the UX of contribution is some how not the best for now? I think we could:
|
As a side note for stdstore: $ forge inspect src/Greeter.sol:Greeter storage --pretty
| Name | Type | Slot | Offset | Bytes | Contract |
|----------|------------------------------------------------|------|--------|-------|-------------------------|
| greeting | string | 0 | 0 | 32 | src/Greeter.sol:Greeter |
| owner | address | 1 | 0 | 20 | src/Greeter.sol:Greeter |
| data | mapping(address => uint256) | 2 | 0 | 32 | src/Greeter.sol:Greeter |
| data2 | mapping(address => mapping(address => int256)) | 3 | 0 | 32 | src/Greeter.sol:Greeter | |
this sounds reasonable to me
defer to @mds1 here |
Ok, I'm good with that. I believe you're right we should be able to build the UX improvements on top of this base, and this is better than the existing solution of having to manually implement/track everything anyway
By default solc/forge does not include the storage layout in the build artifacts. So we'd either have to:
But otherwise you're right that's were we'd get the info from.
This is a good example, thank you.
Here's my suggested plan. First for this PR:
Then create a separate issue to tract UX improvements using mapping names, where we should:
Thanks a lot for this PR! This will be a great improvement, and I appreciate your patience around the UX questions/decisions :) |
i've just stumbled upon this.. absolutely banger feature |
Agreed! @clouds56 any chance you could resolve the conflicts here and maybe we can get this merged this week? |
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.
very nice!
merged thru #5123 |
Motivation
When testing a contract, we have no knowledge about how many elements there're in a
mapping
, so we'd like to add a family of cheatcode to solve this problem.Solution
Cheatcodes
startMappingRecording()
we record all potential mapsstore
to Cheatcodes local stategetMappingLength(address target, bytes32 mappingSlot) returns (uint256)
would get count of elements inmappingSlot
attarget
address.getMappingSlotAt(address target, bytes32 mappingSlot, uint256 idx) returns (bytes)
would get elements ofidx
in the mapping0 <= idx < length
, thelength
is returned bygetMappingLength
getMappingKeyOf(address target, bytes32 elementSlot) returns (bytes)
andgetMappingParentOf(address target, bytes32 elementSlot) returns (bytes)
are called on "element slot" returned bygetMappingSlotAt
Details
sha3
andsstore
on stepsstore
, we would first check if it is generated by somesha3
. If so, this would probable be a element of some mapping.