foundry
foundry copied to clipboard
feat(cast): cast events
This is an early draft PR, but I'm new to Rust & the Foundry codebase, so wanted to post here for feedback and a few questions before going too far.
Motivation
Motivated by #2340:
I'm able to call cast receipt [txHash] logs to get the full logs of a transaction, but it's time consuming to manually parse the logs into legible events.
Seems helpful to be able to call cast events [txHash] to get a nicely formatted view of the events.
I'd love to build this out, as soon as we have agreement on the plan below.
Edited Files
- In
cli/src/opts/cast.rs
, added Events subcommand that takes in a tx hash (& other optional params) - In
cli/src/cast.rs
, set up rpc and etherscan api based on optional inputs or config, call out to core logic, and print result - In
cast/lib.rs
, added the logic for decoding the events, which returns a Result<String> with the formatted event information
Core Logic
-
Grab the receipt for the tx using existing provider.receipt() method and pull logs
-
For each event in the logs, try to pull contract from etherscan.
- If it's there, compare topic 0 to events in the abi to find the correct event
- Cache the abi for the rest of the function execution for faster retrieval (assuming multiple events likely to come from the same contract)
- Backup: If it's not verified on etherscan, run topic 0 against 4byte. If it's also not on 4byte, continue to parse event data without interpreting it.
-
As we parse event, create an instance of the
EventData
struct that holds the address, event signature, and a vector of args, which each contain the argument name, type, indexed bool, and empty fields for their value. -
Zip the topics and data in with these args to associate the proper event data with the tx data, and push it all into one string to return.
Sample Transactions
If you want to play around with it, here are a few sample transactions that contain both verified contract, events on 4byte, and events without any decoding.
- Verified: 0x6070c135759361e9c3b87e18aeb71c4b6a50167207567d9efa97f11e6a222881
- 4byte & Unknown: 0xf8d8da1987c9a444c727350b55be13e95b6d150ca18e88a69bac1cb20c116b0a
Questions
A few things I'd love to get clear on to make sure this is up to par.
-
I have a number of helper functions (and structs, see (2) below) that are needed for decoding the events. I currently just left them all sitting at the bottom of
cast/src/lib.rs
but assume there's a better home for them. Where do you think they should live? -
I could have sworn there were existing types for Events that I could be using for all this, but I can't find them anywhere. If they exist, can you point me in the right direction. If not, can you let me know the best place to move those structs so they're accessible for other features?
-
4byte doesn't capture whether a field is indexed or not, so I'm needing to merge all those topics in with data and treat them as unindexed. Do you see any better ways I might handle that?
-
Are there any existing utils I rebuilt? I went through and tried to avoid it, but big codebase to get my head around so there may be things I missed.
-
I feel like there are a few things I'm handling a lot more sloppily than I could be. New to Rust, so would love any feedback that will make this as clean as possible.
Remaining To Dos
Once that's done, here are the remaining things I'd like to do before this is ready to merge:
- [ ] Add additional types (currently only supports addresses and uint256s)
- [ ] For proxy contracts, grab events from implementation source code
- [ ] Move helper functions and structs into the correct place in the code base (and use existing structs if they exist)
- [ ] Add tests
That should be good to get this ready to merge. After that, I'll start working on a next iteration that allows events to be pulled by contract, block range, topic based filters, etc.
4byte doesn't capture whether a field is indexed or not, so I'm needing to merge all those topics in with data and treat them as unindexed. Do you see any better ways I might handle that?
Yea this is a problem with EVM/events in general, since topic0 is independent of indexed events, and you can't figure out which params were the indexed ones. If there's any ambiguity we'll have to leave it undecoded for now.
Maybe it's possible we can get an update to https://sig.eth.samczsun.com/ such that events return which fields are indexed?
Thanks @onbjerg and @mds1. I'm needing to get much better acquainted with ethers-rs and it's taking a minute, but have the basics figured out and should have fixed version early next week.
Thanks @onbjerg for pointing me in the right direction on this. I think the latest commit should be a lot closer.
Utils
I was able to get rid of all the utility functions by using the proper types, with two exceptions (both currently in cast/src/lib.rs
):
-
translate_event_string_to_event_type()
: I don't see this functionality in the Event type in ethers-rs, but seems like it'd be valuable for it to be there. The idea is to turn the Solidity declaration (ie.event Transfer(address indexed from, address indexed to, uint256 value);
) into an Event type. We could implement this as a helper here, or submit PR straight to ethers to add this to the Event struct. -
convert_string_to_param_type()
: The above function requires a way to turn the text of a type ("address") into the ParamType, so this is a little helper that does that. Should go wherever the above function goes.
Updated Core Logic
- Grab the event declarations from the contract in Etherscan
- Turn each of these lines into an Event type
- Check the signature of these types vs topic 0, and, if there is an Event that matches, save it
- If there is a matching event, use parse_log() to combine this Event with the receipt log to get all the event data, and format it to return
- If there isn't, format it in a more generic way and return that
Trade Offs
The only downside is that we can no longer use 4byte as a backup, because without knowing whether an event argument is indexed, we aren't able to use parse_logs()
. I think that's a worthwhile trade off.
Sample Transactions
If you want to play around with it, here are a few sample transactions that contain both verified contract, events on 4byte, and events without any decoding.
- Verified: 0x6070c135759361e9c3b87e18aeb71c4b6a50167207567d9efa97f11e6a222881
- Unknown: 0xf8d8da1987c9a444c727350b55be13e95b6d150ca18e88a69bac1cb20c116b0a
Remaining To Dos
- [x] Add additional types (only supports value types at the moment)
- [ ] For proxy contracts, grab events from implementation source code
- [x] Decide whether Utils above should be in this code or in ethers-rs and adjust
- [ ] Any other feedback you have (I'm new to Rust, so would love any advice on improving code quality)
- [ ] Add tests
Thanks again for the quick feedback last time. Looking forward to getting this finished and shipped.
Commit from yesterday used proper types for the core logic, but missed using tx receipt to simplify.
Just implemented that and cleaned it up a lot, added comments, etc. Ready for review now.
4byte doesn't capture whether a field is indexed or not, so I'm needing to merge all those topics in with data and treat them as unindexed. Do you see any better ways I might handle that?
Yea this is a problem with EVM/events in general, since topic0 is independent of indexed events, and you can't figure out which params were the indexed ones. If there's any ambiguity we'll have to leave it undecoded for now.
Maybe it's possible we can get an update to https://sig.eth.samczsun.com/ such that events return which fields are indexed?
sourceify has this information for you already FYI
Regarding your tradeoff, we handle that on traces by getting the signature from sam's db and use the following function: https://github.com/foundry-rs/foundry/blob/be07dda496604d53493ec4ae9cd7b553a2d70a61/utils/src/lib.rs#L290-L292
Regarding your tradeoff, we handle that on traces by getting the signature from sam's db and use the following function:
https://github.com/foundry-rs/foundry/blob/be07dda496604d53493ec4ae9cd7b553a2d70a61/utils/src/lib.rs#L290-L292
@joshieDo Interesting. That logic seems to be that if all args are indexed, it adds it. Or if the number indexed is the same as addresses, it assumes addresses are indexed. Otherwise, it gives up.
That seems fine for traces where a "best guess" is fine, but for this function, it seems to me that making incorrect guesses isn't a worthwhile trade off to enable 4byte. Maybe I'll just implement something similar so that if all args are indexed it works, since that's not a guess, but skips it otherwise?
@onbjerg @joshieDo As it currently works, if the transaction is executed with a proxy contract, the ABI from Etherscan is that of the proxy so it doesn't find the matching event.
Do you have any ideas on how I could resolve that? The best I can come up with is that if the ABI doesn't match, I check storage at bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
to see if there's a contract listed there, and rerun the same logic on that contract.
I believe that's what Etherscan does, and seems like the best bet. Any other ideas?
I'd say first try looking for an implementation()
method and if it exists, use what it returns. If that method doesn't exist,then fall back to the EIP-1967 slot that you mentioned. This is because many proxies (like Compound) predate or just don't follow EIP-1967, and in that case there's usually an implementation()
method
Closing as stale.