Events

Logs and Filtering

Logs and filtering are used quite often in blockchain applications, since they allow for efficient queries of indexed data and provide lower-cost data storage when the data is not required to be accessed on-chain.

These can be used in conjunction with the Provider Events API and with the Contract Events API.

The Contract Events API also provides higher-level methods to compute and query this data, which should be preferred over the lower-level filter.

Filters

When a Contract creates a log, it can include up to 4 pieces of data to be indexed by. The indexed data is hashed and included in a Bloom Filter, which is a data structure that allows for efficient filtering.

So, a filter may correspondingly have up to 4 topic-sets, where each topic-set refers to a condition that must match the indexed log topic in that position (i.e. each condition is AND-ed together).

If a topic-set is null, a log topic in that position is not filtered at all and any value matches.

If a topic-set is a single topic, a log topic in that position must match that topic.

If a topic-set is an array of topics, a log topic in that position must match any one of the topics (i.e. the topic in this position are OR-ed).

This may sound complicated at first, but is more easily understood with some examples.

Topic-SetsMatching Logs 
[ A ]topic[0] = A 
[ A, null ] 
[ null, B ]topic[1] = B 
[ null, [ B ] ] 
[ null, [ B ], null ] 
[ A, B ](topic[0] = A) AND (topic[1] = B) 
[ A, [ B ] ] 
[ A, [ B ], null ] 
[ [ A, B ] ](topic[0] = A) OR (topic[0] = B) 
[ [ A, B ], null ] 
[ [ A, B ], [ C, D ] ][ (topic[0] = A) OR (topic[0] = B) ] AND [ (topic[1] = C) OR (topic[1] = D) ] 
Example Log Matching 
ERC-20 Transfer Filter Examples
// Short example of manually creating filters for an ERC-20 // Transfer event. // // Most users should generally use the Contract API to // compute filters, as it is much simpler, but this is // provided as an illustration for those curious. See // below for examples of the equivalent Contract API. // ERC-20: // Transfer(address indexed src, address indexed dst, uint val) // // -------------------^ // ----------------------------------------^ // // Notice that only *src* and *dst* are *indexed*, so ONLY they // qualify for filtering. // // Also, note that in Solidity an Event uses the first topic to // identify the Event name; for Transfer this will be: // id("Transfer(address,address,uint256)") // // Other Notes: // - A topic must be 32 bytes; so shorter types must be padded // List all token transfers *from* myAddress filter = { address: tokenAddress, topics: [ id("Transfer(address,address,uint256)"), hexZeroPad(myAddress, 32) ] }; // List all token transfers *to* myAddress: filter = { address: tokenAddress, topics: [ id("Transfer(address,address,uint256)"), null, hexZeroPad(myAddress, 32) ] }; // List all token transfers *to* myAddress or myOtherAddress: filter = { address: tokenAddress, topics: [ id("Transfer(address,address,uint256)"), null, [ hexZeroPad(myAddress, 32), hexZeroPad(myOtherAddress, 32), ] ] };

To simplify life, ..., explain here, the contract API

ERC-20 Contract Filter Examples
abi = [ "event Transfer(address indexed src, address indexed dst, uint val)" ]; contract = new Contract(tokenAddress, abi, provider); // List all token transfers *from* myAddress contract.filters.Transfer(myAddress) // { // address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // topics: [ // '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', // '0x0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba72' // ] // } // List all token transfers *to* myAddress: contract.filters.Transfer(null, myAddress) // { // address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // topics: [ // '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', // null, // '0x0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba72' // ] // } // List all token transfers *from* myAddress *to* otherAddress: contract.filters.Transfer(myAddress, otherAddress) // { // address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // topics: [ // '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', // '0x0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba72', // '0x000000000000000000000000ea517d5a070e6705cc5467858681ed953d285eb9' // ] // } // List all token transfers *to* myAddress OR otherAddress: contract.filters.Transfer(null, [ myAddress, otherAddress ]) // { // address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // topics: [ // '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', // null, // [ // '0x0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba72', // '0x000000000000000000000000ea517d5a070e6705cc5467858681ed953d285eb9' // ] // ] // }

Solidity Topics

This is a quick (and non-comprehensive) overview of how events are computed in Solidity.

This is likely out of the scope for most developers, but may be interesting to those who want to learn a bit more about the underlying technology.

Solidity provides two types of events, anonymous and non-anonymous. The default is non-anonymous, and most developers will not need to worry about anonymous events.

For non-anonymous events, up to 3 topics may be indexed (instead of 4), since the first topic is reserved to specify the event signature. This allows non-anonymous events to always be filtered by their event signature.

This topic hash is always in the first slot of the indexed data, and is computed by normalizing the Event signature and taking the keccak256 hash of it.

For anonymous events, up to 4 topics may be indexed, and there is no signature topic hash, so the events cannot be filtered by the event signature.

Each additional indexed property is processed depending on whether its length is fixed or dynamic.

For fixed length types (e.g. uint, bytes5), all of which are internally exactly 32 bytes (shorter types are padded with zeros; numeric values are padded on the left, data values padded on the right), these are included directly by their actual value, 32 bytes of data.

For dynamic types (e.g. string, uint256[]) , the value is hashed using keccak256 and this hash is used.

Because dynamic types are hashed, there are important consequences in parsing events that should be kept in mind. Mainly that the original value is lost in the event. So, it is possible to tell is a topic is equal to a given string, but if they do not match, there is no way to determine what the value was.

If a developer requires that a string value is required to be both able to be filtered and also able to be read, the value must be included in the signature twice, once indexed and once non-indexed (e.g. someEvent(string indexed searchBy, string clearText)).

For a more detailed description, please refer to the Solidity Event Documentation.

Other Things? TODO

Explain what happens to strings and bytes, how to filter and retain the value