CosmWasm for CTOs I: The Architecture

Ethan Frey
CosmWasm
Published in
10 min readOct 20, 2021

What makes CosmWasm so secure and easy to test?

Photo by Sven Mieke on Unsplash

Check out the intro to this series for context and links to all other articles.

Our thesis is that the best way to make a secure blockchain system is to make the space between “able to write a contract” and “able to write a secure contract” as small as possible.

This means it may take a few more days or even weeks than with other systems to get productive, but once you are writing contracts, there are very few gotchas beyond common sense. You should be able to get “secure contracts” with a few peer reviews, not $100k audits and years of training.

The way we acheived this is not by throwing away the idea of a Turing Complete language for some limited model, but rather by leveraging the actor model and slowly adding functionality as we figured out how to securely integrate it into the system. This post will give you a good understanding of what makes up our architecture.

Contract Lifetime

CosmWasm smart contracts have a similar flow to those in Ethereum, but with a few important differences. Rather than upload code and instantiate in one step, we realized that many contracts reuse the same code and only modify the configuration. I mean how many times has OpenZepplin’s ERC20 contract been deployed?

The first step in CosmWasm is to upload the code. The entire wasm code is gzipped and placed in a blockchain transaction, which can easily weigh in at 80KB. In this stage, all blockchain nodes unzip the code, verify it is compatible with the given CosmWasm runtime, and, if correct, compile wasm to native code with gas metering and store it on the local filesystem. This is a rather expensive step (may take 50-100ms) and costs a few million gas (which is only a couple dollars even on the larger Cosmos networks). However, it also guarantees that all following steps are very fast.

Once the code is uploaded, you can instantiate the code any number of times, with a different InstantiateMsg. This is similar to the constructor of Solidity contracts, but rather than call it from custom code, this is meant to be called by an external client. Here you set all immutable data and are assigned a unique contract address and storage slot. This is a privileged operation and can only be called once per contract (as you get a new address each time). Note that when instantiating a new contract, you may also assign an optional admin. This admin has the ability to “migrate” the contract to a new code version, replacing the need for all the proxy and library contract magic in Solidity.

Once a contract has been instantiated and an address has been created, anyone can execute it as many times as they wish. This is a standard “call tx” in Ethereum-speak. Rather than exposing a different entry point for each function, we declare one entry point, which takes a JSON-encoded type, which the contract then dispatches to the appropriate function. This dispatching is handled inside the Wasm code to keep the interface as simple and flexible as possible. However, we are working on some improvements to make this even easier..

You may also perform a read-only query on a contract. Either a “smart query”, which is run by the wasm contract with an encoded type, like execute. Or you may perform a “raw query” to read a given key-value pair directly (which can also provide proofs). When a client queries, you do not write a transaction to the blockchain, but read the data from one node. However, if a contract makes a query as part of a transaction, that is performed on every node. The query entry point has read-only access to the data store and cannot “call” into other contracts. This clear distinction between execution and query allows us to properly sandbox them and avoid reentrancy.

Execution in Detail

Now that you understand the general flow, let’s look at the entry points in detail, and the APIs exposed to the contract developer. Instantiate and execute have the same function prototype, they just take a different message and instantiate is guaranteed to be called exactly once. So, I will just describe the execute format.

What are these arguments?

  • DepsMut — mutable dependencies. That is general APIs, a Querier, and read/write Storage access
  • Envenvironment information. Block time/height, contract address and transaction info. (We do not include hashes so they are not misused for poor sources of randomness)
  • MessageInfoinformation used for authorization. The message signer and any native tokens passed as part of the call. (We never expose tx.origin)
  • ExecuteMsg —contract-specific type including the requested function name and arguments.
  • Responseallows you to return events and data, but more importantly, it contains an optional list of messages to be re-dispatched. This is how we enable contract to contract calls without re-entrancy.
  • ContractErrora contract-specific error type, very useful for unit testing. This must implement Display and will be converted to a string when returned from Wasm.

Queries in Detail

The query looks similar to execute, but with a trimmed down interface. Note how both methods to modify state are missing? Both read/write Storage access as well as the ability to call into other contracts via the Response.

  • Deps — immutable dependencies. Same as DepsMut above, but with read-only Storage access
  • Envas above
  • QueryMsg — like ExecuteMsg, a contract-specific type containing all possible queries
  • Binary — a simple wrapper around Vec<u8> to ensure Base64 encoding
  • StdError — the default error from the standard library if you don’t need to extend it with custom ContractErrors. (You could also use ContractError here if you wish)

Actor Model Explained

While the Querier in DepsMut and Deps allows you to call into other contracts (or native modules) in a synchronous way, these are provably read-only operations and it is impossible to trigger any reentrancy (recursion) in execute. This allows us to safely access any available data in the chain with a simple API.

In order to remove the possibility of reentrancy, we only run one message at a time rather than forming a call stack. This is based on the concept of the actor model, and provides a sound theoretical foundation for such encapsulation: “In response to a message it receives, an actor can: make local decisions, create more actors, send more messages, and determine how to respond to the next message received. Actors may modify their own private state, but can only affect each other indirectly through messaging”.

Note that a given actor only can process one message at a time (no second message comes in while it is processing one), and in CosmWasm we ensure all messages are run in a sequential, deterministic order. When contract A wants to call contract B, it will make any local changes and return a message to invoke contract B. If contract B then wants to invoke contract A, it saves its state and dispatches a message. Contract A then has a consistent view of the world in storage and there are no “in memory computations” which are the basis for reentrancy attacks.

The first question many people ask is how to handle errors. If my DEX wants to move 100 token A from your account via TransferFrom and send 50 token B, what happens if the first fails? Can I receive the B without paying the A? We handle this by providing an atomic transaction context for the entire top-level message. If any of the sub-messages returns an error, the parent will also return an error and revert all state changes made under it. This global transaction lock is where we deviate a bit from the pure actor model, as it requires sequential execution rather than concurrency (which we need in blockchain anyway).

Basically, we have optimistic message dispatching by default. Do some local changes on the assumption that all the dispatched messages will succeed. If any fail, then we revert the whole transaction. This avoids a whole class of errors with forgetting to properly check for failures in contract calls. There are some times when you want to handle an error, or get a result from the success case.

In those cases, you can use SubMessages, which allow you to respond to either the success or error case, in a manner consistent with the actor model. The contract dispatches some messages with this extra wrapper, and after that execution is finished, it will receive information on a special reply entry point. This entry point can only be called by the runtime, never by an external user or other contract.

If you wish to dig in deeper, I recommend reading “Actor Model for Contract Calls” from our docs, and the detailed SEMANTICS.md from the cosmwasm repo.

Secure By Design

As you see above, our architecture avoids the common attack vectors that plague Solidity contracts, and many other smart contract platforms as well.

  • By using the actor model, we avoid re-entrancy attacks, and complex work arounds to deal with them. Our default message handling avoids unchecked CALL return values, and you must explicitly handle such cases if you register a reply callback.
  • The separate instantiate endpoint ensures all data is initialized and avoids public init calls on an existing contract. In fact all methods exposed to a caller must be explicitly declared, and we have special entry points to separate privileged runtime callbacks from arbitrary messages from other contracts or external accounts.
  • We provide custom migrate functionality (described in the next article) to remove the need for tricky proxy contract and library contracts. An upgrade has a custom entry point, knows the version being migrated from, and can initialize any missing fields, or even abort the migration if it cannot safely upgrade.
  • We do not expose fields in the API which have been often misused, such as block hash (poor randomness) and tx.origin (poor authentication). The timestamp, which is exposed, is guaranteed to be BFT by Tendermint, meaning over 2/3 of the validators agree it is within some margin of error (10s) of their local time, and it is monotonically increasing.

If you want to see a more detailed comparison of how we handle the top-20 attack vectors in Solidity, check out this article:

Dependency Injection

Were you wondering where the names Deps and DepsMut came from? They are used to encapsulate all external dependencies. This allows us to easily use dependency injection to run the contract in many different environments, which is the key to testability.

When we compile to Wasm, we use ExternalStorage, ExternalApi, and ExternalQuerier to use imports that are provided by the Wasm runtime to call out into the surrounding blockchain. These are convenience wrappers around low-level FFI, providing safe Rust interfaces.

However, we also provide some mock_dependencies that can be used for testing. These implement the exact same interfaces and can be dropped into the same function calls in place of the external deps. Thus, we can unit test any function in native Rust, not only the top-level execute/query calls.

Those mock dependencies were designed to give a static view of the world to the contract and test one actor in isolation. As we started developing more complex composition, we realized we needed some way to easily test multi-contract integration in native Rust. We could upload them to the blockchain and call a method (like testing in Solidity), but this limits us to public interfaces, has a slow feedback loop, and the error messages returned from Wasm are much worse that those returned from native Rust tests (especially upon panic).

Luckily the dependency injection approach allows us to pass in arbitrary code into the contract, as long as it fulfills the proper interface. With that, we built the multi-test package, which is still rather experimental / unstable / undocumented, but used to test a number of cw-plus contracts, as well as our internal Tgrade contracts. This allows you to instantiate multiple contracts in native Rust, and connect them in a simulated blockchain. Take a look at how we test a cw20-base contract sending tokens to an escrow, then releasing them.

You can even build your own mocks without forking the standard library. Maybe you want to write a test to see how the code handles some external errors… just drop write a FailingApi struct and use that instead of MockApi for a test case or two. The key design is flexibility and extensibility to make it easy to test your code in many different ways, while leveraging all the standard Rust testing tools.

Full Stack Testing

Sometimes, full-stack tests with the compiled Wasm are needed, and we also provide a number of ways to do that. The lowest-level is running Wasm in the Rust VM with mock imports. This is generally only done in the CosmWasm repo to ensure our Wasm imports/exports are working properly.

You can also import wasmd and write some tests in Go to see how your compiled contract works in the real runtime. This will also let you test reply and sudo blocks, or other privileged entry points.

If you prefer to test in TypeScript, you can also use CosmJS as an example. We start up a local blockchain in CI, then upload, instantiate, and execute contracts. This is probably quite familiar to Ethereum developer workflow. And is nice if you are testing CosmJS bindings for your contract(s) before integrating them in a frontend app.

If you want to get to an even higher level, you can continue using TypeScript and leverage ts-relayer to write unit tests for cross-chain interactions involving smart contracts. This is how we verify the cw20-ics20 contract is compatible with native ics20 token transfer.

As you may have seen, we love testing our coding and relying on CI to ensure there are no regressions. We believe this is the only way to build complex software systems without rapidly decreasing velocity, as you track down bugs at many different levels. Thus, we provide easy ways to test your contracts on every different level they are executed on. Pick what works for you.

Keep Learning

Found this article useful? Want to know more about CosmWasm? Please follow me or CosmWasm on Medium to get all the new articles.

When you are ready to get your hands dirty, please check out CosmWasm docs and join CosmWasm Discord for support. Follow the tutorials, or leverage cw-template to start your first contract. Once that makes sense, look at cw-plus for a number of complex examples to see what is possible.

--

--