- Docs
- Smart Contracts
- Contract composition
Contract composition
In CosmWasm, the actor model of dispatching messages and making synchronous queries provides the foundation for building complex and interoperable smart contracts. This architecture allows for the composition of contracts with other contracts and native modules, enabling sophisticated decentralized applications. Below, we will explore how these components interact and how they can be extended.
Terminology
- Contracts: Refers to CosmWasm code that is dynamically uploaded to the blockchain and associated with a specific address.
- Native Modules: Refers to Cosmos SDK modules, written in Go, that are compiled into the blockchain binary.
CosmWasm supports composition between contracts and native modules. However, integrating with native modules requires careful consideration, as it can introduce portability issues. To address this, CosmWasm provides abstractions that help minimize these issues when interacting with native modules.
Messages
In CosmWasm, both instantiate (previously known as init) and execute (previously known as handle) functions can return multiple CosmosMsg objects. These messages are re-dispatched within the same transaction, ensuring atomic execution and rollback capabilities. There are three main types of messages:
- Contract: Sends a message to another contract address with a serialized message.
- Module Interfaces: Provides standardized interfaces that expose native modules through a portable interface, allowing interaction with modules like the bank or staking.
- Custom: Enables chain-dependent extensions to message types, allowing interaction with custom native modules. These custom messages should ideally remain immutable on the same chain over time but do not guarantee portability across different chains.
Queries
Contracts can make synchronous, read-only queries to the surrounding blockchain environment. Similar to messages, there are three primary types of queries:
- Contract: Queries another contract by sending a serialized message and receiving a binary serialized response.
- Module Interfaces: Standardized interfaces that allow querying native modules across different chains, such as querying balances or staking information.
- Custom: Chain-dependent queries that interact with custom native modules. Like custom messages, these queries are chain-specific and do not guarantee portability.
Cross-contract queries take the address of the target contract and a serialized QueryMsg in the contract-specific format, returning a binary serialized value. The calling contract must understand the appropriate formats. To simplify this, type-safe wrappers can be provided, similar to how CosmWasm offers the query_balance
method as a wrapper around the query implementation.
Modules
CosmWasm provides standardized module interfaces that ensure consistent integration across all CosmWasm-enabled chains. The most basic module is the Bank module, which provides access to native tokens and includes:
- BankMsg::Send: For sending tokens.
- BankQuery::Balance: For querying a specific balance.
- BankQuery::AllBalances: For querying all balances.
Another key module is Staking, which supports PoS systems with standardized messages like:
- Delegate
- Undelegate
- Redelegate
- Withdraw
These interfaces allow contracts to interact with staking mechanisms and query validators and delegations.
The advantage of these standardized interfaces is that they allow a contract to run on different blockchains, even if they have different customizations or are running different versions of the Cosmos SDK. However, every module interface must be integrated across the entire stack, which can delay support for custom features. Using the standardized module interfaces is recommended for maximum portability, while custom types should be used sparingly.
Customization
Some blockchains may want to allow contracts to interact with their custom Go modules without waiting for these modules to be standardized. For this purpose, CosmWasm introduces the Custom variant in both CosmosMsg and QueryRequest. Contracts can define the types to be included in these custom variants, which must be understood by the corresponding Cosmos SDK application.
Design Considerations
To build robust and interoperable contracts, certain design principles should be adhered to:
Portability
Contracts should be able to run on different blockchains, even if they use different Go modules or versions of the Cosmos SDK. By avoiding custom messages and carefully checking for optional features, contracts can achieve high portability. Currently, the key features to consider are Staking (which assumes a PoS system) and iterator (which assumes the ability to perform prefix scans over storage, typically in a Merkle Tree structure).
Immutability
Contracts are immutable once deployed, meaning that they encode the query and message formats in their bytecode. If a contract were to dispatch sdk.Msg in the native format (e.g., JSON, Amino, or Protobuf), any changes in that format could break the contract. This could prevent operations like undelegating tokens from the staking module. Since the Cosmos SDK frequently introduces breaking changes, CosmWasm ensures an immutable API to a potentially mutable runtime, preventing such issues.
Extensibility
It should be possible to add new interfaces to contracts and blockchains without needing to update all intermediate layers. For example, if you are building a custom blockchain that uses x/wasm from wasmd, you should be able to develop contracts that interact with your custom modules without modifying core CosmWasm repositories. Custom variants of CosmosMsg and QueryRequest enable such extensions.
Usability
CosmWasm's use of JSON encoding for messages allows for easy export of JSON schemas, facilitating auto-generation of client-side codecs.
Checking for Support
Contracts can use feature flags, exposed as wasm export functions, to specify which extra features they require. This allows the host chain to check compatibility before allowing a contract to be uploaded. For example, a contract might include a "ghost" export like requires_staking() to indicate that it requires staking functionality. When instantiating x/wasm.NewKeeper(), you can specify which features are supported.
Type-Safe Wrappers
When querying or calling other contracts, type checks provided by native module interfaces are lost. To mitigate this, type-safe wrappers can be created. These wrappers act as interfaces that other contracts can use to interact with the contract safely.
For example:
pub struct NameService(CanonicalAddr);impl NameService { pub fn query_name(deps: &Extern, name: &str) -> CanonicalAddr { /* .. */ } pub fn register(api: &Api, name: &str) -> CosmosMsg { /* .. */ }}
Rather than storing just the CanonicalAddr of another contract, the contract could store a NameService
instance, providing type-safe methods for interacting with the contract. These wrappers are tied to the contract's interface rather than its implementation, allowing easy integration across different contracts that support the same interface.