Deploying Smart Contracts in Cosmos (CosmWasm)
CosmWasm is not EVM. If you come from a Solidity background, the first thing that's confusing: contracts are written in Rust, compiled to WebAssembly, and interact through message-passing rather than direct function calls. The architectural model is closer to actor model than to object method calls. This is important to understand before writing the first line of code.
Development Environment and Toolchain
To work with CosmWasm, you need Rust with the wasm32-unknown-unknown target:
rustup target add wasm32-unknown-unknown
cargo install cosmwasm-check # validation of compiled wasm
cargo install cargo-generate # project templates
Standard project template via cw-template:
cargo generate --git https://github.com/CosmWasm/cw-template.git --name my-contract
CosmWasm Contract Structure
A contract consists of three mandatory entry points:
// src/contract.rs
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> { ... }
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> { ... }
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(
deps: Deps,
env: Env,
msg: QueryMsg,
) -> StdResult<Binary> { ... }
The key difference from Solidity: DepsMut (for writing) and Deps (read-only) are explicitly separated. It's physically impossible to accidentally write to storage from a query function — the compiler won't allow it.
State management via cw-storage-plus:
use cw_storage_plus::{Item, Map};
pub const CONFIG: Item<Config> = Item::new("config");
pub const BALANCES: Map<&Addr, Uint128> = Map::new("balances");
Map with an address as key is the standard pattern for storing balances. There's no mapping(address => uint256) from Solidity, but the semantics are the same.
Compilation and Optimization
Raw cargo build --target wasm32-unknown-unknown --release produces a binary with debug symbols and unoptimized size. For production, we use the official optimizer:
docker run --rm -v "$(pwd)":/code \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
cosmwasm/rust-optimizer:0.15.0
Result is artifacts/my_contract.wasm. Size should be < 800KB (hard limit for most networks). Verification:
cosmwasm-check artifacts/my_contract.wasm
Deployment to Cosmos-Compatible Network
I'll show using Neutron (one of the main CosmWasm networks for DeFi). Using neutrond CLI or CosmJS:
Via CLI:
# Store — upload bytecode to the network
neutrond tx wasm store artifacts/my_contract.wasm \
--from <wallet> \
--gas auto --gas-adjustment 1.3 \
--fees 5000untrn \
--chain-id neutron-1 \
--node https://rpc-lb.neutron.org:443
# Get code_id from transaction events
# Instantiate — create contract instance
neutrond tx wasm instantiate <CODE_ID> \
'{"admin": "neutron1...", "param": "value"}' \
--label "my-contract-v1" \
--admin <wallet_address> \
--from <wallet> \
--gas auto --gas-adjustment 1.3 \
--fees 5000untrn \
--chain-id neutron-1
Via CosmJS (for automation):
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, {
prefix: "neutron",
});
const client = await SigningCosmWasmClient.connectWithSigner(
"https://rpc-lb.neutron.org:443",
wallet
);
// Upload
const uploadResult = await client.upload(senderAddress, wasmBinary, "auto");
const codeId = uploadResult.codeId;
// Instantiate
const { contractAddress } = await client.instantiate(
senderAddress,
codeId,
{ admin: senderAddress },
"my-contract-v1",
"auto"
);
Migratable Contracts
CosmWasm has a built-in migration mechanism — similar to upgradeable proxies in EVM, but simpler. You need to add an entry point:
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(
deps: DepsMut,
env: Env,
msg: MigrateMsg,
) -> Result<Response, ContractError> {
// Migration logic for state if needed
Ok(Response::default())
}
When deploying an instance with an --admin address, that address can call MigrateContract with a new code_id. Without --admin — the contract is immutable. For production protocols, the admin should be a multisig (via cosmwasm-multisig or DAO DAO).
Time Estimates
A contract with ready logic, written by an experienced Rust developer: deployment and verification 4–8 hours. If development is needed from scratch or porting from Solidity — 1–2 days depending on complexity.







