Skip to content

Cross-Contract Calls

Dispatch / reply pattern

Contracts communicate asynchronously using dispatch and reply:

  1. Contract A calls host::dispatch(target, msg, reply_on, funds) during its exec handler.
  2. A receives back a msg_id (a locally-unique u64).
  3. A finishes execution and returns.
  4. The runtime executes the sub-message against contract B.
  5. If the reply_on variant matches the outcome, A's reply(msg_id, success, data) export is called.

This pattern avoids re-entrancy. A contract never executes while one of its own calls is in flight.

ReplyOn variants

Variant Reply on success? Reply on error? Failure behavior
never No No Reverts entire transaction
success Yes No Reverts entire transaction
error No Yes A handles the error in reply
always Yes Yes A handles both outcomes in reply

When reply_on is error or always, a sub-message failure does not revert the parent. The parent's reply handler receives success = false and can decide how to proceed.

Synchronous queries

query-contract executes another contract's query export synchronously and returns the result inline. No state changes occur during a query -- the callee runs in read-only mode.

let result = host::query_contract(&target_addr, &msg)?;

Storage isolation

Each sub-message gets its own OverlayStack frame:

  • On success, the frame's writes merge into the parent frame.
  • On failure, the frame is discarded -- no state changes leak.

This provides transactional semantics at every level of nesting.

Fuel inheritance

Sub-messages share the caller's fuel budget. There is no separate fuel allocation per sub-message. If a sub-message exhausts all remaining fuel, the entire transaction fails.

Sender vs origin

Function Value
get-sender The contract that dispatched the sub-message (changes at each depth level)
get-origin The original transaction signer (constant across all depth levels)

Depth limit

The maximum sub-message nesting depth is 256 (MAX_SUBMSG_DEPTH). Attempting to dispatch beyond this limit returns an error.

Example flow

User tx -> Contract A (exec)
              |
              +-- dispatch(B, msg1, ReplyOn::Always, 0) -> msg_id=1
              |
              +-- returns Ok(...)
                     |
                     v
              Contract B (exec with msg1)
                     |
                     +-- returns Ok(result_data)
                            |
                            v
              Contract A (reply, msg_id=1, success=true, data=result_data)
                     |
                     +-- returns Ok(...)

If B had failed and reply_on was always or error, A's reply would receive success=false with the error message as data. B's state changes would be discarded.