Events are an essential part of the Cosmos SDK. They are similar to "logs" in Ethereum and allow a blockchain app to attach key-value pairs to a transaction that can later be used to search for it or extract some information in human readable form. Events are not written to the application state, nor do they form part of the AppHash, but mainly intended for client use (and become an essential API for any reactive app or app that searches for txs).
In contrast, transactions also have a binary "data" field that is part of the AppHash (provable with light client proofs, part of consensus). This data is not searchable, but given a tx hash, you can be guaranteed what the data returned is. This is often empty, but sometimes custom protobuf formats to return essential information from an execution.
Every message in the SDK may add events to the EventManager and these are then added to the final ABCI result that is returned
to Tendermint. Events are exposed in 3 different ways over the Tendermint API (which is the only way a client can query).
First of all is the events
field on the transaction result (when you query a transaction by hash, you can see all event emitted
by it). Secondly is the log
field on the same transaction result. And third is the query interface to search or subscribe for
transactions.
The log
field actually has the best data. It contains an array of array of events. The first array is one entry per incoming message.
Transactions in the Cosmos SDK may consist of multiple messages that are executed atomically. Maybe we send tokens, then issue a swap
on a DEX. Each action would return it's own list of Events and in the logs, these are separated. For each message, it maintains a list
of Events, exactly in the order returned by the application. This is JSON encoded and can be parsed by a client. In fact this is
how CosmJS gets the events it shows to the client.
In Tendermint 0.35, the events
field will be one flattened list of events over all messages. Just as if we concatenated all
the per-message arrays contained in the log
field. This fix was made as
part of an event system refactoring. This refactoring is also giving us
pluggable event indexing engines, so we can use eg. PostgreSQL to
store and query the events with more powerful indexes.
However, currently (until Tendermint 0.34 used in Cosmos SDK 0.40-0.43), all events of one transaction are "flat-mapped" on type.
Meaning all events with type wasm
get merged into one. This makes the API not very useful to understanding more complex events
currently. There are also a number of limitations of the power of queries in the search interface.
Given the state of affairs, and given that we seek to provide a stable API for contracts looking into the future, we consider the
log
output and the Tendermint 0.35 event handling to be the standard that clients should adhere to. And we will expose a similar
API to the smart contracts internally (all events from the message appended, unmerged).
The event has a string type, and a list of attributes. Each of them being a key value pair. All of these maintain a consistent order (and avoid dictionaries/hashes). Here is a simple Event in JSON:
{
"type": "wasm",
"attributes": [
{"key": "_contract_address", "value": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6"},
{"key": "transfered", "value": "777000"}
]
}
And here is a sample log output for a transaction with one message, which emitted 2 events:
[
[
{
"type": "message",
"attributes": [
{"key": "module", "value": "bank"},
{"key": "action", "value": "send"}
]
},
{
"type": "transfer",
"attributes": [
{"key": "recipient", "value": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6"},
{"key": "amount", "value": "777000uatom"}
]
}
]
]
There are two places events that are emitted in every transaction regardless of the module which is executed.
The first is {"type": "message"}
defining an action
attribute. This is emitted for each top-level (user-signed) message, but the action names have changed between
0.42 and 0.43.
The other place is in the signature verification AnteHandler, where it emits information on the account sequences and signatures on the transaction.
These are all handled in BaseApp and the middleware before any module is called and thus not exposed to CosmWasm contracts at all.
The events that will actually make it to the contracts are the events that are emitted by the other modules / keepers. Let's look at some good examples of what they look like:
The most basic one is bank
, which emits two events on every send, a custom "transfer" event as well as "sender" information under the standard "message" type. Replacing variables with string literals, they look like this:
sdk.NewEvent(
"transfer"
sdk.NewAttribute("recipient", toAddr.String()),
sdk.NewAttribute("sender", fromAddr.String()),
sdk.NewAttribute("amount", amt.String()), // eg 12456uatom
),
sdk.NewEvent(
"message",
sdk.NewAttribute("sender", fromAddr.String()),
),
The delegation module seems a bit more refined, emitting a generic "message" type event in msg_server.go
including the module name, before
emitting some custom event types closer to the actual code logic in
keeper.go
.
This looks something like:
sdk.NewEvent(
"message",
sdk.NewAttribute("module", "distribution"),
sdk.NewAttribute("sender", msg.DelegatorAddress),
),
sdk.NewEvent(
"set_withdraw_address",
sdk.NewAttribute("withdraw_address", withdrawAddr.String()),
),
In x/wasm
we also use Events system. On one hand, the Go implementation of x/wasm
emits standard events for each
message it processes, using the distribution
module as an example. Furthermore, it allows contracts to
emit custom events based on their execution state, so they can for example say "dex swap, BTC-ATOM, in 0.23, out 512"
which require internal knowledge of the contract and is very useful for custom dApp UIs.
x/wasm
is also a consumer of events, since when a smart contract executes a SubMsg and processes the reply, it receives
not only the data
response from the message execution, but also the list of events. This makes it even more important for
us to document a standard event processing format.
Following the model of distribution
, we will split the emitted events into two parts. All calls to the message server, will receive
the following event:
sdk.NewEvent(
"message",
sdk.NewAttribute("module", "wasm"),
// Note: this was "signer" before 0.18
sdk.NewAttribute("sender", msg.Sender),
),
No further information will be added to the generic "message" type, but rather be contained in a more context-specific event type. Here are some examples:
// Store Code
sdk.NewEvent(
"store_code",
sdk.NewAttribute("code_id", fmt.Sprintf("%d", codeID)),
// features required by the contract (new in 0.18)
// see https://github.com/CosmWasm/wasmd/issues/574
sdk.NewAttribute("feature", "stargate"),
sdk.NewAttribute("feature", "staking"),
)
// Instantiate Contract
sdk.NewEvent(
"instantiate",
sdk.NewAttribute("code_id", fmt.Sprintf("%d", msg.CodeID)),
sdk.NewAttribute("_contract_addr", contractAddr.String()),
)
// Execute Contract
sdk.NewEvent(
"execute",
sdk.NewAttribute("_contract_addr", contractAddr.String()),
)
// Migrate Contract
sdk.NewEvent(
"migrate",
// Note: this is the new code id that is being migrated to
sdk.NewAttribute("code_id", fmt.Sprintf("%d", msg.CodeID)),
sdk.NewAttribute("_contract_addr", contractAddr.String()),
)
// Set new admin
sdk.NewEvent(
"update_admin",
sdk.NewAttribute("_contract_addr", contractAddr.String()),
sdk.NewAttribute("admin", msg.NewAdmin),
)
// Clear admin
sdk.NewEvent(
"clear_admin",
sdk.NewAttribute("_contract_addr", contractAddr.String()),
)
// Pin Code
sdk.NewEvent(
"pin_code",
sdk.NewAttribute("code_id", strconv.FormatUint(msg.CodeID, 10)),
)
// Unpin Code
sdk.NewEvent(
"unpin_code",
sdk.NewAttribute("code_id", strconv.FormatUint(msg.CodeID, 10)),
)
// Emitted when processing a submessage reply
sdk.NewEvent(
"reply",
sdk.NewAttribute("_contract_addr", contractAddr.String()),
// If the submessage was successful, and reply is processing the success case
sdk.NewAttribute("mode", "handle_success"),
// If the submessage returned an error that was "caught" by the reply block
sdk.NewAttribute("mode", "handle_failure"),
)
// Emitted when handling sudo
sdk.NewEvent(
"sudo",
sdk.NewAttribute("_contract_addr", contractAddr.String()),
)
Note that every event that affects a contract (not store code, pin or unpin) will return the contract_addr as
_contract_addr
. The events that are related to a particular wasm code (store code, instantiate, pin, unpin, and migrate)
will emit that as code_id
. All attributes prefixed with _
are reserved and may not be emitted by a smart contract,
so we use the underscore prefix consistently with attributes that may be injected into custom events.
When a CosmWasm contract returns a Response
from one of the calls, it may return a list of attributes as well as a list
of events (in addition to data and a list of messages to dispatch). These are then processed in x/wasm
to create events that
are emitted to the blockchain.
If the response contains a non-empty list of attributes
, x/wasm
will emit a wasm
type event. This event will
always be tagged with _contract_address
by the Go module, so this is trust-worthy. The contract itself cannot overwrite
this field. Beyond this, the attributes
returned by the contract, these are appended to the same event.
A contact may also return custom events
. These are multiple events, each with their own type as well as attributes.
When they are received, x/wasm
prepends wasm-
to the event type returned by the contact to avoid them trying to fake
an eg. transfer
event from the bank module. The output here may look like:
sdk.NewEvent(
"wasm-promote"
sdk.NewAttribute("_contract_addr", contractAddr.String()),
sdk.NewAttribute("batch_id", "6"),
sdk.NewAttribute("address", "cosmos1234567"),
sdk.NewAttribute("address", "cosmos1765432"),
),
sdk.NewEvent(
"wasm-promote"
sdk.NewAttribute("_contract_addr", contractAddr.String()),
sdk.NewAttribute("batch_id", "7"),
sdk.NewAttribute("address", "cosmos19875632"),
)
Note that these custom events also have the _contract_address
attribute automatically injected for easier attribution in the clients.
The multiple event API was designed to allow the contract to make logical groupings that are persisted in the event system,
more than flattening them all into one event like:
sdk.NewEvent(
"wasm"
sdk.NewAttribute("_contract_addr", contractAddr.String()),
sdk.NewAttribute("action", "promote"),
sdk.NewAttribute("batch_id", "6"),
sdk.NewAttribute("address", "cosmos1234567"),
sdk.NewAttribute("address", "cosmos1765432"),
sdk.NewAttribute("batch_id", "7"),
sdk.NewAttribute("address", "cosmos19875632"),
)
If the Response contains neither event
nor attributes
, not wasm*
events will be emitted, just the standard message
type as well as the action-dependent event (like execute
or migrate
). This is a significant change from pre-0.18 versions
where one could count on the wasm
event to always be emitted. Now it is recommended to search for execute._contract_address="foo"
to find all transactions related to the contract.
While the wasm
and wasm-*
namespacing does sandbox the smart contract events and limits malicious activity they could
undertake, we also perform a number of further validation checks on the contracts:
- No attribute key may start with
_
. This is currently used for_contract_address
and is reserved for a namespace for injecting more trusted attributes from thex/wasm
module as opposed to the contract itself - Event types are trimmed of whitespace, and must have at least two characters prior to prepending
wasm-
. If the contract returns " hello\n", the event type will look likewasm-hello
. If it emits " a ", this will be rejected with an error (aborting execution!) - Attribute keys and values (both in
attributes
and underevents
) are trimmed of leading/trailing whitespace. If they are empty after trimming, they are rejected as above (aborting the execution). Otherwise, they are passed verbatim.
Beyond the basic Event system and emitted events, we must handle more advanced cases in x/wasm
and thus add some more logic to the event processing. Remember that CosmWasm contracts dispatch other
messages themselves, so far from the flattened event structure, or even a list of list (separated
by message index in the tx), we actually have a tree of messages, each with their own events. And we must
flatten that in a meaningful way to feed it into the event system.
Furthermore, with the sub-message reply handlers, we end up with eg. "Contract A execute", "Contract B execute", "Contract A reply". If we return all events by all of these, we may end up with many repeated event types and a confusing results. However, we may use the standard "message" events to separate the sub-messages as it marks where the next one starts. With careful analysis of the "sender" field on these "message" markers, we may be able to reconstruct much of the tree execution path. We should ensure all this information is exposed in the most consistent way possible.
Each time a contract is executed, it not only returns the message
event from its call, the execute
event for the
contact and the wasm
event with any custom fields from the contract itself. It will also return the same set of information
for all messages that it returned, which were later dispatched. The event system was really designed for one main
action emitting events, so we define a structure to flatten this event tree:
- We only emit one event of type
message
. This is the top-level call, just like the standard Go modules. For all dispatched submessages, we filter out this event type. - All events are returned in execution order as defined by CosmWasm docs
x/wasm
keeper emits a custom event for each call to a contract entry point. Not justexecute
,instantiate
, andmigrate
, but alsoreply
,sudo
and all ibc entry points.- This means all
wasm*
events are preceeded by the cosmwasm entry point that returned them.
To make this more clear, I will provide an example of executing a contract, which returns two messages, one to instantiate a new
contract and the other to set the withdrawl address, while also using ReplyOnSuccess
for the instantiation (to get the
address). It will emit a series of events that looks something like this:
/// original execution (top-level message is the only one that gets the message tag)
sdk.NewEvent(
"message",
sdk.NewAttribute("module", "wasm"),
sdk.NewAttribute("sender", msg.Sender),
),
// top-level exection call
sdk.NewEvent(
"execute",
sdk.NewAttribute("_contract_addr", contractAddr.String()),
),
sdk.NewEvent(
"wasm",
sdk.NewAttribute("_contract_addr", contractAddr.String()),
sdk.NewAttribute("custom", "from contract"),
),
// instantiating contract (first dipatched message)
sdk.NewEvent(
"instantiate",
sdk.NewAttribute("code_id", fmt.Sprintf("%d", msg.CodeID)),
sdk.NewAttribute("_contract_addr", newContract.String()),
)
// didn't emit any attributes, but one event
sdk.NewEvent(
"wasm-custom",
sdk.NewAttribute("_contract_addr", newContract.String()),
sdk.NewAttribute("foobar", "baz"),
),
// handling the reply (this doesn't emit a message event as it never goes through the message server)
sdk.NewEvent(
"reply",
sdk.NewAttribute("_contract_addr", contractAddr.String()),
sdk.NewAttribute("mode", "handle_success"),
),
sdk.NewEvent(
"wasm",
sdk.NewAttribute("_contract_addr", contractAddr.String()),
sdk.NewAttribute("custom", "from contract"),
),
// calling the distribution module (second dispatched message)
sdk.NewEvent(
"set_withdraw_address",
sdk.NewAttribute("withdraw_address", withdrawAddr.String()),
),
When the reply
clause in a contract is called, it will receive the data returned from the message it
applies to, as well as all events from that message. In the above case, when the reply
function was called
on contractAddr
in response to initializing a contact, it would get the binary-encoded initData
in the data
field, and the following in the events
field:
sdk.NewEvent(
"instantiate",
sdk.NewAttribute("code_id", fmt.Sprintf("%d", msg.CodeID)),
sdk.NewAttribute("_contract_addr", newContract.String()),
)
sdk.NewEvent(
"wasm-custom",
sdk.NewAttribute("_contract_addr", newContract.String()),
sdk.NewAttribute("foobar", "baz"),
),
If the original contract execution example above was actually the result of a message returned by an eg. factory contract,
and it registered a ReplyOn clause, the reply
function on that contract would receive the entire 11 events in the example
above, and would need to use the message
markers to locate the segment of interest.
The governance process is handled by the cosmos-sdk gov
module. We do not emit any events of type "message" anymore in v0.18+.
Context-specific events are emitted as described above. Execution
and Migration
return some contract result though that are
emitted as:
sdk.NewEvent(
"gov_contract_result",
sdk.NewAttribute("result", hex.EncodeToString(data)),
)
All IBC entry points are only called by external accounts and not from contracts. They need to contain proofs of state of other blockchains and cannot be called by other contracts on the same chain. Therefore, the event emitted are not essential for cross-contract calls, and x/wasm
does not emit custom events for these actions.
There are well-defined events emitted by the IBC base layer and are required for the relayer functionality. If you wish to subscribe to these, you can find them defined in the ibc-go
codebase.