CosmWasm concepts overview

The creators of the CosmWasm pieces had the benefit of hindsight and so reused and/or modified concepts found in other smart contracting systems, and invented others.

Bytecode lifecycle

At the risk of pushing an open door, a smart contract has an associated bytecode, and that bytecode has to be stored somewhere.

If you come from Ethereum, then it sounds natural to you that each smart contract instance has its own bytecode.

CosmWasm, on the other hand, separates storing of the bytecode on chain, and instantiating a smart contract instance using said bytecode.

A stored bytecode is identified by an id, which is just a good old auto-incrementing integer. Optionally, you can also apply permissions to control the use of this stored bytecode, and change these permissions at a later stage. Thereafter, when you instantiate a smart contract instance, you mention the bytecode id to use.

Here is what these actions would look like on the command line:

Sending a transaction to store a bytecode on-chain looks like this:

Command line
wasmd tx wasm store path_to/compiled_smart_contract.wasm \
    --from ...

After the transaction has been validated, you can find the bytecode id in the events, as it looks something like that:

Transaction event
...
"type": "store_code",
"attributes": [
    {
        "key": "code_id",
        "value": "8",
        "index": true
    },
]
...

This tells you that the just-stored bytecode has the id 8.

A non-negligeable side benefit of having the bytecode stored separately is that when one smart contract deploys another, it only needs to mention the bytecode id to use, instead of passing the whole bytecode. From the smart contract's point-of-view, it would look like this:

Pseudo-Rust-code execute return
return Ok(response.add_message(
    SubMsg::reply_on_success(      // The eventual reply contains the new address
        WasmMsg::Instantiate {     // Instruct the CosmWasm module
            code_id: 8u64,         // 8 written with 64 bits
            msg: to_json_binary(   // Serialize the instantiate message
                &InstantiateMsg {  // An instantiate message valid for this bytecode
                    constructor_field1: ...
                }
            )?,
            ...
        },
        ...
    )
));

If you want to store code and instantiate within the same transaction, you can also do that with a special message, although it is not available from the command-line out of the box. Unless you add the command-line bindings yourself, that is.

Once stored, a bytecode cannot be modified. In particular, you cannot upgrade, or migrate, a bytecode as part of an upgrade of the underlying app chain. On the other hand, a smart contract instance's id of the bytecode in use can either be immutable or upgradeable; you decide at deployment which one it shall be by setting or ommitting the admin.

--- config: {} title: The CosmWasm Way --- flowchart TB subgraph StoreTx["Signed Store Tx"] BytecodeTx["Bytecode"] end subgraph BytecodeStore["CosmWasm Bytecode Store"] direction LR CodeId["code_id"] BytecodeStored["Bytecode"] end subgraph Tx1["Signed Instantiate Tx1"] CodeIdTx1["code_id"] end subgraph Instance1["Smart Contract Instance1"] direction TB State1["Instance Store"] StoredCode1["code_id"] Stor1["State"] end subgraph Tx2["Signed Instantiate Tx2"] CodeIdTx2["code_id"] end subgraph Instance2["Smart Contract Instance2"] direction TB State2["Instance Store"] StoredCode2["code_id"] Stor2["State"] end Code["Code"] --> Compile{"Compile"} Compile --> Bytecode["Bytecode"] Bytecode -->|Copy| BytecodeTx StoreTx --> BytecodeStore CodeId -->|Maps to| BytecodeStored Tx1 --> Deploy1{"Instantiate"} Tx2 --> Deploy2{"Instantiate"} Deploy1 --> Instance1 Deploy2 --> Instance2 CodeId -.->|Copy| CodeIdTx1 & CodeIdTx2 State1 -->|Contains| StoredCode1 & Stor1 State2 -->|Contains| StoredCode2 & Stor2 style BytecodeTx fill:#E1BEE7 style BytecodeStored fill:#E1BEE7 style CodeId fill:#BEE1E7 style StoredCode1 fill:#BEE1E7 style StoredCode2 fill:#BEE1E7

If you come from Ethereum, you expect a deterministic compilation step, which makes it possible to verify that a given bytecode is the product of a given code. CosmWasm offers the same tool chain, with a verifier for Rust code.

The module's messages

Because the CosmWasm module is a Cosmos module, it is called to action with its own queries, messages, and hooks. As examples of Cosmos messages, you have:

  • MsgStoreCode is used to store a bytecode inside the CosmWasm bytecode store. When sending it, you just put the bytecode in bytes wasm_byte_code. To see it in action, go to Store your contract code part of the hello world.

  • MsgExecuteContract is used to instruct the CosmWasm module to call the execute entry point, a.k.a. function, on the contract instance identified by string contract, and with the entry point arguments serialized in bytes msg.

    tx.proto
    message MsgExecuteContract {
        ...
        // Sender is the that actor that signed the messages
        string sender = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];
        // Contract is the address of the smart contract
        string contract = 2 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];
        // Msg json encoded message to be passed to the contract
        bytes msg = 3 [
            (gogoproto.casttype) = "RawContractMessage",
            (amino.encoding) = "inline_json"
        ];
        ...
    }

    To belabor the above point:

    1. message MsgExecuteContract is a Cosmos SDK message that the app-chain sends to its own CosmWasm module.
    2. bytes msg is the serialized CosmWasm ExecuteMsg that the CosmWasm module sends to the execute entry point of one of the smart contracts instantiated within the CosmWasm module. Its interpretation is up to the smart contract itself.

    To see it in action, go to Send a transaction to your contract part of the hello world. To see how the smart contract handles the ExecuteMsg, go to the practical exercise's First Execute Transaction.

A smart contract's messages

When it comes to the smart contract messages, the role of the CosmWasm module is:

  • To pass these pieces of information onwards to the proper smart contract(s)' entry points,
  • and in Rust form (in its WebAssembly form actually),
  • with certain guarantees about the information passed along.

For instance:

  • The smart contract can know with certainty:
    • Among other message-related information, which account sent the message:
      MessageInfo's sender
      pub struct MessageInfo {
          /// The `sender` field from `MsgInstantiateContract` and `MsgExecuteContract`.
          /// You can think of this as the address that initiated the action (i.e. the message). What that
          /// means exactly heavily depends on the application.
          ///
          /// The x/wasm module ensures that the sender address signed the transaction or
          /// is otherwise authorized to send the message.
          pub sender: Addr,
          ...
      }
      This sender may be the signer of a transaction or may be another smart contract within the same CosmWasm module (think Ethereum Solidity's msg.sender). Your smart contract ought to be agnostic as to which one it is.
    • And, among other block-related information, at what height the whole chain is/was at the time of execution:
      BlockInfo's height
      pub struct BlockInfo {
          /// The height of a block is the number of blocks preceding it in the blockchain.
          pub height: u64,
          ...
      }
      (Think Ethereum Solidity's block.height).
  • When a user instructs the CosmWasm module that a smart contract call needs to include a certain amount of tokens, the CosmWasm module will take these tokens from the sender and inform the smart contract that it has in fact taken these tokens and credited them to the smart contract's address (Think if there was an Ethereum Solidity's msg.value for ERC-20s). The smart contract can then work with this guaranteed assumption.
    MessageInfo's funds
    pub struct MessageInfo {
        ...
        /// The funds that are sent to the contract as part of `MsgInstantiateContract`
        /// or `MsgExecuteContract`. The transfer is processed in bank before the contract
        /// is executed such that the new balance is visible during contract execution.
        pub funds: Vec<Coin>,
    }
    See the hello world's Send a transaction to your contract to pass funds along with a contract call, and the practical exercise's Proper Fund Handling to see it implemented in your own smart contract.
  • When a smart contract sends a message to another one, and this other contract fails, the originating smart contract does not by default need to roll back any state changes. Instead, the CosmWasm module provides this guarantee of atomicity, unless instructed otherwise. See the practical exercise's First Contract Reply Integration for an example of the default behavior.
  • Additionally, when a smart contract sends a message to another and expects a reply, the payload returned (CosmWasm v2 only) is the same as the one that was sent initially, guaranteed by the CosmWasm module.

The structure from a Cosmos transaction to a message received by a CosmWasm smart contract can be summarized as follows:

--- title: Cosmos SDK to CosmWasm module's Messages Structure (Clickable) --- classDiagram namespace CosmosSDK { class Tx { +TxBody body ... } class TxBody { +Message[] messages ... } class CosmosMessage { +String type +[]Byte value ... } } namespace Bank { class BankModuleMessage { type = "cosmos.bank.v1beta1..." ... } class MsgSend { +Coins amount ... } } namespace CosmWasm { class CosmWasmModuleMessage { type = "cosmwasm.wasm.v1..." ... } class MsgStoreCode { +[]Byte wasm_bytecode ... } class MsgExecuteContract { +String contract_address +[]Byte inner_msg ... } } namespace SmartContractX { class ExecuteMsg { +String type +Any optional_data ... } } link Tx "https://github.com/cosmos/cosmos-sdk/blob/v0.50.9/types/tx/tx.pb.go#L34-L44" "Tx in Cosmos SDK" link TxBody "https://github.com/cosmos/cosmos-sdk/blob/v0.50.9/types/tx/tx.pb.go#L348-L372" "TxBody in Cosmos SDK" link CosmosMessage "https://github.com/cosmos/cosmos-sdk/blob/v0.50.9/codec/types/any.go#L15-L56" "CosmosMessage in Cosmos SDK" link BankModuleMessage "https://github.com/cosmos/cosmos-sdk/blob/v0.50.9/x/bank/types/tx.pb.go" "Bank messages in Cosmos SDK" link MsgSend "https://github.com/cosmos/cosmos-sdk/blob/v0.50.9/x/bank/types/tx.pb.go#L37-L41" "Bank MsgSend in Cosmos SDK" link CosmWasmModuleMessage "https://github.com/CosmWasm/wasmd/blob/v0.53.0/x/wasm/types/tx.pb.go" "CosmWasm messages in Wasmd" link MsgStoreCode "https://github.com/CosmWasm/wasmd/blob/v0.53.0/x/wasm/types/tx.pb.go#L40-L48" "MsgStoreCode in Wasmd" link MsgExecuteContract "https://github.com/CosmWasm/wasmd/blob/v0.53.0/x/wasm/types/tx.pb.go#L342-L351" "MsgExecuteContract in Wasmd" link ExecuteMsg "https://github.com/b9lab/cw-my-collection-manager/blob/main/src/msg.rs#L31-L36" "ExecuteMsg in smart contract" Tx --* TxBody : contains 1 TxBody --* CosmosMessage : contains many CosmosMessage <|-- BankModuleMessage : is a BankModuleMessage --* MsgSend : contains 1 (example) CosmosMessage <|-- CosmWasmModuleMessage : is a CosmWasmModuleMessage --* MsgStoreCode : contains 1 (example) CosmWasmModuleMessage --* MsgExecuteContract : contains 1 (example) MsgExecuteContract --* ExecuteMsg : contains 1 (example)

Entry points

As mentioned earlier, a CosmWasm smart contract can be deployed and called with a message, a reply or a query. These concepts are in fact identified in the smart contract interface as different methods called entry points.

A WebAssembly binary instead exports a set of functions that can be called, not unlike what a library would do. The Wasm VM of the CosmWasm module can call such a function. So the purpose of the entry point is to identify a function that should be exported by the WebAssembly compiler, and to confirm that the function signature conforms to the expectations of CosmWasm.

The execute entry point
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    mut deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> ContractResult {
    ...
}

The role of the CosmWasm module is to:

  • Call the relevant entry points of relevant smart contracts in the relevant situations.
  • Populate elements such as env and info with the right values.
  • Verify and then pass on the msg or other value coming from elsewhere.
  • Run the code and handle all returns, including potential rollbacks.

If you don't implement a given entry point, then it is closed and attempting to call it results in an error; there are no fallbacks. For instance, if your smart contract does not implement the sudo, or the migrate, entry point, then these functions are unavailable although your smart contract can still execute and respond to query.

--- title: Not all entry points need to be implemented --- classDiagram class ContractA { +execute() +query() +sudo() +migrate() +ibc_packet_receive() } class ContractB { +execute() +query() +sudo() } class ContractC { +execute() +query() }

Smart contract execution

Invocation

When a CosmWasm smart contract is invoked:

  • Its code is loaded into memory, either from the node's cache or from the app-chain state.
  • It is allocated some configurable amount of memory, typically 32 MB for the stack and the heap.
  • It is given access to its state.
  • It is given the arguments to run on. These arguments can come from various places, and are not limited to:
    • The message itself in the case of an ExecuteMsg or SudoMsg,
    • Or from IBC packets if the smart contract is configured for IBC.
  • Out of the execution come further messages and IBC packets, plus a possibly updated state.
--- title: State Machine - Execution Simplified --- flowchart TD MessageIn["Message In"] --> Execute StateIn["State In"] --> Execute StateIn --> HandlePacket subgraph ExecutionEnv["Execution Environment"] Execute HandlePacket["Handle IBC Packet"] end PacketIn["IBC Packet In"] --> HandlePacket subgraph ResponseOut["Response Out"] MessageOut["Messages Out"] PacketOut["IBC Packets Out"] end Execute --> ResponseOut HandlePacket --> ResponseOut Execute --> StateOut["State Out"] HandlePacket --> StateOut

Determinism

To achieve consensus on the blockchain, smart contracting platforms need to have deterministic execution, in the sense that non-validating nodes should be able to verify blocks, transactions and state at any later time. If you are familiar with the EVM, you know that all available opcodes are deterministic, and in fact have been created from scratch with determinism as a core requirement.

On the other hand, WebAssembly has a degree of nondeterminism, and Rust has libraries that give access to nondeterministic aspects, such as CPU time or network I/O. CosmWasm mitigates this issue by not exposing the OS layer to smart contracts.

DoS protection

In terms of gas, because each metered action in Web Assembly, for instance adding 2 integers, is potentially quite insignificant, the module meters gas in its own unit: wasm gas. At the moment, 1 wasm gas converts to 1/140,000th regular Cosmos gas. Also, the module does not double-count what is anyway measured at the SDK level, such as accessing the on-chain storage.

Deterministic invocation

Looking back at the execution arguments, wheverer they come from, they too are always part of the consensus:

  • Either because the values are inscribed in a transaction's message,
  • Or they have been computed by another smart contract, in the case of cross-contract messages.

Receiving tokens

A CosmWasm smart contract has an address and as such, you can send tokens to it in the same way that you send tokens to a Cosmos account, with the use of the bank module's SendMsg. And unlike what happens in Ethereum, this action does not trigger code execution. A smart contract can also send out tokens it owns as part of its execution.

Of course, it is possible to have code execute in the same transaction by sending a MsgExecute alongside the MsgSend. However, in this case, both messages are uncorrelated, and the smart contract cannot verify that the adequate transfer has been made. It can only check its own balance, which would be an attack vector.

However, throwing tokens over the fence to the smart contract is the lesser-used way of sending tokens to a smart contract. You should preferably use the funds feature of MsgExecute. As mentioned in the messages section above, the CosmWasm module will do the requested fund transfer prior to the execution, so that the smart contract has guarantees that the adequate token transfer has been done to its benefit. See this hands-on exercise for an example.

On the other hand, for IBC token transfers, it is possible for a smart contract to subscribe to IBC callbacks such that it can be notified as soon as it receives tokens or when the tokens it has sent cross-chain have been received or have timed out.

For a token transfer to be handled by the CosmWasm module, and the smart contract to have guarantees that the token transfer was done, a user has to create a MsgExecute message that carries funds information.

Smart contract instance storage

As mentioned earlier, bytecode is stored separately from smart contract instances. Now, each instance has its own on-chain storage, separate from each other. It is the role of the CosmWasm module to give access to its own storage, and only it, to a smart contract:

  • In read/write mode when passing a message or an IBC packet.
  • In read-only mode when passing a query.

The WebAssembly VM has no primitives that give access to a hypothetical on-chain storage. Instead, when the CosmWasm module calls one of the smart contract's functions, it passes along a context object with fields and methods that give access to the on-chain storage in read-only or read/write, depending on the context of the call, i.e. query or execution respectively.

pub fn query(deps: Deps, ...) ...

Deps gives access to immutable, i.e. unmodifiable, state.

Unsurprisingly, the CosmWasm module arranges each smart contract's storage (think contract1337/) within its own module's storage (think wasm/), which is itself part of the on-chain storage (think /). If you need to, refresh yourself on Cosmos SDK keeper storage.

--- title: Chain Storage Sub-division --- flowchart TB subgraph ChainStorage["Chain Storage '/'"] direction TB subgraph WasmModule["CosmWasm Module Storage 'wasm/'"] direction TB subgraph BytecodeStorage["Bytecode Storage 'bytecode/'"] direction TB map["code_id → wasm_bytes"] end subgraph Contracts["Instances Storage 'contract'"] CONTRACT1["Contract #1337 '1337/' • State • Config"] CONTRACT2["Contract #1338 '1338/' • State • Config"] CONTRACT3["Contract #1339 '1339/' • State • Config"] end end OTHER_MODULES["Other Modules' Storage • 'bank/' • 'staking/' • Custom..."] end %% Styling classDef chainBox fill:#e6f3ff,stroke:#99ccff classDef moduleBox fill:#fff0f5,stroke:#ffb6c1 classDef contractBox fill:#f0fff0,stroke:#99ff99 classDef accessBox fill:#fffae6,stroke:#ffd700 classDef evmBox fill:#f0e6ff,stroke:#cc99ff classDef implBox fill:#ffe6e6,stroke:#ff9999 class ChainStorage chainBox class WasmModule,BYTECODE moduleBox class Contracts,CONTRACT1,CONTRACT2,CONTRACT3 contractBox class AccessControl,MSG_ACCESS,QUERY_ACCESS accessBox class ComparisonEVM,EVM_CONTRACT,EVM_OPS,EVM_SCOPE evmBox class Implementation,PREFIX,KEY_VALUE,ISOLATION implBox

Calling out from the smart contract

An interesting feature of smart contracts is that they can call out to other parts of the system. CosmWasm smart contracts do too.

Almost absent reentrancy risk

If you come from the EVM world, you ought to be well acquainted with the risks associated with reentrancy. This risk is facilitated by smart contracts calling each other mid-execution. Eventually, you learn to implement the checks-effects-interactions design pattern as a protection mechanism.

CosmWasm reduces the risk of reentrancy with a different design at the platform level: a smart contract instance can only call out to other systems, including other smart contracts, only after it has exited its own invocation. In effect, this forces the interactions part to come last. Of course, it is still on you, the smart-contract developer, to correctly implement the checks and effects parts within the function's body. The call mechanism is as follows.

Invocation mechanism

A successful smart contract execution can return a list of messages that will be acted upon in turn within the same transaction, therefore atomically. For instance, to invoke another smart contract, the invoking smart contract can return a WasmMsg::Execute as part of its returned messages. This sequential design approach ensures that the execution of the invoked smart contract does not insert itself in the middle of the execution of the invoker, all the while preserving guarantees of atomicity.

pub fn execute(...) -> ContractResult {
    // Checks
    // Effects
    Ok(response.add_submessage(onward_sub_msg))
}

Note that if there are nested returned messages, messages are evaluated depth-first as this cleaves to the intent of the caller.

Go to this exercise to see it in action.

Reply mechanism

Of course, in some situations, your smart contract may invoke another and expect to receive a value in return, such as a newly created identifier. A fire-and-forget message at the end of the execution will not work. In this case, instead of returning a simple message, you need to wrap the message to create a sub-message. The mechanism for this is to mention, as part of the sub-message, that a reply is expected.

pub fn execute(...) -> ContractResult {
    // Checks
    // Effects
    let onward_sub_msg = SubMsg {
        id: 1u64,
        msg: CosmosMsg::<Empty>::Wasm(onward_exec_msg),
        reply_on: ReplyOn::Success,
        gas_limit: None,
    };
    Ok(response.add_submessage(onward_sub_msg))
}

And to code the reply entry point (function) such that it handles all expected replies. Your sub-message contains an id for the smart contract to match the reply to the original sub-message, in order to carry on with the execution. The sub-message also contains a binary payload (in CosmWasm 2.0) that you can use as call context instead of using the storage temporarily, thereby saving gas. Go to this exercise to see it in action.

pub fn reply(..., msg: Reply) -> ContractResult {
    match ReplyCode::try_from(msg.id)? {
        1u64 => reply_pass_through(..., msg),
    }
}

Take note of how the reply entry point is deliberately different from the execute entry point, always with a view of reducing the risk of reentrancy. It is incumbent on you, the developer, to not re-introduce an attack vector by moving checks or effects from execute to reply.

Composing invocations

Moreover, there is a mechanism to let you, the developer, substitute the returned value of the original execute call with the returned value of the corresponding reply call. This substitution makes it possible to have the combination of execute and reply act as as single invocation from the point of view of the original caller.

This lets you achieve cross-contract communication and cooperation, also called composition, as with the EVM, but with a reduced risk of reentrancy. This does not eliminate it entirely, though, as you still have to code all of your checks-effects into the body of the execute function, and avoid implementing the effects in the reply function.

In a sense, with this message-passing architecture, the set of CosmWasm smart contract instances follows the principles of an actor model.

Cross query

The above is about messages, which have the potential to change contracts' states, and where attacks may happen. On the other hand, if a smart contract only needs a value in a read-only mode, this is a different and safer situation. For this situation, the context object has the method deps.querier.query that lets the smart contract call another synchronously in a read-only mode, where the read-only part is enforced at the VM level.

let token_count_result = deps.querier
    .query::<NumTokensResponse>(&QueryRequest::Wasm(WasmQuery::Smart {
        contract_addr: collection,
        msg: to_json_binary(&CollectionQueryMsg::NumTokens {})?,
    }));

If your smart contract calls others, you can also include the target's message types in order to benefit from compile-time type checking, instead of passing along serialized binaries.

If you want to learn more about learnings from Ethereum, head here.

To app-chain modules

Smart contract can also call modules found in the underlying Cosmos SDK app-chain, and vice versa.

So that you don't have to reinvent the wheel, the Rust library offers traits that expose frequently-used Cosmos SDK modules, such as bank. It also has base types that help you create custom messages and queries, called bindings, to access custom modules of your app chain.

Use Bank's MsgSend
BankMsg::Send {
    to_address: payment_params.beneficiary.to_string(),
    amount: vec![paid],
}

And to mitigate potential mishaps, smart contracts can check at instantiation that the underlying app chain supports their requirements.

It is also possible for app-chain modules to call smart contracts. A simple way would be to have an app-chain module's keeper have access to the CosmWasm module's keeper (or message server) so as to call the Execute or SmartQuery functions on it.

Sending tokens

You have seen that a smart contract can receive tokens, just as any other on-chain account but also, and preferably, via the funds feature of MsgExecute. Now because a smart contract can:

  • Send a MsgExecute as part of its return calls, therefore it can also set the funds field so that the target smart contract receives the funds and gets the assurance it received the funds.
  • Send a bank MsgSend as part of its return calls, therefore it can also send tokens to whatever on-chain account, including another smart contract.

Tying it together

As an example, if you have Contract A's execution that makes calls to other smart contracts and modules, it could look like this:

--- title: Cross elements execution --- sequenceDiagram participant System box Contract A participant Contract A Execution as Execute participant Contract A Reply as Reply end participant Contract B as Contract B Query participant Contract C as Contract C Execute participant Other Cosmos Module System->>Contract A Execution: Invoke execute activate Contract A Execution Contract A Execution->>Contract B: Query for value activate Contract B Note over Contract A Execution,Contract B: Read-only! Contract B->>Contract A Execution: Receive queried value deactivate Contract B Contract A Execution->>Contract A Execution: Return response with onward messages Contract A Execution->>Contract C: Execute message 1 with reply deactivate Contract A Execution activate Contract C Contract C->>Contract C: Return response with reply result Contract C->>Contract A Reply: deactivate Contract C activate Contract A Reply Contract A Reply->>Contract A Reply: Return response without further messages Contract A Reply-->>System: Success! deactivate Contract A Reply Contract A Execution->>Other Cosmos Module: Execute message 2 without reply Other Cosmos Module-->>System: Success!

Testing

Testing your smart contracts, as for all your software projects, should be part of your development. Their are different levels of testing, and you benefit from existing tools at each level.

  • Rust unit tests. They reside in your code, and are tested purely in a Rust way, without even any conversion to WebAssembly. See this exercise for an introduction and this one to experience it in more details.
  • Mocked-app tests, happening all in Rust with the use of the cw-multi-test library to test your contract's interactions with a mocked CosmWasm module. See this exercise for an introduction and this one to experience it in more details.
  • Message mocked tests. You create mocks of your SDK modules, against which your smart contracts communicate.

Client side

You can easily create UIs for your smart contracts, all the more so that they encode their messages in JSON. This JSON encoding also allows you to easily debug messages when the time comes. The CosmJS library offers CosmWasm bindings that you can extend with your own types.