Cross-Contract Calls¶
Dispatch / reply pattern¶
Contracts communicate asynchronously using dispatch and reply:
- Contract A calls
host::dispatch(target, msg, reply_on, funds)during itsexechandler. - A receives back a
msg_id(a locally-uniqueu64). - A finishes execution and returns.
- The runtime executes the sub-message against contract B.
- If the
reply_onvariant matches the outcome, A'sreply(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.
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.