Contract composition
Given the Actor model of dispatching messages and synchronous queries, we have all the raw components necessary to enable arbitrary composition of contracts with both other contracts and native modules. Here, we will explain how the components fit together and how they can be extended.
Terminology
Note that "Contracts" refer to CosmWasm code that is dynamically uploaded to the blockchain at a given address, while "Native Modules" are Cosmos SDK modules (written in Go) that are compiled into the blockchain binary.
We support composition between both types, but we must examine the integration with "Native Modules" more closely, as using them can cause portability issues. To minimize this issue, we provide some abstractions around "Modules".
Messages
Both init and handle can return an arbitrary number of CosmosMsg objects, which will be re-dispatched in the same transaction (thus providing atomic success/rollback with the contract execution). There are three classes of messages:
- Contract: This will call a given contract address with a given message (provided in serialized form).
- Module interfaces: These are standardized interfaces that can be supported by any chain to expose native modules under a portable interface.
- Custom: This encapsulates a chain-dependent extension to the message types to call into custom native modules. Ideally, they should be immutable on the same chain over time, but they make no portability guarantees.
Queries
Contracts can make synchronous read-only queries to the surrounding runtime. Similar to Messages, we have three fundamental types:
- Contract: This will query a given contract address with a given message (provided in serialized form).
- Module interfaces: These are standardized interfaces that can be supported by any chain to expose native modules under a portable interface.
- Custom: This encapsulates a chain-dependent extension to query custom native modules. Ideally, they should be immutable on the same chain over time, but they make no portability guarantees.
Cross-Contract queries take the address of the contract and a serialized QueryMsg in the contract-specific format, and synchronously receive a binary serialized return value in the contract-specific format. It is up to the calling contract to understand the appropriate formats. To simplify this, we can provide some contract-specific type-safe wrappers, much in the way we provide a simple query_balance method as a wrapper around the query implementation provided by the Trait.
Modules
To enable better integrations with the native blockchain, we are providing a set of standardized module interfaces that should work consistently across all CosmWasm chains. The most basic one is the Bank module, which provides access to the underlying native tokens. This gives us BankMsg::Send, as well as BankQuery::Balance and BankQuery::AllBalances to check balances and move tokens.
The second standardized module is Staking, which provides some standardized messages for Delegate, Undelegate, Redelegate, and Withdraw, as well as querying Validators and Delegations. These interfaces are designed to support most PoS systems and can be used with any PoS system, not just the current staking module of the Cosmos SDK.
This approach provides a clean design, where one can develop a contract that can run on two different blockchains, even if they are both heavily customized and running on different Cosmos SDK versions. The downside here is that every module interface must be added to all layers of the stack, which may cause some delay in supporting custom features, and we cannot easily add support for every custom module in every Cosmos SDK-based blockchain.
That said, it is highly recommended to use the module interface when possible and to use custom types as a temporary measure to maintain a fast release cycle.
Customization
Many chains will want to allow contracts to execute with their custom Go modules, without going through all the work to try to turn them into a standardized Module Interface and wait for a new CosmWasm release. For this case, we introduce the Custom variant on both CosmosMsg and QueryRequest. The idea here is that the contract can define the type to be included in the Custom variants, and it just needs to be agreed upon by the Cosmos SDK app (in Golang) on the other side. All the code between the contract and the blockchain code treats it as opaque JSON bytes.
Design considerations
To produce a solid design, we need to fulfill these requirements:
Portability
The same contract should be able to run on two distinct blockchains, with differing Golang modules, and different versions of the Cosmos SDK (or ideally, even based on different frameworks, for example, running on Substrate). This should be possible when avoiding Custom messages and checking the optional features one uses. The features are currently Staking, which assumes a PoS system, and iterator, which assumes we can do prefix scans over the storage (i.e., it is a Merkle Tree, not a Merkle Trie).
Immutability
Contracts are immutable and encode the query and message formats in their bytecode. If we allowed dispatching sdk.Msg in the native format (be it JSON, Amino, or Protobuf), and the format of native messages changes, then the contracts would break. This may mean that a staking module could never undelegate the tokens. If you think this is a theoretical issue, please note that every major update of the Cosmos SDK has produced such breaking changes and has migrations for them, migrations that cannot be performed on immutable contracts. Thus, we need to ensure that our design provides an immutable API to a potentially mutable runtime, which is a primary design criterion when designing the standard module interfaces.
Extensibility
We should be able to add new interfaces to contracts and blockchains without needing to update any of the intermediate layers. Let's consider the example in which you are building a custom superd blockchain app that imports x/wasm from wasmd and wants to develop contracts on it that call into your custom superd modules. The ideal scenario is that you could add those message types to an additional cw-superd library that you can import in your contracts and add the callbacks to them in superd Go code, without any changes in the cosmwasm-std, cosmwasm-vm, go-cosmwasm, or wasmd repositories.
Custom variants of CosmosMsg and QueryRequest are provided to enable such cases.
Usability
Using JSON encoding for the CosmWasm messages makes it simple to export JSON schemas for every contract to allow auto-generation of client-side codecs.
Checking for support
You can use feature flags, exposed as wasm export functions, to configure which extra features are required by the contract. This lets the host chain inspect compatibility before allowing an upload. To do so, we make some "ghost" exports like requires_staking(). This would only pass a compatibility check if the runtime also exposed such features. When instantiating x/wasm.NewKeeper(), you can specify which features are supported.
Type-safe wrappers
When querying or calling into other contracts, we give up all the type-checks we get with native module interfaces, meaning that the caller would need to know the details. It is useful to have "interface" wrappers that a contract could export so that other contracts can easily call into it.
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 the other contract in our configuration, we could store NameService, which is a zero-cost "newtype" over the original address, with the same serialization format. However, it would provide us with some type-safe helpers to make queries against the contract, as well as produce CosmosMsg for registration.
Note that these type-safe wrappers are not tied to an implementation of a contract but rather the contract's interface. Thus, we could create a small library with a list of standard/popular interfaces (like the ERCxxx specs) represented with such "newtypes". A contract creator could import one of these wrappers and then easily call the contract, regardless of implementation, as long as it supported the proper interface.