- Docs
- Smart Contracts
- Actor model for contract calls
Actor model for contract calls
The actor model is a design pattern used to build reliable, distributed systems. In this model, each "actor" has exclusive access to its own internal state and cannot call other actors directly. Instead, actors communicate by dispatching messages through a dispatcher, which manages the system's state and maps addresses to code and storage. In CosmWasm, this pattern is encapsulated in a simple interface:
pub trait Actor { fn handle(msgPayload: &[u8]) -> Vec<Msg>;}pub struct Msg { pub destination: Vec<u8>, pub payload: Vec<u8>,}
This is the basic model that underpins contract design in CosmWasm. You can see the same influence in the function:
pub fn handle<T: Storage>(store: &mut T, params: Params, msg: Vec<u8>) -> Result<Response>
Here, the response includes a Rust Vec
From this basic design, a few other useful aspects can be derived:
- Loose Coupling: Actors are loosely coupled, interacting only through well-defined message formats. This reduces the complexity of inter-contract communication and makes systems more modular. Contracts can communicate much like services using REST or RPC calls, enabling scalability and composability across different vendors and systems.
- Serialized Execution: Each actor (contract) processes messages one at a time, ensuring that the execute method cannot be interrupted by another message until it completes. This serialized execution model prevents race conditions and reentrancy attacks, making the contract execution more secure by design.
- Access Control: While contracts can perform "raw querying" to read the state of other contracts, they are strictly prohibited from writing to another contract's state. This ensures strong boundaries and prevents unauthorized modifications across contracts.
Another important aspect related to CosmWasm is locality: For two actors to communicate, a contract creator or user needs to send a message to the actor. Actors can only communicate with other actors after they have received the other actor's address. This is a flexible way to set up topologies in a distributed manner, and only requires you to hard-code the data format to pass to such addresses. Once some standard interfaces are established (such as ERC20, ERC721, ENS, etc.), we can support composability between large classes of contracts and different backing codes.
Locality and communication
In CosmWasm, the concept of locality is crucial. For two actors (contracts) to communicate, one must have the address of the other. This allows developers to set up flexible topologies in a distributed environment. Contracts can only send messages to other contracts whose addresses they have received, reinforcing security and modularity.
Once standard interfaces (like those for ERC20, ERC721, or other established protocols) are adopted, contracts can easily interact and compose with other contracts, regardless of the underlying implementation.
Security benefits
By enforcing a strict separation of internal state, CosmWasm ensures that each contract can fully control and validate all transitions within its state. Unlike the Cosmos SDK's capabilities model, where trusted modules can access and modify other modules' storage, CosmWasm maintains stricter boundaries. This is essential in a decentralized system where compile-time checks aren't possible, and runtime security is paramount.
- Serialized Execution: By enforcing serialized execution, CosmWasm guarantees that all state changes are written to storage before the next message is processed. This model is akin to an automatic mutex over the entire contract code, which effectively prevents reentrancy attacks—a common vulnerability in other smart contract platforms like Ethereum.
- Reentrancy Protection: An example of reentrancy prevention in CosmWasm would be:
Contract A
callsContract B
, which then calls back intoContract A
.- If
Contract A
had pending state changes from the first call (e.g., a balance deduction), these are finalized before any subsequent call. This design ensures that outdated state cannot be exploited, eliminating the risk of double-spending or similar attacks.
Atomic execution
A common challenge with message-based systems is ensuring that state changes across multiple contracts are committed atomically. In CosmWasm, this is achieved by using a SavePoint
mechanism:
- Before executing a message from an external transaction, the system creates a
SavePoint
of the global data store and passes a subset to the first contract. - All messages returned from this contract are executed within the same sub-transaction.
- If all messages succeed, the sub-transaction is committed. If any message fails (e.g., due to insufficient gas or a logical error), the system rolls back to the
SavePoint
.
This optimistic update approach allows developers to rely on rollback for error handling, ensuring that state transitions are either fully completed or not applied at all, thereby maintaining consistency.
Dynamically linking host modules
CosmWasm's locality and loose coupling mean that contracts can interact not only with other CosmWasm contracts but also with native modules in the Cosmos SDK or other blockchains. For instance, a contract can issue a SendMsg that the native x/bank module processes to move tokens. This dynamic linking capability is facilitated by the dispatcher, which routes messages to the appropriate modules based on addresses.
As standard interfaces for composability are established, developers can define clear, reusable interfaces for interacting with core modules and other contracts, making it easier to build complex, interoperable systems.
Inter blockchain messaging
CosmWasm's actor model, with its message-based communication, is naturally suited for cross-chain contract calls using the IBC protocol. However, when making cross-chain calls, the atomic execution guarantees provided by CosmWasm within a single chain do not apply. Instead, contracts must manage intermediate states until the result of the IBC call is known.
For example, to transfer tokens from Chain A to Chain B:
- Contract A reduces the sender's token balance and creates an escrow linked to the IBC message ID.
- Contract A then commits its state and initiates an IBC transaction to Chain B.
- If the IBC transfer fails, the transaction is rolled back, and the original state is restored.
Upon receiving a success or error/timeout message from the IBC handler:
- If successful, the escrowed tokens are moved to an account representing "Chain B".
- If the IBC transfer fails, the escrow is canceled, and the tokens are returned to the sender.
This approach ensures that cross-chain operations are secure and reliable, even when operating in a distributed, multi-chain environment.