Transactions
Every contract invocation is wrapped into a transaction. If you know about transactions in SQL databases, you can consider them as the same basic concept. You execute multiple operations in a single transaction, and if one of them fails, the whole transaction is rolled back.
In our case, these operations are invocations of contract entrypoints. If one of the invocations in the chain fails, the whole transaction is usually rolled back. Failing in this context means that the contract entrypoint returns an error or panics.
Dispatching Submessages
Now let’s move onto the messages
field of the
Response
. Some contracts
are fine only talking with themselves. But many want to move tokens or call into other contracts for
more complex actions. This is where messages come in. We return
CosmosMsg
, which is a
serializable representation of any external call the contract can make.
This may be hard to understand at first. “Why can’t I just call another contract?”, you may ask. However, we do this to prevent one of the most widespread and hardest to detect security holes in Ethereum contracts - reentrancy. We do this by following the actor model, which doesn’t nest function calls, but returns messages that will be executed later. This means all state that is carried over between one call and the next happens in storage and not in memory. For more information on this design, I recommend you read our docs on the Actor Model.
A common request was the ability to get the result from one of the messages you dispatched. For
example, you want to create a new contract with
WasmMsg::Instantiate
,
but then you need to store the address of the newly created contract in the caller. This is possible
with messages
and replies. This makes use of
CosmosMsg
as above, but it
wraps it inside a SubMsg
envelope.
What are the semantics of a submessage execution? First, we create a sub-transaction context around
the state, allowing it to read the latest state written by the caller, but write to yet-another
cache. If gas_limit
is set, it is sandboxed to how much gas it can use until it aborts with
OutOfGasError
. This error is caught and returned to the caller like any other error returned from
contract execution (unless it burned the entire gas limit of the transaction).
If it returns success, the temporary state is committed (into the caller’s cache), and the
Response
is processed as
normal. Once the response is fully processed, this may then be intercepted by the calling contract
(for ReplyOn::Always
and ReplyOn::Success
). On an error, the subcall will revert any partial
state changes due to this message, but not revert any state changes in the calling contract. The
error may then be intercepted by the calling contract (for ReplyOn::Always
and ReplyOn::Error
).
In this case, the message’s error doesn’t abort the whole transaction.
Note, that error doesn’t abort the whole transaction if and only if the reply
is called - so in
case of ReplyOn::Always
and ReplyOn::Error
. If the submessage is called with ReplyOn::Success
or ReplyOn::Never
, the error in a subsequent call would result in failing whole transaction and
not commit the changes for it. The rule here is as follows: if for any reason you want your message
handling to succeed on submessage failure, you always have to reply on failure.
Preventing rollbacks in case of failure
If you don’t want your entire transaction to be rolled back in case of a failure, you can split the
logic into multiple messages. This can be two contracts, a contract executing itself or a contract
that sends a message to a Cosmos SDK module. Then use the reply_on
field in the message you send.
Set the field to one of the following values and instead of rolling back the transaction, you will
receive a message containing the error:
ReplyOn::Always
ReplyOn::Error
That way you can handle the error and decide what to do next, whether you want to propagate the error, retry the operation, ignore it, etc.
The default value ReplyOn::Success
means the caller is not ready to handle an error in the message
execution and the entire transaction is reverted on error.
Order of execution and rollback procedure
Submessages handling follows depth first order rules. Let’s see the following example scenario:
Note1: The
msg_responses
of the response are not forwarded down the call path. It means that for e.g. if Contract2
will not
explicitly handle response from Contract3
and forward any data, then Contract1
will never learn
about results from Contract3
.
Note2: If Contract2
returns an error, the error message can be handled by the Contract1
reply entry-point and prevent the whole transaction from rollback. In such a case only the
Contract2
and Contract3
states changes are reverted.