Loading article...
<1> From CosmWasm to Solana

rustopia<N>


From CosmWasm to Solana

Learning a New Platform as a Rust Smart Contract Developer

> <1>: CosmWasm vs. Solana for Rust Smart Contract Engineers <

<2>: Dopple DEX: Parallel Implementations in Solana and CosmWasm

<3>: Dopple DEX Continued: Finishing the Implementation

<4>: Testing and Benchmarking Dopple DEX


Solana and CosmWasm (for Cosmos SDK chains) offer smart contract platforms in Rust, but they differ in architecture, developer APIs, and tooling.

In 2024, I began to move from CosmWasm to Solana. Previously, I wrote smart contract systems and developer tooling and educational materials on CosmWasm. I've since implemented several Solana systems on and off chain, won a place in a Solana hackathon with an escrow and proof program, and now work full time with Solana on-chain programming at Anza Labs.

If you’ve learned one platform thoroughly and need to work in the other, this compendium is for you.

It's a comprehensive, organized reference of all the major differences, from architecture all the way to testing and deployment, with examples and external resources.

Note: the various available CosmWasm development frameworks haven't yet become the default for developers, but* Anchor *has seen significant adoption by Solana developers. For this reason, Solana example code and discussion considers three approaches:

  1. Solana SDK (using the breakout crates like solana_pubkeyCratesolana_pubkeyFrom crate: solana-pubkeyClick to view documentation →, solana_instructionCratesolana_instructionFrom crate: solana-instructionClick to view documentation →, etc., which previously were the monolithic solana_programCratesolana_programFrom crate: solana-programClick to view documentation → crate),
  2. AnchorCrateanchor_langFrom crate: anchor-langClick to view documentation →, a development framework that abstracts away some of the complexity of working with accounts and instructions – I do not personally use Anchor, but I've included examples for completeness – and
  3. pinocchioCratepinocchioFrom crate: pinocchioClick to view documentation → (a lightweight, compute-efficient alternative that can use up to 90% fewer compute units).

Table of Contents

  1. Overview: Blockchain Architecture & Execution Model
  2. Contract Deployment and Lifecycle
  3. Entry Points: Messages vs. Instructions
  4. A New State Model (Accounts on Solana)
  5. Managing Tokens & Native Assets
  6. Cross-Contract Calls and Composability (CPI vs Message Calls)
  7. Serialization and Data Formats
  8. Standard Library, Crates, and Environment
  9. Error Handling and Safety Patterns
  10. Time, Block Information, and Randomness
  11. Testing: Frameworks and Approaches
  12. Tooling and Developer Workflow

What I Assume You Know:

  1. The Rust programming language. But if you're more of a researcher or hobbyist, you'll still be able to follow the discussion.
  2. Basics of smart contract platforms. Terms like WASM, VM, etc. aren't defined here. I'm not going to explain why everything must be deterministic, why floats aren't allowed, or what a block height or slot hash is.
  3. Basics of either the CosmWasm or Solana architecture. You're free to try and learn both at once, but don't blame me if you end up confused. I often won't mention less obvious commonalities which are relevant to blockchain development: for example, how IDE tools like Rust work well with both, how "caveman debugging" is common, and so on.

! Definitions for new terms unique to Solana look like this and will be included throughout. If you see a box like this – TransferEnumTokenInstruction::TransferTransfer { amount: u64 }Click to view full documentation → – you can hover or click it to see more information straight from docs.rs.

Quick Start Example:

If you want to have a quick look at some code first, the incrementing counter is the Hello World of smart contracts, ever since Ethereum's first counter contract in 2015 (maybe earlier, if I missed it – the current version is here).

A version of the incremental counter implemented in both CosmWasm and Solana is available here.

But an incrementing counter leaves a lot to be desired, so I also provide a much deeper example to dive into in the next article in the series.


We have a lot of ground to cover; you might want to bookmark this article and take only a few sections at a time.

So, let's get started. It's tempting to take a hint from some 1977 sci-fi and just jump right into part 4: A New State Model. That's the juiciest plot point, and it's what gives Solana its ludicrous performance.

© Not Disney

First, though, we must endure a few prequels.

1. Overview: Blockchain Architecture & Execution Model

CosmWasm runs as a WASM-based VM module on Cosmos SDK blockchains. Not all Cosmos SDK blockchains have this module enabled. Smart contract code is deployed as WebAssembly (WASM) bytecode, and the CosmWasm toolchain and libraries generally assume that code will be written in Rust. Other languages are possible, but not common.

Each blockchain (referred to within the IBC world as each "zone") can host many CosmWasm contracts and can communicate across zones via IBC. This design is meant to scale horizontally across many chains and emphasizes interoperability.

Chains in the "Interchain" are governed independently: some, like the Cosmos Hub, have never enabled CosmWasm; some, like Osmosis, only allow smart contract code to be deployed if it passes a governance vote; and others have custom modules which provide CosmWasm non-standard capabilities.

CosmWasm contracts execute within a deterministic WASM sandbox which prevents floating-point operations and uncontrolled nondeterminism. Gas is metered per operation by the Cosmos SDK environment. More on these items in later sections.

Solana instead uses the Sealevel runtime with eBPF (Extended Berkeley Packet Filter) bytecode programs. Solana does not typically use the term "smart contract": code units deployed to the Solana network are referred to as "programs." As with CosmWasm, programs are typically written in Rust, though other languages can be used.

Solana scales vertically on a single high-throughput chain, parallelizing non-conflicting transactions. Solana uses a custom VM and runtime instead of EVM or WASM.

Solana also prevents floating-point operations and uncontrolled nondeterminism. Programs run as native BPF code on validators, with a computational budget (compute units) rather than a gas schedule.

Implications of Horizontal vs. Vertical Scaling:

  • Finality & Speed: Solana blocks are very fast (400ms) with high TPS. Cosmos chains have ~6 second blocks by default, with some faster exceptions such as Sei and (recently) Neutron.
  • Parallelism: Solana can execute transactions in parallel if they avoid touching the same accounts. CosmWasm contract calls execute one at a time; horizontal scaling means that parallelism architectures usually require multiple zones.

TL;DR – Solana scales by improving the speed of one chain. CosmWasm scales by adding more chains. Both run Rust smart contracts/programs which are forbidden from using floating-point numbers or randomness.

2. Contract Deployment and Lifecycle

Code and Code Addresses

CosmWasm contracts are deployed as WASM bytecode to a Cosmos chain via a store code transaction. This yields a code_id, and then one or more instances of that code can be deployed via an instantiate message. Each instance of contract code has its own address and state (storage). For example, the same NFT collection contract code may be used by various collections with no other relationship to one another.

(Ethereum is more similar to CosmWasm in this respect: different addresses with their own state can have the same code hashes, but their own state.)

Solana programs are deployed via solana program deploy, Anchor CLI, or somewhere else like the Solana Playground web interface. Each program is a single deployed binary identified by a program ID (public key). Solana doesn't support multiple "instances" of the same program code. Once deployed, program code has one constant address.

However, because Solana programs can manage arbitrary accounts, you don't need multiple instances of the same code. For example, one token program manages any number of token mints – most tokens on Solana today are managed by the SPL Token program or its extensible sibling Token-2022.

In this way, the USDC mint address is only a data storage location, with no code; the program logic and program address it uses are shared with innumerable other mint addresses.

Let's save any further discussion of this for #4. For now, remember that a single Solana program can act with any number of different states (different settings, accounts, balances, etc.). The logic program ID doesn't determine "which token" or "which pool" or "which user group" you're working with – that is determined by the account(s) being used by the program in any particular transaction.

Solana has no separate instantiate step as in CosmWasm: any Solana program logic for initialization must be handled within its instruction handlers, often via an Initialize instruction.

Upgrading Contract/Program Code

CosmWasm's migrate lets you change a contract's code (replace its binary) while preserving state. The new code's migrate() entrypoint can transform state if schema updates are required.

Upgrading a CosmWasm contract means uploading new code and executing a migrate message, if the contract was instantiated with an admin who can migrate. If migrate is unavailable or an admin is not set, the code is immutable. The admin can be nulled later, making a mutable contract immutable.

Smart contracts can even have the admin permission over other smart contracts. This enables wrapped migration, so that migration logic and migration admin management can be handled in contract code and not just by default protocol rules.

Solana's upgrade requires the upgrade authority and essentially replaces the program's binary. If a Solana program was deployed without the upgradeable loader or control of the upgrade authority address is unavailable, the program is permanent.

Since state remains in accounts, which the new code can read, no state copying is required here, either. State schema migration can be tricky, especially if accounts are used to hold structured state rather than primitives. The new program code can simply refer to additional accounts in order to extend state, but if those new accounts should somehow be "filled" for existing entities using the program, you'll probably have to handle this lazily in future executions, rather than in code executed upon migration.

If no upgrade authority exists, the program is immutable. Programs usually must be explicitly deployed without an upgrade authority, using a syntax such as solana program deploy --final.

upgrade-authority-flow diagram (dark theme)

Deployment Flow Summary

  • CosmWasm: Use the chain's CLI (e.g., wasmd tx wasm store <contract.wasm>) to upload code. Instantiate by sending an InstantiateMsg with any initial state – sometimes even {} – to get a new contract address. Each instance has an optional code admin who can migrate the contract to upgrade the code.
  • Solana: Use solana program deploy <program.so>, anchor build && anchor deploy with Anchor, or the deploy functionality of an interface like the Solana Playground. The program gets a fixed address. If your program needs initialization, the client must perform a transaction with the relevant instruction as a separate action. If you need upgradeability, use the upgradeable loader and ensure the key to the upgrade authority is safeguarded.

! Authority: A Solana address that has some kind of program logic permission over an account: permission to modify state by minting tokens, transferring tokens, updating settings, and so on.

Multi-Program Setups

Multi-contract applications are common in both ecosystems. In CosmWasm, "contract factories" are also common: smart contracts which deploy known on-chain code IDs to new contract addresses, thus creating new tokens, new user smart accounts, new liquidity pools, etc.

In Solana, code is not instantiated, but similar multi-program operations are handled with cross-program invocations. More on that later.

TL;DR – In CosmWasm, contracts can share code, but different state = different contract addresses. You can create a Solana-like model by having one logic contract (a unified address) refer to a state contract whose address is passed in, but this is uncommon.

In Solana, different state, different tokens, and different applications can use the same program ID, since state is held in accounts, not addressed to the program.

3. Entry Points: Messages vs. Instructions

Entry points are how the outside world interacts with a smart contract.

CosmWasm uses different entry points for contract calls.

cosmwasm-entrypoints diagram (dark theme)

Every CosmWasm contract has at least three entry functions: instantiate(), execute(), and query(). These correspond to different message types:

  • InstantiateMsg – parameters for one-time initialization when a contract instance is created.
  • ExecuteMsg – an enum of all executable actions (actions which change state).
  • QueryMsg – an enum of read-only queries. These do not change state, and as such do not require consensus and can be invoked quickly and easily. By default, queries cost no gas. Queries invoked by other smart contracts during multi-contract actions do cost gas.

Other entry points include migrate (update the contract), sudo (bypass the normal signature checking on chain – useful for applying custom verification logic), and reply (handle responses during multi-contract operations).

Inside an Entrypoint Function

At runtime, when an ExecuteMsg arrives, the contract's execute() code matches the variant and calls the appropriate logic:

#[cfg_attr(not(feature="library"), entry_point)]
pub fn execute(
    deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::Transfer { recipient, amount } =>
          try_transfer(deps, env, info, recipient, amount),
        ExecuteMsg::Burn { amount } =>
          try_burn(deps, env, info, amount),
        // ... any other variants ...
    }
}

Variants can be handled in line – which is often done with queries – or with handler functions (e.g., try_transfer).

Solana (native) programs have a single entry point:

solana-entrypoints diagram (dark theme)

This entrypoint function is usually named process_instruction(program_id, accounts, instruction_data) -> ProgramResult. All calls into a Solana program funnel through this C-compatible entry point, generated by some entrypoint!Macroentrypoint!macro_rules! entrypoint { ($($tt:tt)*) => { /* macro expansion */ } }Click to view full documentation → macro, of which several are available. It's up to the program to decode the byte array instruction_data in order to figure out which function to execute. Although many programs employ borshCrateborshBinary Object Representation Serializer for HashingClick to view full documentation → or bincodeCratebincodeA compact encoder / decoder pair that uses a binary zero-fluff encoding schemeClick to view full documentation → for this, any deterministic serialization can be used.

Similarly to CosmWasm, instructions are then matched:

entrypoint!(process_instruction);
fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
) -> ProgramResult {
    // Here `MyInstruction` is a custom enum of all of the program's instructions.
    // This is one way you might see deserialization performed.
    let instruction = MyInstruction::try_from_slice(instruction_data)
        .map_err(|_| ProgramError::InvalidInstructionData)?;
    match instruction {
        MyInstruction::Transfer { amount, recipient } => processor::process_transfer(program_id, accounts, amount, recipient),
        MyInstruction::Burn { amount } => processor::process_burn(program_id, accounts, amount),
        // ...
    }
}

Notice that while CosmWasm usually receives a typed message such as ExecuteMsg, which derives DeserializeTraitDeserializepub trait Deserialize<'de>: Sized { /* methods */ }Click to view full documentation →, here in Solana we explicitly deserialize, using a library or hand-rolled deserialization logic.

Anchor abstracts all of this away with attribute macros. In Anchor, you declare multiple functions in a #[program] module, for example:

#[program]
mod my_token {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, mint_authority: Pubkey) -> Result<()> {
        // ...
    }
    pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> {
        // ...
    }
    pub fn burn(ctx: Context<Burn>, amount: u64) -> Result<()> {
        // ...
    }
}

Anchor generates the process_instruction dispatch for you, based on an IDL and method name -> enum discriminant mapping.

! IDL: Interface Definition Language does exactly what it sounds like: it defines how the world can speak to the program. This is analogous to the ABI (Application Binary Interface) in EVM, or the auto-generated schema files in CosmWasm.

Each of these functions has a corresponding Context<...> struct that defines the expected accounts and signers. Anchor thus simulates multiple entry points, like CosmWasm's multiple messages, even though under the hood it's only using one dispatch function.

The newer **`pinocchio`**CratepinocchioFrom crate: pinocchioClick to view documentation → offers a third approach focused on compute efficiency. It provides lightweight alternatives to standard Solana SDK functions that can reduce compute unit consumption by up to 90%. While using the same underlying program model, pinocchio optimizes account access patterns and provides more efficient implementations of common operations. This makes it particularly valuable for programs that need to minimize transaction costs or fit within tight compute budgets.

Usually, the first thing any executable instruction does, on both platforms, is validate that the transaction is authorized and correct. Validation will be a significant part of writing our DEX in the next article, but first, we'll encounter some simple validation in later examples here.

On CosmWasm, you can get the contract's own address by using env.contract.address and get the message sender – the address that signed the message – via info.sender.

On Solana, the handlers validate that the expected accounts are present in the accounts array and have the correct permissions.

This has different implications for different kinds of instructions. A simple program may only care about the authority of the transaction sender, but a complex program may need to verify that a dozen passed-in account addresses are correct: matching canonical program IDs, deriving correctly as program-derived accounts, and/or matching authority addresses stored in accounts.

If you're new here, for you to understand any of the stuff I just said, we must finally talk about accounts.

TL;DR – CosmWasm's entry points are separated by purpose (instantiate/execute/query) and use structured message types. Solana's entry point is unified, but frameworks like Anchor provide a similar multi-function abstraction. In CosmWasm, the public API schema can be derived (as a JSON Schema) for client interaction with cargo run schema. Solana provides an IDL (Interface Description Language) if using Anchor, but manual documentation of the binary interface is required if not.

4. A New State Model (Accounts on Solana)

Previously titled Part III: “Duel of the States.

Since Bitcoin, blockchains have been bound by strict order. They must execute transactions sequentially, one at a time, to prevent attacks like the feared "double spend." Enforcing a limit of one transaction at a time is a sensible but primitive way to safeguard a ledger.

Solana's account model unlocks parallel transaction execution. In doing so, it also defines the developer experience. With every instruction processor you write and every frontend you connect, working with accounts will be your bread and butter.

This is the biggest conceptual difference between development on CosmWasm (and Ethereum) and development on Solana: how state is handled.

CosmWasm Smart Contract State Storage

As with Ethereum, on CosmWasm each contract instance has its own isolated key-value store. Think of this as the contract's own database, namespaced by the contract's address and writable only by the smart contract.

Other contracts cannot directly write to a contract's state – they must go through messages, and they must have write access as determined by the logic of the target smart contract.

This CosmWasm storage is accessed through deps.storage in the contract. Working with raw storage is uncomfortable, so CosmWasm provides high-level libraries like cw-storage-plus that wrap this storage with typed items, maps, and singleton patterns.

For example, a contract might define a CONFIG: Item and a BALANCES: Map like this:

// In state.rs
use cw_storage_plus::{Item, Map};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct Config { pub owner: Addr, pub max_tokens: u32 }
pub const CONFIG: Item<Config> = Item::new("config");
pub const BALANCES: Map<&Addr, Uint128> = Map::new("balance");

This means that, under the hood, the keys "config" and "balance" (with addresses as subkeys) will be used in the contract's KV store, allowing the contract to save and access typed state for each.

Many different kinds of types can be used as keys, subkeys, prefixes, and suffixes, and maps can contain other maps. The chain stores this data in the application state – usually RocksDB – associated with the contract.

During execution, a CosmWasm contract uses deps.storage to load, save, or update data. For example:

let mut cfg = CONFIG.load(deps.storage)?;
cfg.max_tokens = 1000;
CONFIG.save(deps.storage, &cfg)?;
BALANCES.update(deps.storage, &addr, |bal: Option<Uint128>| -> StdResult<_> {
    Ok(bal.unwrap_or_default() + amount_to_add)
})?;

The CosmWasm runtime charges gas proportional to data size and read/write ops, but developers do not lock minimum rent balances or allocate accounts for state. The state is auto-persisted by the VM after execution, but since it is not accessed by reference, your changes will only be committed if you remember to call .save() or .update().

Solana Accounts (Addressed Storage)

On Solana, program state lives in accounts.

The term "accounts" is more general on Solana than on Ethereum or CosmWasm, where it usually refers to user or smart contract accounts and may be associated with any arbitrary amount of account-specific data.

On Solana, accounts are distinct storage buffers identified by public keys. An account often just holds a single primitive type, such as a u64 or a Pubkey, but it can hold arbitrary data of a fixed length – as long as your program knows how to serialize and deserialize such data.

When you hear "account," don't think "some user's information." Just think "a place to store a piece of information."

A Solana program can only mutate the data of accounts that have been passed to it as writable accounts in the transaction and are owned by that program's ID. This means the client building the transaction must pass in any account that the program will use for storage. No global storage exists for programs, not even configuration settings: everything is stored in explicit "accounts."

Any accounts that will be used (one last time, this means "any storage that will be accessed") by the programs that are invoked must also be passed in. If your transaction calls program A, and it invokes program B, and program B accesses account x, then your transaction must include account x in its account array – even though program A does not access it directly.

This is how Solana achieves parallel executions and high transaction capacity: transactions can run alongside each other as long as no transaction reads or writes to accounts which are being written to in a simultaneous transaction. In other words, if a transaction writes to an account (to storage), that storage cannot be accessed in any way by another transaction until after the writing transaction is complete.

Got the key takeaway yet?: Solana accounts are distinct storage buffers, identified by public keys (addresses). This is the only way to store data in Solana – balances, addresses, configuration settings, etc.

For example, if you are implementing a token program in Solana, you would have at least these accounts:

  • one account for the token mint (holding mint info like supply, etc.)
  • separate accounts for each user's token balance (token accounts).

Your token program owns those accounts and can mutate their data, so its code determines how and under what conditions mutations occur. It is the gatekeeper of its accounts: if you request tokens to be decreased in your account and increased in someone else's account, the program will happily do so, as long as its logic determines you're allowed to make that request.

An account's data size is fixed at creation. The responsible account – which of course must be a signer of the transaction – pays refundable "rent" of 0.002 SOL for the account to reside in state storage.

! Rent: A legacy term from days when rent was periodically charged unless an account had enough balance to be "rent exempt." This is no longer true – all storage requires the minimum balance – but the term remains.

If the account's data is ever not needed – for example, a token account's balance is emptied to 0 – this can be refunded, as long as the refund address was set when the account was created.

In Solana code, a custom program might have a state struct like this:

#[derive(BorshSerialize, BorshDeserialize)]
pub struct Config { pub owner: Pubkey, pub max_tokens: u32 }

Say this is a staking pool program that you've written. Users, including but not limited to yourself, can create staking pools that use the program's logic. However, the pool configurations are not held in any kind of direct program storage – as they would be in CosmWasm, as a MapStructMappub struct Map<'a, K, T> { /* private fields */ }Click to view full documentation → of Configs keyed by something – but held in specific managed accounts.

Here, a user's client creates an account of the appropriate size in bytes and designates your staking pool program as its owner. Then, when calling your program's Initialize function, the client passes in that account. Your program deserializes and serializes the data as required – by calling Config::try_from_slice(&account.data.borrow())for example, and later config.serialize(&mut account.data.borrow_mut()[..]) before saving changes.

Only your program can modify account.data, according to its code, because the runtime checks that **account.owner == program_id**.

If another program tries to write to one of these accounts, or if your program tries to write to an account it doesn't own, the transaction will fail.

Typically, however, you don't want to force users or clients to make and manage all of the account addresses they'll use. In this case, you can use PDAs.

! Program Derived Addresses (PDAs): In Solana, a program can create "virtual" addresses that have no private key – they're off curve, meaning they mathematically cannot be cryptographically controlled. Yet, these addresses are known to be under a program's control, since their addresses are derived from the program's ID.

A PDA is generated from seeds + the program ID, and the runtime allows the program to sign on behalf of that PDA if and only if it was derived with the correct seeds and bump. bump is used to skip any derived address that randomly happens to be on curve.

This enables a program to create and manage accounts with deterministic addresses. So, an account like "vault account for user X" would be derived from [user_pubkey, "vault" seed, program_id].

Aside: CosmWasm has a design pattern that's analogous to PDAs, but it is not often needed. Smart contracts on CosmWasm do not derive new addressable state containers. Instead, they ask the chain to instantiate new smart contracts, which each have their own address and state.

These addresses can be deterministic (known in advance, before deployment), so contract factories can achieve PDA-like functionality in order to, for example, implicitly get user state contract addresses for new users and deploy them on demand.

In practice, though, since smart contracts have their own state and can store large amounts of cheap data within it – including entire lists of user balances and other data – per-user state containers are not very common in CosmWasm.

The Solana PDA mechanism lets Solana programs manage sub-accounts without needing private keys. It is heavily used for things like program-owned vaults, config accounts, etc. It's a crucial concept in Solana deveploment, and if you don't quite grok it yet, head over to Solana Stack Exchange to read some alternative explanations of PDAs.

Example 1: Associated Token Accounts

A common real-world example is how Associated Token Accounts (ATAs) work. One singlgit e ATA Program governs all token accounts for all tokens. Each address is derived from various inputs:

ata-derivation diagram (dark theme)

This shows how the ATA Program takes four components (Alice's wallet, the RUST token mint, the Token Program, and the ATA Program itself) to deterministically derive Alice's specific token account address. Each user gets a unique ATA for each token, but the addresses are predictable and don't require private keys.

Example 2: Many Counter Contracts vs. One Counter Program

Here's another example: The counter contract in CosmWasm stores an integer *count* in its storage and updates it on *ExecuteMsg::Increment*.

In order to have another counter, you typically create a new instance of the counter code – a new smart contract address. You could do this differently by having the counter contract hold a MapStructMappub struct Map<'a, K, T> { /* private fields */ }Click to view full documentation → of a bunch of addresses and counter values, but they would all still be held in the contract's state. Any further separation of contract logic and counter state would become impractical; it only makes sense to separate state and logic on CosmWasm in more complex situations.

cosmwasm-counter-instances diagram (dark theme)

On Solana, by contrast, state and logic are always separated. The counter program requires an account to hold the count value – the account's 8 bytes of data hold the u64 count. Users or apps pass in a dedicated account each time. The program will read/write the relevant account's data: there is no single "source of truth" for what the count is for the program (unless an account is somehow hardcoded into the program), just a source of truth for* count for a given account.

Another way to look at this is that the counter program can manage any arbitrary number of different counters, each of which is an account with its own address, without managing these addresses itself.

counter-pdas diagram (dark theme)

5. Managing Tokens & Native Assets

CosmWasm Native Tokens

On Cosmos chains, native currency – ATOM on Cosmos Hub, NTRN on Neutron, OSMO on Osmosis – "lives" in the bank module, not in contract storage. A CosmWasm contract cannot arbitrarily take user tokens unless the user explicitly sends tokens to it in a transaction, even if the user has signed.

Note: IBC tokens and Token Factory tokens act like native tokens – except that an IBC or Token Factory token may or may not be a valid gas fee token, depending on the chain's settings. This does not matter to your smart contract code, but it may matter to other parts of your stack. *CW20* tokens do not act like native tokens, but they are less and less common these days.

When a user executes a contract and includes attached funds, the MessageInfo.funds field in the contract's execute entry will contain those coins (as CoinStructCoinpub struct Coin { pub denom: String, pub amount: Uint128 }Click to view full documentation → types). The contract can then, for example, burn them or transfer them by emitting a BankMsgEnumBankMsgpub enum BankMsg { Send { to_address: String, amount: Vec<Coin> }, Burn { amount: Vec<Coin> } }Click to view full documentation → in its response.

Contracts themselves can hold native token balances and use them – the chain tracks balances for the contract's account, just as it tracks user balances for native tokens.

Again, the contract cannot directly debit someone else's account – it must use a Cosmos message (Bank transfer message) which the chain (bank module) executes if authorized. This generally means that the smart contract itself holds the funds, which were deposited as part of the transaction using the "deposit-and-spend" pattern. There is one lesser-known exception: *authz* permissions do allow spending on behalf of another account.

  • If a CosmWasm contract needs to send tokens: it returns a CosmosMsg::Bank(BankMsg::Send{to_address, amount}) in its ResponseStructResponsepub struct Response<T = Empty> { /* private fields */ }Click to view full documentation →. The Cosmos runtime processes this as a token transfer from the contract's balance to the target.
  • To receive tokens, a contract can specify that the caller should send funds, and can throw an error if the necessary funds are not attached to the message (as MessageInfo.funds). If funds are sent but not expected, the contract can decide to reject or accept them – they will just accrue in its balance if accepted.
  • CosmWasm also supports custom tokens via contract (CW20 tokens). A CW20 token is a contract that manages balances in its own state, similar to ERC20 on Ethereum. The standard CW20 contract stores a Map<Addr, Uint128> of balances. Transferring CW20 involves adjusting these state entries and possibly calling other contracts' hooks.
  • The recent Token Factory module has been added to most popular Cosmos chains, resulting in native custom tokens, issuable by chain users. CW20 has become less popular, and most assets today are native, factory, or IBC (native assets from other Cosmos chains moved over via Inter-Blockchain Communication). For many applications, special CW20 handling may not be necessary.

Solana (Native SOL)

Lamports (SOL units) are tracked in system accounts.

! System Accounts: Accounts owned by the System Program and governed by its logic, rather than by the logic of some other program. The System Program can only move lamports in system accounts, which has some implications. For example, only system accounts can pay transaction fees. Also, assigning the ownership of an accounts with lamports to another program may result in those lamports becoming irretrievable.

Every account – even program accounts – has a minimum lamport balance.

Like CosmWasm contracts, a Solana program cannot directly take lamports from a user's account. The user must sign a transaction that either:

  • transfers lamports to a program-owned account using a System Program instruction, or
  • passes in an account with lamports that the program can manipulate – for example, a PDA owned by the program.

Typically, to send SOL within a program, you invokeFninvokepub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResultClick to view full documentation → the System Program's transferFntransferpub fn transfer(from_pubkey: &Pubkey, to_pubkey: &Pubkey, lamports: u64) -> InstructionClick to view full documentation → instruction via CPI (see part 6) or as a separate transaction instruction.

SPL Tokens (fungible tokens on Solana)

Solana uses a system-wide token program (SPL Token) for fungible and NFT tokens. Instead of deploying a custom contract for each token (as with ERC20 or CW20), all standard tokens use the same program: the original Token or the extensible Token-2022.

A new "token mint" is created by making a mint account and calling the token program's mintFnmint_topub fn mint_to(token_program_id: &Pubkey, mint_pubkey: &Pubkey, account_pubkey: &Pubkey, owner_pubkey: &Pubkey, signer_pubkeys: &[&Pubkey], amount: u64) -> Result<Instruction, ProgramError>Click to view full documentation → instruction. Balances are not stored "in the mint account." Instead, each user has associated token accounts,which are also program-owned accounts, that store their balance for that mint.

This means that token balances live in separate accounts, not in a single contract's internal storage. All tokens on Solana are data accounts owned by the Token Program. The Token Program's code governs these operations and ensures that only authorized operations can be performed: transfer with correct signer, mint with mint authority, etc.

Example: Transferring Tokens

  • Cosmos SDK Native/IBC/Token Factory: use CosmWasm's "add message to response" pattern to append BankMsg::SendEnumBankMsg::SendSend { to_address: String, amount: Vec<Coin> }Click to view full documentation →. For example, to send 100 NTRN to an address addr:
Response::new().add_message(
  BankMsg::Send{
    to_address: addr,
    amount: coins(100_000_000, "untrn") // 6 decimal places
  }
)
invoke(
    &system_instruction::transfer(user.key, dest.key, lamports),
    &[user.clone(), dest.clone(), system_program.clone()]
)?;

This requires user to be a signer (so the transaction must include a signature). In Anchor, you'd include #[account(mut)] on the payer account and call anchor_lang::system_program::transfer.

  • SPL Token: Use the token program's instructions (via CPI or directly in the transaction): invokeFninvokepub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResultClick to view full documentation → the Token Program's TransferEnumTokenInstruction::TransferTransfer { amount: u64 }Click to view full documentation → instruction with the source_account, destination_account, and authority. The relevant token accounts must be writable. In Anchor, have an #[account(mut)] on the token accounts and call token::transfer(cpi_ctx, amount)?;.

We'll have numerous examples with more context in the next article's sample Solana/CosmWasm DEX.

TL;DR – CosmWasm contracts use high-level messages attached to the ResponseStructResponsepub struct Response<T = Empty> { /* private fields */ }Click to view full documentation → to move native tokens. Contracts usually cannot spend funds directly, so transactions requiring funds use the "deposit-and-spend" pattern. Solana programs send CPIs to the relevant program – System Program for SOL, or standard Token Program for SPL tokens.

6. Cross-Contract Calls and Composability (CPI vs Message Calls)

Blockchain platforms wouldn't be nearly as useful as they are without multi-program interaction.

DEXes, marketplaces, treasury management programs, user management, tokens, NFTs, and DeFi and social apps of all kinds are much more powerful when they can call each other during execution.

So, of course, both the Solana and the CosmWasm ecosystems allow contracts/programs to call one another. The mechanisms they use differ significantly.

CosmWasm Inter-Contract Messages

Contracts communicate by attaching messages to their response upon completion of execution. These messages can target other modules or contracts. When a CosmWasm contract returns a ResponseStructResponsepub struct Response<T = Empty> { /* private fields */ }Click to view full documentation → with messages or submessages (SubMsgStructSubMsgpub struct SubMsg<T> { /* private fields */ }Click to view full documentation →), the Cosmos runtime will execute those as either immediate sub-calls or separate transactions. For example, contract A can call contract B by emitting a WasmMsg::Execute:

let msg = to_binary(&OtherContractExecuteMsg::DoTheThing { param: 42 })?;
let exec = WasmMsg::Execute {
    contract_addr: other_contract.to_string(),
    msg,
    funds: vec![coin(100_000_000, "untrn")], // optional funds to send
};
return Ok(Response::new().add_message(exec));

Here, after A's execution, the chain will invoke B's execute(DoTheThing { param: 42 }) message. If DoTheThing errors, by default the whole transaction fails, and state is rolled back. (An exception is when CosmWasm's SubMsgStructSubMsgpub struct SubMsg<T> { /* private fields */ }Click to view full documentation → reply handling is used in a way that allows continued execution even on error.)

This model is akin to an actor sending a message "fire-and-forget" style – and possibly waiting for a reply. Instead of await-type functionality, a contract can feature callback functions accessible via its reply endpoint.

This flow is asynchronous from the contract's perspective: contract A's code doesn't directly get to use B's return value in the same scope, but it can react in a reply or, if no further action is needed, just trust that B was called. Any temporary state must be stored, since the reply entry point will be unaware of any "partial execution state." The reply call occurs within the same overall transaction/block, so it's not a separate user transaction, but it is managed by the chain, not by direct function calls.

In order to distinguish one reply from another, reply IDs can be specified when a SubMsgStructSubMsgpub struct SubMsg<T> { /* private fields */ }Click to view full documentation → is created. These IDs are passed on and can be parsed and understood by the reply endpoint so that execution proceeds appropriately. Enums make this easy to work with:

// in reply() entrypoint function...
match id {
    RETICULATE_SPLINES_ID => {
        // cool cool, we can have any post-success stuff here
    },
    OPEN_POD_BAY_DOORS_ID => {
        // denied? Well, we can have post-error behavior too
    },
    _ => result.map(|_| Err(ContractError::UnknownReplyId(id))),
}

Solana Cross-Program Invocations (CPIs)

! Cross-Program Invocations (CPIs): Invoke another program's instruction directly in the runtime context.

Solana programs can call into other programs by using the runtime's invokeFninvokepub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResultClick to view full documentation → or invoke_signedFninvoke_signedpub fn invoke_signed(instruction: &Instruction, account_infos: &[AccountInfo], signers_seeds: &[&[&[u8]]]) -> ProgramResultClick to view full documentation → functions, passing an InstructionStructInstructionpub struct Instruction { /* private fields */ }Click to view full documentation → and the accounts array. This is a synchronous call – program A calls program B and B executes fully (and returns) before A continues. Solana CPI acts like a normal function call.

However, both programs' account access must be specified up-front in the transaction, or the data needed by Program B won't be accessible to it. Program A cannot invoke program B on accounts that weren't provided to A originally – the runtime won't even have them available. CPI requires the transaction creator to anticipate all of the account needs of both A and B.

As covered in part 4, this clear delineation of accounts accessed in the whole transaction is what makes Solana's parallelization and speed possible: any given account can only be mutably involved in one transaction at a time.

Here's a simple example: Program A wants to transfer some SPL tokens as part of its logic. This will invokeFninvokepub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResultClick to view full documentation → the SPL Token or Token-2022 program's TransferEnumTokenInstruction::TransferTransfer { amount: u64 }Click to view full documentation → instruction:

let ix = spl_token::instruction::transfer(
    token_program_id,
    source_account,
    dest_account,
    authority,
    &[], // signers
    amount,
)?;
invoke(
    &ix,
    &[
        source_account_info.clone(),
        dest_account_info.clone(),
        authority_info.clone(),
        token_program_info.clone(),
    ],
)?;

This code calls the Token program to move tokens from source_account to dest_account under authority. If Program A needs to sign as a PDA for any reason, use invoke_signedFninvoke_signedpub fn invoke_signed(instruction: &Instruction, account_infos: &[AccountInfo], signers_seeds: &[&[&[u8]]]) -> ProgramResultClick to view full documentation → instead.

Anchor simplifies CPI with CpiContextStructCpiContextpub struct CpiContext<'_, '_, '_, 'info, T> { /* private fields */ }Click to view full documentation → and CPI functions provided by the Anchor crates of the other program. For instance, if program A has use anchor_spl::token; it can call token::transfer(cpi_ctx, amount) after constructing a context with accounts.

All CPIs execute in one transaction, so compute usage adds up. Each CPI does copying and checking, so it is wise to reduce the number of CPI calls made when possible. There is also a CPI call depth limit. Currently, this is 4, with a proposal ongoing to upgrade it to 8.

So, you can have contract A call B call C call D, but don't call E – and don't even call D if you want smart wallets to work with your program.

I'll provide more examples in the next section, the Interlude. But first, a quick summary:

TL;DR – Key Differences between CosmWasm Contract Calls & Solana CPIs

Contract/Program A calls Contract/Program B:

  • Synchronous vs Asynchronous: Solana CPI is synchronous (if B fails, A can catch the error immediately with a Rust ? result). CosmWasm contract calls via messages are asynchronous in flavor: if B fails and it wasn't caught via SubMsgStructSubMsgpub struct SubMsg<T> { /* private fields */ }Click to view full documentation → with reply_on_error, the failure will rollback the enclosing transaction, but A doesn't get a programmatic handle to the error except via an optional **reply** hook.
  • Account Passing: CosmWasm calls don't need the transaction to specify B's state at all; the chain fetches contract B's code and storage and runs it with the message. In Solana, the transaction has to include all accounts B will need, including B's program ID and any data accounts it will access.
  • Composability: Both platforms allow complex multi-contract workflows. CosmWasm can form a sort of transaction within a transaction: if you mark a message as a SubMsgStructSubMsgpub struct SubMsg<T> { /* private fields */ }Click to view full documentation → with reply_on_successEnumreply_on_successReplyOn::SuccessClick to view full documentation →, contract A will get a reply callback after B executes, allowing A to perform logic based on B's result, like an on-chain callback. Solana has no direct equivalent to a "reply callback," but it doesn't need one, since A can contain code that continues after B returns a Result from invokeFninvokepub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResultClick to view full documentation →.
cosmwasm-async-calls diagram (dark theme)
solana-sync-calls diagram (dark theme)

Example scenario: Suppose you want a contract that on execution triggers a swap on an AMM contract and then uses the result to do something else:

  • In CosmWasm, your contract A uses add_message to call an ExecuteMsg::Swap to contract B (an AMM), and then uses SubMsgStructSubMsgpub struct SubMsg<T> { /* private fields */ }Click to view full documentation → to get a reply with the swap results. In the reply handler, your contract then proceeds to send the obtained tokens to user C. This all happens in one transaction (one block), but with two contract executions. If B fails – say the pool has no liquidity – your contract's reply with an error can handle it or revert accordingly.
  • In Solana, your program calls invokeFninvokepub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResultClick to view full documentation → on the AMM program's instruction directly after crafting the AMM instruction, including pool accounts, etc. If this returns Err, handle it by returning an error from your program. If it returns OK, you now have the tokens in an account you control (which you passed in from the start), and you can then call another instruction if your program needs it. The initial transaction to your program must include both the AMM's accounts and the accounts to receive tokens. Multi-instruction sequences can also be done by the client as separate instructions in one transaction – first call the AMM, then call your program – but doing it as CPI means it's atomic as one call from the user's perspective, preventing the need for multiple signing events.

Interlude: A Full Token Transfer Example, with PDAs

Here's a full-program example in Anchor, with a simple token transfer (not using a PDA yet – we'll show the PDA next):

use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer};

declare_id!("autofilled");

#[program]
pub mod cpi_anchor_example {
    use super::*;

    /// A standard token transfer where the source authority is a Signer.
    /// This is the simplest case: the user owns the source account
    /// and signs the transaction.
    pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> {
        // Build the context to pass into the SPL Token program's `transfer`.
        let cpi_program = ctx.accounts.token_program.to_account_info();
        let cpi_accounts = Transfer {
            from: ctx.accounts.source_account.to_account_info(),
            to: ctx.accounts.dest_account.to_account_info(),
            authority: ctx.accounts.authority.to_account_info(),
        };
        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);

        // Execute the SPL token transfer with our context.
        token::transfer(cpi_ctx, amount)?;

        Ok(())
    }
}

/// Accounts context for a basic token transfer where
/// the source account authority is a Signer of the transaction
#[derive(Accounts)]
pub struct TransferTokens<'info> {
    /// The source token account (must be mutable to decrement balance).
    #[account(mut)]
    pub source_account: Account<'info, TokenAccount>,

    /// The destination token account (mutable to increment balance).
    #[account(mut)]
    pub dest_account: Account<'info, TokenAccount>,

    /// The authority that owns `source_account`; must be a signer.
    pub authority: Signer<'info>,

    /// The SPL Token program.
    pub token_program: Program<'info, Token>,
}

Using a PDA isn't too much more complicated, but I should demonstrate the creation of the PDA, too – so here's a longer example that transfers tokens which are not under the authority of the signer but under the authority of a PDA managed by the program itself:

use anchor_lang::prelude::*;
use anchor_spl::token::{
    self, Token, TokenAccount, Mint, Transfer, SetAuthority, spl_token,
};

declare_id!("autofilled");

#[program]
pub mod cpi_anchor_example {
    use super::*;

    /// Initializes a vault by assigning authority of a pre-created token account to a PDA.
    /// The PDA is derived from seeds: [b"vault", user.key].
    /// This lets the program control the vault using invoke_signed later.
    pub fn initialize_vault(ctx: Context<InitializeVault>, bump: u8) -> Result<()> {
        // Recalculate the expected PDA to verify it matches the provided vault_authority.
        let (expected_pda, _) = Pubkey::find_program_address(
            &[b"vault", ctx.accounts.user.key.as_ref()],
            ctx.program_id,
        );

        // This ensures users can't spoof an incorrect PDA.
        require_keys_eq!(
            expected_pda,
            ctx.accounts.vault_authority.key(),
            CustomError::InvalidAuthority
        );

        // Set the vault token account's authority to the PDA.
        // This allows the PDA to later sign CPI transfers.
        let cpi_program = ctx.accounts.token_program.to_account_info();
        let cpi_ctx = CpiContext::new(
            cpi_program,
            SetAuthority {
                account_or_mint: ctx.accounts.vault_token_account.to_account_info(),
                current_authority: ctx.accounts.user.to_account_info(),
            },
        );
        token::set_authority(
            cpi_ctx,
            spl_token::instruction::AuthorityType::AccountOwner,
            Some(ctx.accounts.vault_authority.key()),
        )?;

        Ok(())
    }

    /// Transfers tokens from a source account using a PDA as authority.
    /// The PDA must have been derived using the same seed logic as in `initialize_vault`.
    pub fn transfer_tokens_with_pda(
        ctx: Context<TransferTokensWithPda>,
        amount: u64,
        bump: u8,
    ) -> Result<()> {
        // The SPL checks this internally;
        // this line just demonstrates which keys need to be equal.
        require_keys_eq!(
            ctx.accounts.source_account.owner,
            ctx.accounts.pda_authority.key(),
            CustomError::InvalidAuthority
        );

        // These seeds must match exactly how the PDA was derived earlier
        let seeds = &[b"vault", ctx.accounts.user.key.as_ref(), &[bump]];
        let signer = &[&seeds[..]];

        // Build the CPI context for SPL Token transfer
        let cpi_program = ctx.accounts.token_program.to_account_info();
        let cpi_accounts = Transfer {
            from: ctx.accounts.source_account.to_account_info(),
            to: ctx.accounts.dest_account.to_account_info(),
            authority: ctx.accounts.pda_authority.to_account_info(),
        };

        // Use `invoke_signed` under the hood to sign as the PDA
        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
        token::transfer(cpi_ctx, amount)?;

        Ok(())
    }
}

/// Accounts required to initialize a PDA-controlled token vault.
#[derive(Accounts)]
pub struct InitializeVault<'info> {
    /// Pre-created token account that will be converted into a vault.
    #[account(mut)]
    pub vault_token_account: Account<'info, TokenAccount>,

    /// The PDA that will take authority over the vault_token_account.
    /// Must match `Pubkey::find_program_address(&[b"vault", user.key], program_id)`
    pub vault_authority: AccountInfo<'info>,

    /// The user creating and funding the token account initially.
    pub user: Signer<'info>,

    /// The token mint for the vault token account (optional use).
    pub mint: Account<'info, Mint>,

    /// SPL Token program
    pub token_program: Program<'info, Token>,
}

/// Accounts required to perform a transfer using a PDA as the authority.
#[derive(Accounts)]
pub struct TransferTokensWithPda<'info> {
    /// The source token account (must be PDA-owned).
    #[account(mut)]
    pub source_account: Account<'info, TokenAccount>,

    /// The destination token account to receive the tokens.
    #[account(mut)]
    pub dest_account: Account<'info, TokenAccount>,

    /// The PDA that owns `source_account`. Not a signer of the tx,
    /// but signs via `invoke_signed`.
    pub pda_authority: AccountInfo<'info>,

    /// The user whose public key was used in PDA derivation.
    /// Used to regenerate seeds.
    pub user: Signer<'info>,

    /// SPL Token program
    pub token_program: Program<'info, Token>,
}

#[error_code]
pub enum CustomError {
    #[msg("Provided PDA authority does not match derived address.")]
    InvalidAuthority,
}

You can validate PDAs directly in your Anchor program like this:

/// Explicitly derived PDA
#[account(
    seeds = [b"vault", user.key().as_ref()],
    bump
)]
pub vault_authority: AccountInfo<'info>,

However, the account address still must be passed in with the transaction, even if you verify in the program, since the runtime must know all of the accounts which will be accessed in order to parallelize transactions. The client cannot just punt address derivation: even if the PDA derivation is declared in the program using #[account(seeds = ..., bump)], the client must:

  1. compute the PDA address with the same parameters using findProgramAddressFnfind_program_addresspub fn find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> (Pubkey, u8)Click to view full documentation →, and
  2. provide it in the accounts list when building the instruction.

7. Serialization and Data Formats

Another difference to be aware of is how instructions are put together.

If you've worked with EVM, you're aware of call data and ABIs. A smart contract's existence on chain does not mean that the world knows how to talk to it: an Application Binary Interface provides the necessary information for communication.

CosmWasm Messages

CosmWasm uses JSON as the primary interface format for messages. InstantiateMsg, ExecuteMsg, and QueryMsg (as well as MigrateMsg, ReplyMsg, and SudoMsg) are typically annotated with serde Derive statements, nowadays included in the catch-all attribute #[cw_serde].

This newer attribute also includes #[serde(rename_all = "snake_case")], so that the case convention of EnumMembers and message_arguments can be followed in both contexts. An ExecuteMsg::ClearAllTribbles can be conveyed in JSON from the CLI, from a JavaScript client, or from elsewhere as clear_all_tribbles without causing any issues.

When a user calls a contract, the JSON message is delivered and automatically deserialized into the appropriate Rust Msg struct by the CosmWasm VM.

Note: Though most commonly used, JSON is not the most efficient. CosmWasm developers sometimes choose to serialize data differently (e.g., store binary data and use custom serialization). In some communities, *bincode* is standard practice. You can also store state by serializing to *borsh* then saving as bytes to avoid JSON overhead.

**Solana Instructions</span**>

The **instruction_data** is a binary array in a custom format. As mentioned elsewhere, developers often select borsh or bincode.

! Borsh: Binary Object Representation Serializer for Hashing, often used by Solana developers for instruction data and account data. Borsh is used by Anchor IDLs and in many programs by convention, but some Solana developers use other solutions like **bincode** or custom bit layouts.

Whatever is used must be deterministic, and both program and client must agree on it.

Numeric types and no_std limitations

Both CosmWasm and Solana are no_std, which influences the data types available to developers:

  • CosmWasm forbids f32/f64 completely – the VM will reject the contract if any floating point op is present. This means that you must use integer or decimal fixed-point types for any calculations. CosmWasm provides DecimalStructDecimalpub struct Decimal(#[doc(hidden)] pub Uint128);Click to view full documentation → and Decimal256StructDecimal256pub struct Decimal256(#[doc(hidden)] pub Uint256);Click to view full documentation → in cosmwasm_std for high precision fixed-point needs. One common pitfall is that CosmWasm also disallows usage of usize/isize in any exported struct, such as messages or any type used in a message, since serialization of these can invoke floats in serde. Developers use u64, u128 etc. instead. The JSON encoder in CosmWasm also has to handle larger unsigned integers as strings due to JSON number limits (2⁵³) , so CosmWasm provides Uint128StructUint128pub struct Uint128(#[doc(hidden)] pub u128);Click to view full documentation → and Uint256StructUint256pub struct Uint256(#[doc(hidden)] pub [u64; 4]);Click to view full documentation → to ease working with large numbers and automatically provide the necessary serialization workarounds.
  • Solana BPF also does not support floats – the LLVM BPF target omits them. Just as in CosmWasm, you cannot do floating point math on-chain. Use integer math or the provided PreciseNumberStructPreciseNumberpub struct PreciseNumber { value: U256 }Click to view full documentation →, a U256 wrapper allowing precise math on u128-size numbers. If you ever manually manipulate bytes for numbers in Solana, Solana BPF is little-endian.

Developer Ergonomics:

  • In CosmWasm, working with JSON means you can easily print your messages as JSON and write messages in-line on CLI. The chain can also return JSON query results that are human-readable. This does have a performance cost, since parsing or serializing JSON is slower and bigger. For data-heavy contracts containing millions of entries, other formats can save appreciable resources. You can see examples of other serialization formats in use in this article.
  • In Solana, you'll typically inform your client how to serialize instructions, if you're not using Anchor and IDL. For debugging, you will rely on unit tests, scripts, or tools, since it's impractical to craft binary-encoded instructions in-line.

8. Standard Library, Crates, and Environment

Both CosmWasm and Solana operate in a stripped down environment – no OS, no filesystem.

Memory and Allocations

CosmWasm contracts have a linear memory managed by the VM. You can use Vec, String, etc., as the alloc crate is provided.

Solana's BPF has an allocator as well, provided by the runtime. However, recursion and large stack usage are limited, and deep recursion can fail.

Neither environment has threading, dynamic memory beyond what's available, or things like networking, of course.

Crates and Compatibility

Many Rust crates support no_std or have features to toggle off std.

CosmWasm's cosmwasm-std is the go-to for all essentials – it provides AddrStructAddrpub struct Addr(String);Click to view full documentation → (a wrapper around human-readable addresses), Math (see the next point) and various utility functions. It also provides wrappers for results and errors such as StdResultTypeStdResultpub type StdResult<T> = Result<T, StdError>Click to view full documentation →.

Solana's SDK, which used to be the monolithic solana_programCratesolana_programFrom crate: solana-programClick to view documentation → crate but is now broken into focused breakout crates like solana_pubkeyCratesolana_pubkeyFrom crate: solana-pubkeyClick to view documentation →, solana_instructionCratesolana_instructionFrom crate: solana-instructionClick to view documentation →, solana_account_infoCratesolana_account_infoFrom crate: solana-account-infoClick to view documentation →, solana_sysvarCratesolana_sysvarFrom crate: solana-sysvarClick to view documentation →, and others, provides account types, system instructions, and logging macros. Anchor provides anchor_langCrateanchor_langFrom crate: anchor-langClick to view documentation → and anchor_splCrateanchor_splFrom crate: anchor-splClick to view documentation → for a higher-level interface and common patterns. Rust's efficiency is sufficient for well-built external libraries to be used in contract/program code without bloating size too much, as long as they support no_std and can avoid things like floats.

Math Support

CosmWasm's cosmwasm-std provides Uint128StructUint128pub struct Uint128(#[doc(hidden)] pub u128);Click to view full documentation → and Uint256StructUint256pub struct Uint256(#[doc(hidden)] pub [u64; 4]);Click to view full documentation →. Since using Rust u128 in JSON would be problematic, these custom types serialize as a string. It also provides DecimalStructDecimalpub struct Decimal(#[doc(hidden)] pub Uint128);Click to view full documentation →.

Similarly, Solana gives you PreciseNumberStructPreciseNumberpub struct PreciseNumber { value: U256 }Click to view full documentation → – a U256 wrapper which allows precise math on u128 numbers.

Logging/Debug

CosmWasm doesn't have a direct println!– there is nowhere to print to– but it can log events. For production logging, a contract can attach attribute key value pairs to the ResponseStructResponsepub struct Response<T = Empty> { /* private fields */ }Click to view full documentation → which get persisted in the transaction result and can be viewed in block explorers or via queries. This is used for important events, such as transfer events with sender/receiver/amount. For debugging, you might temporarily include debug info in events, as well. deps.api.debug(...) is also available and logs to the node console, but this only works on testnets, where debug logs are enabled.

Solana has the msg!() macro from solana_program that prints to the transaction log, which can, like CosmWasm attributes, be viewed via explorer or CLI. For example, msg!("current count: {}", count); will show up in tx logs. Logging does cost compute units, and both systems limit how much you should log. In varying test cases, you'll see debug_msg, println!, or eprintln! used to output information in contexts other than mainnet.

Randomness

Neither environment has access to random numbers by default. Some programmers use blockhash as a source of entropy, but this is bad practice and prone to manipulation, with the possible exception of the occasional harmless use case: for example, a block-based random color in a game without value to be extracted.

But when randomness is required for economic soundness, it can easily be achieved with 1) an oracle contract that pushes in randomness, 2) random number modules such as those provided by a connection to Secret Network, or most commonly 3) commit-then-reveal randomness schemes, where all parties commit hashed entropy and then reveal the plaintext.

**std** library polyfills

CosmWasm's cosmwasm-std and cosmwasm-schema help to make development feel somewhat like normal Rust by providing necessary data structures and removing std-only parts. For instance, cosmwasm-std re-implements some things for no_std, and it re-exports serdeCrateserdeA framework for serializing and deserializing Rust data structuresClick to view full documentation → for serialization without floats.

Solana's SDK similarly provides stuff like solana_program::keccak for hashing, and Solana programs regularly use bytemuckCratebytemuckA crate for mucking around with piles of bytesClick to view full documentation →, borshCrateborshBinary Object Representation Serializer for HashingClick to view full documentation →, etc., which all support no_std.

Assertions

Both frameworks advise againt panic events in the code proper, though they allow it for the purpose of failing tests.

In CosmWasm, the best practice is to use the anyhow::ensure!Macroensure!macro_rules! ensure { ($cond:expr, $msg:expr) => { /* macro expansion */ } }Click to view full documentation → macro, now standard in CosmWasm libraries; this throws a specified Err if the provided equality is false. For example:

ensure!(
  amount > 0,
  ContractError::InvalidAmount {}
);

In Solana, Anchor provides the similar require!Macrorequire!From crate: anchor-langClick to view documentation → macro, returning a ProgramErrorEnumProgramErrorpub enum ProgramError { InvalidArgument, Custom(u32), /* ... */ }Click to view full documentation → if the condition is false. For example:

require!(
  amount > 0,
  CustomErrorCode::InvalidAmount
);

Larger Data Structures

CosmWasm discourages large structs in state if not needed – it's better to break into smaller ItemsStructItempub struct Item<T> { /* private fields */ }Click to view full documentation →/MapsStructMappub struct Map<'a, K, T> { /* private fields */ }Click to view full documentation →. However, CosmWasm's lower chain usage easily accommodates growable collections.

Solana requires you to allocate a fixed size for each account upfront, so you often define a struct and ensure it's tightly packed with #[repr(packed)], or just by field order, as Borsh does. If you want a growable collection in Solana, you might use a Vec in an account, but you must specify a max or use the account's entire data as a vector – and enforce size. In many cases, it's better to use multiple accounts.

Transaction Expiration

Solana transactions, with the exception of the soon-to-be-deprecated Durable Nonce transactions, must include a recent blockhash – something within the last 2 minutes. This imposes certain UX restrictions.

Cosmos SDK chains do have the concept of expiration, but this is not enforced by the protocol to be within a certain range: nothing stops a user from setting a transaction to expire in 10,000 years. However, like Ethereum transactions, Cosmos transactions typically have a nonce called sequenceConceptsequencepub sequence: u64Click to view full documentation → in order to prevent attackers from submitting the same signed transaction repeatedly.

Miscellaneous Tips

CosmWasm provides deps.api.addr_validate() to validate bech32 addresses into AddrStructAddrpub struct Addr(String);Click to view full documentation →. Solana programs might use Pubkey::find_program_address for PDAs or Pubkey::create_program_address (which actually computes the PDA and errors if on-curve).

Both Solana and CosmWasm restrict you to a deterministic, sandboxed subset of Rust.

CosmWasm feels more like a cushioned environment with many utilities, the Cosmos SDK modules directly at your disposal via messages, and growable collections.

Solana is more bare-metal – you're closer to the raw bytes of your accounts and instructions – but Anchor and other emerging frameworks offer conveniences that bring the same higher-level experiences.

9. Error Handling and Safety Patterns

Novice developers sometimes rely on unwrap() or even panic! to handle error cases, but CosmWasm and Solana both recommend against these and provide safe and transparent alternatives.

And no,

CosmWasm's ContractError

Define a ContractError enum for your contract, with a derive attribute pointing to thiserror::Error. Each variant in this enum can correspond to a user-facing or internal error:Unauthorized, InsufficientFunds, etc. Returning an Err(ContractError::Foo {}) from an entry point will cause the transaction to rollback and return that error string to the user.

CosmWasm uses Result<Response, ContractError> for executes (which can also be stylized as StdResult<Response>) and Result<<!--DOCSLINK70-->, ContractError> for queries. Because the error is your custom type (which implements Display), the chain will include the error message in the transaction log if it fails. For assertions, you can either use Rust's assert! (which will panic – the VM will catch it and also turn it into an error, but without a nice message) or more properly return a ContractError. Here are some common patterns:

if info.sender != state.owner {
  return Err(ContractError::Unauthorized {});
}

or...

ensure!(
  info.sender == state.owner,
  ContractError::Unauthorized {}
);

This way, you avoid panicking and can provide informative error strings. Errors can take parameters:

return Err(ContractError::Unauthorized {
  reason: "I am sorry Dave".to_string()
});

which your enum can use to format pretty (or even localized) error messages:

use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
    #[error("{0}")]
    StdError(#[from] StdError),
    #[error("{reason}, I cannot do that")]
    Unauthorized { reason: String },
}

Solana's ProgramError

Solana programs use ProgramResultTypeProgramResultpub type ProgramResult = Result<(), ProgramError>;Click to view full documentation →, which is just Result<(), ProgramError>, very similar to CosmWasm's StdResult. This ProgramErrorEnumProgramErrorpub enum ProgramError { InvalidArgument, Custom(u32), /* ... */ }Click to view full documentation → is an enum of general errors such as InvalidArgument, Custom(u32), etc.

For custom errors, define an enum and implement From<YourError> to ProgramError, mapping to one of the Custom codes, and then implement PrintProgramError for nice logs. In my opinion, you should avoid mapping any custom condition to ProgramError::InvalidArgument or a similar "catch-all" error except as a stopgap during development, as it can make debugging much harder when every class of error throws InvalidArgument.

Solana (Anchor)

Anchor auto-generates an ErrorCode for your custom errors if you use the #[error_code] macro (see Custom Errors). You define an enum like:

#[error_code]
pub enum MyError {
    #[msg("Not enough funds to do X")]
    NotEnoughFunds,
    #[msg("Unauthorized action")]
    Unauthorized,
}

Each variant gets an autogenerated code, distinct from system errors. Anchor's require! macro works with these, so require!(condition, MyError::NotEnoughFunds); will return an Err(ProgramError::Custom(code)) behind the scenes, with the code for NotEnoughFunds.

Anchor also provides the require_eq! and require_keys_eq! macros for common checks. This is analogous to assert in Solidity or CosmWasm, but it produces a proper error the client can parse via the Anchor IDL or error mapping.

Messages and Events on Error

In CosmWasm, if you return an error, all state changes and messages are discarded (transaction rollback).

The same is true in Solana – an error aborts the transaction (or at least the instruction).

One difference, though: in CosmWasm, a contract can handle sub-message errors and not propagate them, by using reply_on_error in SubMsgStructSubMsgpub struct SubMsg<T> { /* private fields */ }Click to view full documentation →). In Solana, if a CPI returns an error and you catch it (not using ? but a manual match), you can decide to proceed or not. But typically, you propagate errors up. For more information see part 6 above.

What happens when you panic?

You should avoid panic! and unwrap() in both frameworks. CosmWasm will trap a panic and turn it into an generic error: panic, which is not user-friendly. Solana BPF will abort on panic – which becomes a generic ProgramError::Custom(0x0) or some unspecified error.

Always, always, always return errors instead.

Quick win snippets:

  • Asserting ownership:

CosmWasm: if info.sender != owner { return Err(ContractError::Unauthorized {}); }

Solana native: if *signer.key != owner_pubkey { return Err(ProgramError::IllegalOwner); } (or a custom error).

Solana Anchor: require!(signer.key() == owner_pubkey, MyError::Unauthorized);

  • Checking amount > 0:

CosmWasm: if amount.is_zero() { return Err(ContractError::InvalidAmount{}); }

Solana native:if amount == 0 { return Err(ProgramError::InvalidArgument); }

Solana Anchor: require!(amount > 0, MyError::InvalidAmount);

Error Visibility

Anchor errors show up in the transaction logs (e.g., "AnchorError occurred: NotEnoughFunds"), and correctly managed Solana errors can show up as well.

CosmWasm errors will show up, as well, as something like "Unauthorized: sender is not owner" – as long as you write the appropriate Display message in your enum.

10. Time, Block Information, and Randomness

Smart contracts often need to know the current time or block height for things like unlocking tokens, opening mints, calculating staking rewards, and so on.

The two ecosystems handle this via contextual data in different ways:

CosmWasm: The contract's EnvStructEnvpub struct Env { pub block: BlockInfo, pub transaction: Option<TransactionInfo>, pub contract: ContractInfo }Click to view full documentation → (environment) passed to entry points includes env.block.height and env.block.time, usually the blockchain's Unix TimestampStructTimestamppub struct Timestamp(Uint64)Click to view full documentation →. Cosmos chains use "BFT time," which is loosely the wall-clock time set by validators. This isn't perfectly precise, but it doesn't need to be when block times are slightly longer, as it increases per block. This TimestampStructTimestamppub struct Timestamp(Uint64)Click to view full documentation → type has nanosecond precision internally, but most chains use only seconds.

If you're checking a deadline, expiration time, or start time, compare env.block.time against your target time. For example:

if env.block.time < Timestamp::from_seconds(start_time) {
  return Err(ContractError::NotStarted {});
}

Other less commonly used information in envStructEnvpub struct Env { pub block: BlockInfo, pub transaction: Option<TransactionInfo>, pub contract: ContractInfo }Click to view full documentation → includes env.block.chain_id and env.transaction. env.block.height strictly increases by 1 each block, so it's often used as a replacement for time.

CosmWasm's EnvStructEnvpub struct Env { pub block: BlockInfo, pub transaction: Option<TransactionInfo>, pub contract: ContractInfo }Click to view full documentation → cannot be manipulated by the contract and is safe from tampering.

Some Cosmos zones provide env.block.random, but this is uncommon, and contracts which do not use this sort of randomness source either rely on an randomness oracle or multi-party commit/reveal schemes.

Solana: Solana provides sysvars, which are accounts that the runtime populates with data like clock, rent, etc. (Solana Sysvars Explained – RareSkills).

! Sysvars: Accounts that the Solana runtime populates with data like clock, rent, etc. (Solana Sysvars Explained – RareSkills).

The **Clock**sysvar (SysvarClock) includes:

  • unix_timestamp – the estimated Unix timestamp of the current slot (cluster time).
  • slot – current slot number.
  • epoch – current epoch.

! Slot: The basic unit of time in Solana. This is a short window (~400ms) in which a validator can propose a block. Not every slot produces a block, but each has a designated leader.

! Epoch: A collection of slots. It's a longer period during which the validator schedule is fixed. Epochs are used for staking rewards, updating validator roles, and reconfiguring network parameters.

Accessing Sysvars

A program using solana_program crates can access sysvars such as Clock in two ways:

1. Directly via **Sysvar::get** – Solana's BPF can fetch certain sysvars without them being passed in, and Clock is one of these. For example, this retrieves clock information via a syscall:

let clock = Clock::get()?;
msg!("current time: {}", clock.unix_timestamp);

2. Via account parameter – The transaction can include the Clock sysvar account (SysvarC1ock11111111111111111111111111111111) as a read-only account. In your program, you can then refer to the account:

let clock_account = next_account_info(accounts_info_iterator)?;
let clock = Clock::from_account_info(clock_account)?;

Using pinocchio – For more compute-efficient account access, pinocchioCratepinocchioFrom crate: pinocchioClick to view documentation → provides lightweight and sometimes unsafe/unchecked alternatives to solana_program crates, often reducing compute units significantly:

// Account SysvarC1ock11111111111111111111111111111111 is included
// in the transaction as a non-writeable account.

// SAFETY: accounts must not be empty.
let clock_account = unsafe { accounts.get_unchecked(0) };
// SAFETY: caller must ensure it is safe to borrow the account
// (i.e. there are no mutable borrows of the account data).
// `from_account_info_unchecked` ensures that the account's ID
// is the Clock program ID.
let clock = Clock::from_account_info_unchecked(clock_account)?;

Using Anchor: Anchor automatically provides Clock if you include #[account(address = sysvar::clock::ID)] clock: AccountInfo<'info> or use the Clock type in context accounts.

The Solana unix_timestamp is roughly UTC time. (On devnet/localnet, it might start at 0 from genesis.) It is **not strictly increasing each slot – **it can drift between slots, even though it generally increases. Thus, for time-locked operations, we often use slot or epoch numbers for deterministic progress. Or, we rely on the timestamp with a bit of tolerance.

Delay and scheduling: Neither CosmWasm nor Solana has a built-in scheduler to wake up contracts at a future time – a transaction must be sent. Cosmos SDK does have modules available like cron on some chains, or a contract can always be called by an external keeper. Solana has no native cron – on-chain programs like Switchboard or Cronos exist to trigger at specific times.

Block height vs. Slot: CosmWasm's height is equivalent to Solana's slot in purpose (incremental counter of block-like units). But Solana's slot is more granular. An epoch is a larger unit (many slots).

Example usage:

  • CosmWasm: a vesting contract stores an unlock_time as a TimestampStructTimestamppub struct Timestamp(Uint64)Click to view full documentation →. Then, each execute that is attempting to withdraw checks if env.block.time >= unlock_time.
  • Solana: a vesting program stores an unlock_slot or unlock_timestamp. Then, in the instruction:
let clock = Clock::get()?;
require!(clock.unix_timestamp >= unlock_ts, Error::NotUnlockedYet);

Trusting time: In Cosmos, env.block.time is provided by validators and generally should not be wildly incorrect, but could be a bit off depending on block delays. In Solana, unix_timestamp is derived from validator time and is usually within some bound of real time, but not strictly guaranteed, and can jump under certain conditions. For strict control, such as financial scenarios, block height or slot can be safer as a predictable counter.

11. Testing: Frameworks and Approaches

Testing smart contracts fully requires simulating the blockchain.

The developer experience here is quite different between CosmWasm and Solana.

CosmWasm: Because CosmWasm contracts are pure functions (WASM) with a well-defined API, you can compile and execute them in a local Rust environment using provided helpers. CosmWasm offers a cosmwasm_std::testing module which allows an isolated contract to act as if it is running on a live network with blocks and users.

Contract-to-contract interaction, however, requires a tool such as the [**cw-multi-test**](https://book.cosmwasm.com/basics/multitest-intro.html) framework.

Unit tests: You can call a contract's instantiate, execute, and query functions directly in Rust tests as regular functions. The cosmwasm_std::testing::mock_* utilities help create a dummy DepsStructDepspub struct Deps<'a> { pub storage: &'a dyn Storage, pub api: &'a dyn Api, pub querier: QuerierWrapper<'a> }Click to view full documentation → (dependencies), EnvStructEnvpub struct Env { pub block: BlockInfo, pub transaction: Option<TransactionInfo>, pub contract: ContractInfo }Click to view full documentation →, and MessageInfoStructMessageInfopub struct MessageInfo { pub sender: Addr, pub funds: Vec<Coin> }Click to view full documentation →. For example:

use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
#[test]
fn test_initialization() {
    let mut deps = mock_dependencies(); // in-memory storage, mock API
    let env = mock_env();
    let info = mock_info("creator", &[]);
    let msg = InstantiateMsg { count: 0 };
    let res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap();
    assert_eq!(0, query_count(deps.as_ref()).unwrap().count);
}
  • This test doesn't spin up a chain – it just uses the contract logic directly. This is fast and good for most logic testing, and may even prove sufficient for single-contract systems that don't need to interact with other contracts. You can simulate different sender actors, attach funds in mock_info, and set custom block times, allowing you to test 1-year lockups, for example.
  • Multi-contract tests: With cw-multi-testCratecw-multi-testA testing framework for multi-contract CosmWasm environmentsClick to view full documentation →, you can simulate an environment with multiple contracts and real message passing. MultiTest provides a struct that acts like a chain, where you can instantiate multiple contracts (even by uploading WASM bytecode directly) and execute messages that may call each other. It handles the queues of messages, allows asserting on events which are emitted by contracts during the course of execution, and still provides simulated block progression, allowing you to increment block height/time between calls. This an on-chain integration test environment, but all in Rust. It's extremely useful for testing how your contract interacts with standard contracts such as multisig, token, or DEX contracts – or your own contracts in a custom multi-contract setup – without the hassle and latency of deploying to a real testnet. You can also effectively test the reply and migrate endpoints using MultiTest.
  • Integration tests on a real local chain: Finally, you can run a local node and use CosmJS or CLI scripts to store code, instantiate, execute, and then assert state. This is heavier, but required for maximum test effectiveness and for some features such as IBC. Some ecosystems provide dockerized or GitPod-style environments to help you easily set up a testing environment. Tests here (and on live testnets and mainnets) can be automated with great effectiveness using a framework such as Mocha – allowing you to incorporate full testing into your CI/CD flows.

Solana: Testing Solana programs can be done in a few ways:

  • **solana_program_test** (the old way): The Solana SDK provides the solana_program_test crate which can spin up a local in-memory Solana bank that behaves like a validator, allowing you to execute transactions against your program. You can think of this like a one-node cluster running in your test process. With it, you can simulate sending transactions, invoking your program, and reading account states. However, it's slow and inflexible and is no longer recommended.
  • MolluskSVM: the recommended testing and benchmarking tool for serious Solana developers. I might add it to this article and the others in the series shortly.
  • LiteSVM: this in-process Solana VM is much faster at running and compiling than alternatives like the above solana-program-test and solana-test-validator. I like the snarky README: "In a further break from tradition, it has an ergonomic API with sane defaults and extensive configurability for those who want it." LiteSVM lets you compose easy Rust language tests and easily perform any setup you need to do, for example:
use litesvm::LiteSVM;
use solana_keypair::Keypair;
use solana_message::Message;
use solana_pubkey::Pubkey;
use solana_signer::Signer;
use solana_system_interface::instruction::transfer;
use solana_transaction::Transaction;

let from_keypair = Keypair::new();
let from = from_keypair.pubkey();
let to = Pubkey::new_unique();

let mut svm = LiteSVM::new();
svm.airdrop(&from, 10_000).unwrap();

let instruction = transfer(&from, &to, 64);
let tx = Transaction::new(
    &[&from_keypair],
    Message::new(&[instruction], Some(&from)),
    svm.latest_blockhash(),
);
let tx_res = svm.send_transaction(tx).unwrap();

let from_account = svm.get_account(&from);
let to_account = svm.get_account(&to);
assert_eq!(from_account.unwrap().lamports, 4936);
assert_eq!(to_account.unwrap().lamports, 64);

We use LiteSVM in the Dopple DEX we build in the next few chapters.

  • Anchor testing (with Mocha or Rust): If using Anchor, the best option may be to write tests in TypeScript using the Anchor framework (which provides a nice API to call Rust functions as if they were RPC methods). Anchor sets up a local validator (similar to program test) when you run anchor test. You write tests that create KeypairsStructKeypairpub struct Keypair(/* private fields */)Click to view full documentation →, call your program's instructions via the Anchor generated client, and assert results. This is high-level and convenient if you're comfortable with JS/TS for testing.
  • Solana CLI or localnet: For integration, you can run a local Solana validator (e.g., solana-test-validator) and use the CLI or RPC calls to deploy and test your program, or use third-party frameworks (like Neodyme's Loki). But for most, program_test or Anchor cover the needs.

To Bytecode Or Not To Bytecode

  • With CosmWasm, you can test most logic without compiling to Wasm, since you call the Rust directly. However, this means you won't catch errors such as rogue F64 instructions in WASM – these will be detected once you're testing on an actual local, test, or main network. In Solana, to use program_testCrateprogram_testA testing framework for Solana programsClick to view full documentation →, you need the BPF compiled. However, you can use the processor! macro to call the Rust process_instruction function directly for simpler tests. This is somewhat equivalent to calling CosmWasm logic directly, but still happens via BanksClientStructBanksClientpub struct BanksClient { /* private fields */ }Click to view full documentation →.
  • Error handling and logs can be tested: CosmWasm unit tests can catch ContractError by using .unwrap_err() in Rust. Solana program_testCrateprogram_testA testing framework for Solana programsClick to view full documentation → will give you a TransportError if a transaction failed; you can decode to TransactionError::InstructionError to get the ProgramErrorEnumProgramErrorpub enum ProgramError { InvalidArgument, Custom(u32), /* ... */ }Click to view full documentation → and check the code.

12. Tooling and Developer Workflow

CosmWasm Project Setup

After ensuring you have Rust installed with the wasm32-unknown-unknown target, you typically start with a template (e.g., cw-template via cargo generate).

This gives you a ready-to-go contract structure with:

  • src/contract.rs (entry points),
  • src/msg.rs (message types),
  • src/state.rs (state structs), and
  • tests.

You can use cargo to build the Wasm (cargo wasm) and Rust tests (cargo test).

To deploy, you use a chain's CLI or REST API. There isn't a single all-in-one CLI for CosmWasm itself; you use whatever client the chain provides (e.g., neutrond or osmod) to upload and instantiate contracts. However, the CLI program commands are generally equivalent among clients, except for chain-specific functionality.

For local testing, you can use MultiTest or a local network as mentioned in part 11.

Solana Project Setup

If using Anchor, anchor init sets up a project with an Anchor.toml, program code, and a TypeScript test scaffold. Anchor provides you these main commands:

  • anchor build (compiles BPF with the right toolchain),
  • anchor deploy (deploys to cluster specified in Anchor.toml, often localnet for tests), and
  • anchor test (runs tests).

Without Anchor, you manage a standard Cargo project but with target BPF. Install Solana's BPF toolchain and Solana's LLVM if needed. The easiest path here is the install script provided by Anza, but other options are available as well.

The Solana CLI is as extensive as Cosmos SDK CLIs, including key management, account inspection, and more.

Build and CI

In CosmWasm, you run tests with the standard cargo test. You might run cargo clippy and cargo fmt as usual. Then, use a dockerized WASM optimizer to prepare your contract for on-chain use; containerized optimizers ensure that your resulting binary can be verified by other parties. The command for optimizing requires installing Docker ahead of time and looks something like this, but you should search for the latest recommendations for your chain:

docker run --rm -v "$(pwd)":/code \
  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \
  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
  cosmwasm/optimizer:0.16.0

Workspace optimizers, designed for optimizing multi-contract workspaces with multiple WASM files at once, are also available.

On Solana, you simply run cargo build-sbf (or anchor build), which uses the Solana SDK's BPF tools. No further optimization is required.

Linting and Safety

As mentioned previously, CosmWasm has to ensure your code has no floating point operations. Depending on your workflow, this usually won't alert you until the WASM gatekeeper module prevents you from storing code on chain – rather late in your development timeline. The most common culprits here are getrandom and serialization of usize.

Solana's BPF target will just not link any unsupported stuff, so if you accidentally bring in a std lib that's not allowed, it will likely outright fail to compile.

Deployment & Network

For CosmWasm, after writing and testing, you need to pick a chain (e.g., a testnet or localnet). Each chain might have different parameters (gas costs, supported CosmWasm version). You upload the code and get a code ID. Then instantiate it with an admin or not, with initial state. This can be done with a single CLI command or a script.

For Solana, use anchor deploy or solana program deploy.

On devnet/mainnet, you need an upgrade authority keypair with enough SOL to lock for rent, as well as a minimal deploy fee. anchor deployautomates a lot of this work, once configured.

CLI Interactions

Both CLIs use a subcommand style syntax. In CosmWasm, for example:

neutrond tx wasm execute <contract_address> \
'{"do_something":{...}}' --from wallet

In CosmWasm, since messages are JSON, you can often use CLI or REST directly for simple interactions. For more complex interactions, setting message strings as environment variables or using shell scripts is best.

The Solana CLI equivalent is:

solana program invoke <PROGRAM_ID> \
[accounts...] [instruction data in hex]

This is not nearly as user-friendly – since the account list is often quite long and the instruction data is in hex format – so you usually write a client script or use IDL to create instructions.

Ecosystem Libraries

CosmWasm has several libraries, including:

  • cw-plus (a set of standard contracts like cw20, cw721, multisig, etc., that you can use or reference),
  • cw-utils for common patterns (e.g., expiration handling, payment assertions), and
  • various community crates, like cw-storage-plus, which we discussed.

There are also front-end libraries like CosmJS to easily call contracts from a web app.

Solana has the SPL (Solana Program Library), which includes tested and deployed standard programs for tokens, staking, etc., that you often integrate with rather than rewrite or redeploy. Another notable source is the Metaplex libraries for compressed NFTs.

Learn More: Documentation and References

CosmWasm's official docs (the "CosmWasm Book") and the Cosmos SDK docs cover the basics and advanced topics like IBC, testing, etc. Many Cosmos SDK chains also maintain their own educational materials, from Osmosis's documentation to Archway's Area 52 Academy.

For Solana, the official Solana docs and the Solana Cookbook are great places to start. Anchor has its own doc site with examples and explanations of each macro. After those sources, the best approach is to jump straight to reading code, especially the Solana SPL docs and code on GitHub.


By understanding these key differences, you can navigate both CosmWasm and Solana environments.

Despite the differing philosophies of the two ecosystems – CosmWasm’s multi-chain WASM vs Solana’s ultra-optimized single-chain BPF – as a Rust engineer, you’ll find your skills transferable.

As long as you're willing to put in the practice time. In the next (shorter) article, we start building a DEX called "Dopple DEX," which implements the same features in CosmWasm and in Solana.

Telegram| LinkedIn| Anza Labs| Email Obi.money Temper