1. Design Overview
This guide is for Rust smart contract developers who already know CosmWasm and want to make sound Solana architecture decisions instead of trying to preserve patterns that do not translate cleanly.
Four Solana concepts shape the whole rewrite: a program is executable onchain code, an account is the state object the runtime stores and loads, an instruction is a request to run one program handler, and a transaction is the signed bundle of instructions that succeeds or fails atomically.
Before touching code, internalize the two chains' core bets:
| Dimension | CosmWasm | Solana / Anchor |
|---|---|---|
| Scaling strategy | Horizontal app-chain ecosystem connected by IBC | One high-throughput chain with parallel execution |
| Typical block cadence | Seconds | Sub-second slots, which are Solana's leader-scheduled time windows |
| Execution VM | WASM bytecode | sBPF bytecode, Solana's executable program format |
| Nondeterminism | Disallowed | Disallowed |
| Contract instances | Many instances per uploaded code | One deployed program; many state accounts |
| Scaling consequence | Application isolation by chain | Parallel execution through explicit account locks |
The biggest practical consequence is Solana's parallel runtime: transactions declare every account they will touch up front. The runtime can then lock only those accounts and run transactions that do not write to the same account at the same time. That requirement shapes the rest of the programming model.
2. The Fundamental Shift: The Account Model
This is the hardest concept to internalize and the one that explains almost every other difference.
CosmWasm mental model
Code Upload (code_id: 42)
|
|-- instantiate() -> Contract Address A [owns its own key-value store]
|-- instantiate() -> Contract Address B [owns its own key-value store]
'-- instantiate() -> Contract Address C [owns its own key-value store]Each contract instance is self-contained. Logic and state live together at the same address, and deps.storage is the contract's private namespace.
Solana mental model
Deployed Program (address: Program111...)
|
|-- Pool Account A (PDA)
|-- Pool Account B (PDA)
'-- User Account C (PDA)A Solana program is stateless logic. State lives in separate accounts owned by the program. Ownership matters because only the owner program can modify an account's data, and the client or calling program must pass every required account into each instruction explicitly.
Why this matters in practice
- A CosmWasm factory often stores deployed child addresses. A Solana program usually derives addresses deterministically instead of storing them.
- A CosmWasm contract owns one isolated KV store. A Solana program looks more like a database engine that defines schemas and rules for many rows stored as separate accounts.
- Transactions declare all accounts they will read or write. That declaration is what unlocks parallelism.
Program Derived Addresses
Program Derived Addresses (PDAs) are the cornerstone of Solana state management. They are deterministic, off-curve account addresses derived from seeds plus the program ID and a bump seed. "Off-curve" means no private key exists for the address, so only the deriving program can sign for it through the runtime.
One note before the first program snippet: Solana has several frameworks for writing programs. This guide's Rust examples use Anchor, a framework that expands declarative macros like #[derive(Accounts)] and #[account(...)] into the account-validation and serialization code you would otherwise write by hand. Anchor is the most widely used choice, but not the only one — you can also build with the lower-level Pinocchio or with no framework at all; the Toolchain section compares the options.
Solana PDA
#[derive(Accounts)]
pub struct Increment<'info> {
pub user: Signer<'info>,
#[account(
mut,
seeds = [b"counter", user.key().as_ref()],
bump,
)]
pub counter: Account<'info, Counter>,
}Cosmos map lookup
pub const USER_COUNTERS: Map<&Addr, u64> = Map::new("user_counters");
let count = USER_COUNTERS.load(deps.storage, &info.sender)?;That CosmWasm map lookup becomes PDA derivation plus account deserialization on Solana. The key is the address.
Client-side derivation
The derivation is not program-only: any client can compute the same address off-chain and fetch the account directly, so frontends and indexers never need an on-chain registry. With @solana/kit, the TypeScript equivalent of the seeds above is:
import { getAddressEncoder, getProgramDerivedAddress } from "@solana/kit";
const [counterPda, bump] = await getProgramDerivedAddress({
programAddress: COUNTER_PROGRAM_ADDRESS,
seeds: ["counter", getAddressEncoder().encode(userAddress)],
});3. Project Structure & Toolchain
CosmWasm and Solana are both Rust ecosystems, but the build pipeline, deployment model, and testing workflow differ substantially. Solana programs compile to sBPF, the Solana bytecode format executed by the runtime.
CosmWasm toolchain
rustup target add wasm32-unknown-unknown
cargo generate --git https://github.com/CosmWasm/cw-template
cargo build --target wasm32-unknown-unknown --releaseKey crates: cosmwasm-std, cw-storage-plus, cw-multi-test, and thiserror.
Solana toolchain options
Most Solana Rust teams choose between Anchor and Pinocchio. Both target Solana's runtime, but they optimize for different tradeoffs.
Anchor toolchain
Anchor is the higher-level framework. It adds account validation macros, IDL generation, client bindings, and a batteries-included test workflow.
An IDL is a machine-readable description of a program's instructions, accounts, and types. Clients can use it to build transactions safely, which makes Anchor the default choice when you want fast iteration, better ergonomics, and a larger ecosystem of examples.
Key crates: solana-program, anchor-lang, anchor-spl, and SPL Token libraries such as spl-token.
Pinocchio toolchain
Pinocchio is a lower-level framework with a much thinner abstraction layer. It keeps you closer to native Solana program structure, which can be useful when you want tighter control over instruction dispatch, account handling, binary size, or compute budget behavior.
Compute budget is Solana's per-transaction limit for execution work, measured in compute units. Use Anchor when you want framework support for account constraints, IDLs, and client generation. Use Pinocchio when you want a lighter abstraction and are comfortable owning more of the boilerplate and safety checks yourself.
Account constraints are declarative checks that tell Anchor which accounts must sign, be writable, match seeds, or satisfy ownership rules.
Directory layout comparison
src/
contract.rs
msg.rs
state.rs
error.rs
tests/4. Entry Points: Messages vs Instructions
CosmWasm exposes distinct entry points for different classes of work. Native Solana exposes one instruction processor that you dispatch yourself, and Anchor rebuilds a multi-handler abstraction on top.
There is no separate instantiate step on Solana. Initialization is just another instruction, usually one that creates state accounts and derives any PDA addresses they use. Queries are off-chain RPC reads, not on-chain handlers; clients fetch account data from an RPC node and decode it locally. Kit's codecs give TypeScript clients composable decoders for turning those raw account bytes back into structured data.
Compare the entry points:
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> { ... }
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> { ... }
#[entry_point]
pub fn query(
deps: Deps,
env: Env,
msg: QueryMsg,
) -> StdResult<Binary> { ... }That match on the first byte is Solana's replacement for CosmWasm's ExecuteMsg enum dispatch: instruction data begins with a discriminator that assigns the payload to a handler. In Pinocchio you define and read it yourself — here a single byte. Anchor abstracts the same mechanism: it derives an 8-byte discriminator from each handler's name and routes on it automatically, which is why the snippet below has no visible dispatch.
5. State Management: Contract Storage vs PDAs
Every Item or Map you used in CosmWasm needs an explicit account model on Solana. In practice, each logical entry often becomes its own program-owned account: an account whose owner field is set to your program ID, which lets your program write that account's data.
Compare state management:
use cw_storage_plus::{Item, Map};
pub const CONFIG: Item<Config> = Item::new("config");
pub const BALANCES: Map<&Addr, Uint128> = Map::new("balances");
pub const ALLOWANCES: Map<(&Addr, &Addr), Uint128> = Map::new("allowances");Rent and space allocation
Solana storage is not abstracted away. Accounts must carry enough lamports, the smallest unit of SOL, to remain rent-exempt. Rent exemption means the account has the minimum balance required to stay onchain for its allocated data size, and you must allocate enough space up front for the layout you intend to store. Despite the name, rent is a refundable deposit, not a fee: the lamports sit escrowed in the account to hold its state and come back in full when the account is closed.
The CLI prints the deposit for any data size — here 49 bytes, the 8-byte discriminator plus UserBalance::INIT_SPACE:
solana rent 49 -um
# Rent-exempt minimum: 0.00123192 SOL#[account(
init,
payer = user,
space = 8 + UserBalance::INIT_SPACE,
)]
pub user_balance: Account<'info, UserBalance>The same allocation in Pinocchio is an explicit system-program CPI: read the rent sysvar, compute the minimum balance for the space, and create the account with your program as its owner.
use pinocchio::sysvars::{rent::Rent, Sysvar};
use pinocchio_system::instructions::CreateAccount;
let lamports = Rent::get()?.try_minimum_balance(space)?;
CreateAccount {
from: payer,
to: user_balance,
lamports,
space: space as u64,
owner: program_id,
}
.invoke()?;In Anchor, the leading 8 bytes are the account discriminator, a type identifier Anchor writes before the account data so it can reject the wrong account type. In Pinocchio or other lower-level frameworks, you define and validate the binary layout yourself. Variable-length fields such as strings and vectors require explicit sizing discipline either way.
#[account]
#[derive(InitSpace)]
pub struct Metadata {
#[max_len(32)]
pub name: String,
#[max_len(200)]
pub uri: String,
}If you may add fields later, either pre-allocate spare space up front or plan a realloc or migration path — the realloc program example shows the resize flow.
Reallocation changes an existing account's data length, but it has runtime limits and may require extra lamports to keep the account rent-exempt. Existing Solana accounts do not automatically grow the way contract-local storage feels like it does in CosmWasm.
6. Cross-Contract Communication: Sub-Messages vs CPIs
CosmWasm composes contracts through asynchronous messages. Solana composes programs through synchronous cross-program invocations (CPIs). A CPI is one Solana program calling an instruction on another program during the same transaction.
Compare contract calls:
let exec = WasmMsg::Execute {
contract_addr: pool_contract.to_string(),
msg: to_json_binary(&PoolExecuteMsg::Swap { amount_in, min_amount_out })?,
funds: coins(amount_in.u128(), "uatom"),
};
let sub_msg = SubMsg::reply_on_success(exec, SWAP_REPLY_ID);
Ok(Response::new().add_submessage(sub_msg))let cpi_ctx = CpiContext::new(
ctx.accounts.pool_program.to_account_info(),
pool_program::cpi::accounts::Swap {
pool: ctx.accounts.pool.to_account_info(),
user_src: ctx.accounts.user_src.to_account_info(),
user_dst: ctx.accounts.user_dst.to_account_info(),
authority: ctx.accounts.user.to_account_info(),
},
);
pool_program::cpi::swap(cpi_ctx, amount_in, min_amount_out)?;| Dimension | CosmWasm | Solana |
|---|---|---|
| Execution | Asynchronous message queue | Synchronous call stack |
| Return values | Observed through reply | Available immediately |
| Accounts known up front | No | Yes, across the whole call chain |
| Nesting limit | Flexible application-level design | CPI depth is limited |
Practical consequence: on Solana, the caller must gather every account the entire CPI chain will need and include them in the transaction.
Account privileges such as signer and writable status flow from caller to callee, so a callee cannot use an account with privileges the original transaction did not grant. The reward is a more predictable execution surface.
7. Token Handling: Bank Module / CW20 vs SPL Tokens
CosmWasm splits native-denom flows and CW20 flows. Solana standardizes fungible tokens around the SPL Token program, where a mint account defines a token and token accounts hold balances for one owner and one mint.
SOL itself moves through the System Program, the built-in program that creates accounts, transfers SOL, allocates data, and assigns ownership.
Every Solana snippet below is a CPI from the previous section into the System or Token Program. The other term to know is authority: the account or PDA that a program recognizes as allowed to perform an action such as transferring tokens, minting tokens, or changing settings.
The examples below cover the three common transfer cases: moving SOL with the System Program, moving SPL tokens with the Token Program, and moving tokens from a program-controlled vault by signing with PDA seeds.
Compare token handling:
let payment = info.funds.iter()
.find(|c| c.denom == "uatom")
.ok_or(ContractError::NoFundsProvided {})?;
let transfer_msg = Cw20ExecuteMsg::TransferFrom {
owner: info.sender.to_string(),
recipient: env.contract.address.to_string(),
amount,
};| Concern | CosmWasm | Solana |
|---|---|---|
| Receiving tokens | info.funds or CW20 receive hook | User token account or ATA is passed explicitly |
| Sending tokens | BankMsg::Send or CW20 execute | System Program CPI or SPL Token CPI |
| Token standard | CW20 contract per token | Shared token program plus mint accounts |
| Program escrow | Contract balance or tracked allowances | PDA-controlled vault token accounts |
An Associated Token Account (ATA) is the conventional token account address for a wallet and mint. It is derived deterministically, which lets clients find or create the expected token account without asking users to manage token-account addresses directly.
One difference that catches Cosmos developers: a bank send credits any address implicitly, but an SPL transfer fails unless the recipient's token account has already been initialized. Create the ATA before transferring — anyone can pay to create one for any wallet, and idempotent creation (the ATA program's create-idempotent instruction, or Anchor's init_if_needed constraint) makes it safe to include in every transfer flow.
A vault token account is a normal token account whose authority is a PDA instead of a user's wallet. Your program can move tokens out of that vault only when it proves the PDA seeds to the runtime with invoke_signed or Anchor's signer-seed helpers.
8. Serialization: JSON / serde vs Binary Data
CosmWasm messages are JSON-first and CLI-friendly. Solana instruction data and account data are binary-first: programs receive byte arrays and interpret those bytes according to the program's own layout.
Anchor prepends an 8-byte discriminator to each instruction and each account type so it can route handlers and validate layouts automatically. For instructions, a discriminator identifies which handler should run; for accounts, it identifies which account type is being decoded.
One practical upside over JSON is that integers stay integers. You do not need the string-encoded Uint128 convention that exists to protect precision in JSON tooling.
If you are writing native Solana instruction enums without Anchor's IDL layer, choose a deterministic binary codec such as wincode or borsh, then document that format so clients can encode the same bytes.
The tradeoff is debuggability: instruction bytes are not naturally human-readable at the CLI layer, so your client, IDL, and tests become part of the interface contract.
Compare serialization:
#[cw_serde]
pub enum ExecuteMsg {
Transfer { recipient: String, amount: Uint128 },
Burn { amount: Uint128 },
}Deserialization
Deserialization mirrors this on both sides of the RPC boundary. Inside the program, Anchor's Account<'info, T> checks the discriminator and borsh-decodes the bytes before your handler runs, so deserialization is part of account validation. In Pinocchio or native code you do it yourself, either with borsh's try_from_slice or by reinterpreting raw bytes against a #[repr(C)] layout the way the State section's try_from_bytes example does.
On the client, account data comes back from the RPC as raw bytes and must be decoded against the same layout. Anchor clients decode through the IDL (program.account.userBalance.fetch(pda)); Kit clients compose the layout from the same codecs used for encoding, and Codama can generate these decoders from your IDL.
import { getAddressCodec, getStructCodec, getU64Codec, getU8Codec } from "@solana/kit";
const userBalanceCodec = getStructCodec([
["owner", getAddressCodec()],
["amount", getU64Codec()],
["bump", getU8Codec()],
]);
// Skip Anchor's 8-byte account discriminator, then decode the layout.
const state = userBalanceCodec.decode(accountData.slice(8));9. Invoking Your Program: Generated Clients
On Cosmos, a client sends a JSON message through CosmJS and the chain routes it to the contract. On Solana, the client assembles the whole transaction: it picks the instruction, encodes its data, and lists every account the instruction touches. Nobody writes that by hand per instruction — clients are generated from the IDL, and the generated code does the account bookkeeping for you.
Compare invocation:
const client = await SigningCosmWasmClient.connectWithSigner(rpcUrl, signer);
await client.execute(sender, contractAddress, { increment: {} }, "auto");const program = new Program(idl, provider);
await program.methods
.increment()
.accounts({ user: wallet.publicKey })
.rpc();The Anchor client reads instruction shapes, account lists, and PDA seed metadata from the IDL, so it derives the counter PDA and fills in known programs itself — you pass only what it cannot infer. The earlier rule that the caller must list every account still holds at the wire level; the generated client is what does the listing.
Standalone clients with Codama
Codama generates framework-free clients from the same IDL: TypeScript builders compatible with @solana/kit through @codama/renderers-js, and Rust clients through @codama/renderers-rust. Each generated instruction builder validates inputs, resolves default and derivable accounts, encodes arguments, and returns a ready-to-send instruction, with account decoders and PDA helpers alongside — Kit's own program clients are generated this way.
npx codama init
npx codama run jsAnchor projects can fold this into the build: run anchor codama generate -l js,rust -p clients target/idl/counter.json, or set [clients] auto = true in Anchor.toml so every anchor build regenerates the clients.
10. Error Handling
Both ecosystems lean on Rust error types, but Solana makes it more important to avoid panics because a panic often collapses into an unhelpful runtime failure. Prefer explicit Anchor #[error_code] values or native ProgramError variants so callers and tests can identify the failure path.
Compare error handling:
#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
#[error("Unauthorized")]
Unauthorized {},
}#[error_code]
pub enum CounterError {
#[msg("You are not authorized to perform this action")]
Unauthorized,
#[msg("Arithmetic overflow")]
Overflow,
}require!(
ctx.accounts.counter.authority == ctx.accounts.user.key(),
CounterError::Unauthorized
);11. Time & Block Information
CosmWasm gives you consensus time and block height in env. Solana exposes time and slot data through sysvars, which are read-only system accounts that expose cluster state to programs. The most common one here is Clock.
let now_secs = env.block.time.seconds();
let height = env.block.height;let clock = Clock::get()?;
require!(
clock.unix_timestamp >= unlock_time,
LockError::NotYetUnlockable
);Use clock.slot for ordering and sequencing. A slot is Solana's logical time unit for leader-scheduled block production. Use clock.unix_timestamp for approximate wall-clock checks such as unlock windows or expiries. Treat the timestamp as good enough for time-based UX, not as a perfect monotonic clock.
12. Testing
On Solana, transaction assembly is part of the product surface. Good tests exercise accounts, signers, PDA derivation, token accounts, and CPI behavior in addition to pure business logic. A signer is an account whose private key authorized the transaction, and signer status is one of the account privileges the runtime checks before execution.
let mut app = App::default();
let code = ContractWrapper::new(execute, instantiate, query);
let code_id = app.store_code(Box::new(code));LiteSVM
cargo add --dev litesvm
cargo add --dev litesvm-utils
cargo add --dev litesvm-token
cargo add --dev anchor-litesvmlet mut svm = LiteSVM::new();
svm.add_program_from_file(PROGRAM_ID, "target/deploy/counter.so")?;LiteSVM is a strong default for Rust-first Solana tests. It keeps execution in process, which makes it fast for instruction-level testing, PDA flows, account setup, and failure-path coverage.
litesvm: the core in-process VM.litesvm-utils: general helpers for setup, assertions, and repetitive test plumbing.litesvm-token: helpers for SPL token mint, account, and transfer scenarios.anchor-litesvm: Anchor-aware helpers when your program and accounts use Anchor types and patterns.
For working references: every project in the program-examples repository ships a test suite alongside each framework implementation, and LiteSVM's own test directory shows the harness exercised end to end.
Surfpool
surfpool startSurfpool is useful when you want a validator-like local environment with RPC workflows, debugger-style inspection, and on-demand access to cluster state. A local validator environment gives you a private Solana cluster for integration testing without relying on devnet or mainnet RPC. See also the Surfpool GitHub repository.
Port your old contract tests conceptually, then add Solana-specific cases for signer spoofing, account substitution, stale reads after CPI, rent-funded account creation, and account closure behavior.
Account closure transfers lamports out and leaves the account unusable for the old state layout, so test it anywhere your program releases escrow or deletes state.
13. Deployment & Upgrades
CosmWasm separates code upload from instantiation and supports explicit migrate handlers. Solana deploys one program address and upgrades it in place under an upgrade authority. The upgrade authority is the signer allowed to replace the program's bytecode; revoking it makes the program immutable.
# CosmWasm
cargo build --target wasm32-unknown-unknown --release
wasmd tx wasm store counter_opt.wasm --from wallet
wasmd tx wasm instantiate CODE_ID '{"count":0}' --from wallet# Solana / Anchor programs
anchor build
anchor deploy
solana program show PROGRAM_ID
solana program set-upgrade-authority PROGRAM_ID --final# Solana / Pinocchio programs with the Solana CLI
cargo build-sbf
solana program deploy ./target/deploy/my_program.so
solana program show PROGRAM_ID
solana program deploy --program-id PROGRAM_ID ./target/deploy/my_program.so
solana program set-upgrade-authority PROGRAM_ID --finalCosts, addresses, and free devnet rehearsals
Deployment locks a rent deposit sized to the program binary — usually a few SOL, and by far the largest cost of going live. Check it up front, and note that closing a program with solana program close returns the deposit:
solana rent $(wc -c < ./target/deploy/my_program.so)A program ID is an ordinary keypair until you deploy to it, so teams often grind a recognizable vanity address first and deploy to that:
solana-keygen grind --starts-with CNTR:1
solana program deploy ./target/deploy/my_program.so --program-id CNTR_KEYPAIR.jsonFor devnet rehearsals, kora-deploy removes the SOL requirement entirely: a hosted Kora paymaster funds the rent and fees, your wallet is registered as the only key allowed to upgrade the program, and programs idle for more than seven days are reclaimed automatically.
cargo install kora-deploy
kora-deploy --program-so ./target/deploy/my_program.so| Concern | CosmWasm | Solana |
|---|---|---|
| Upload vs instantiate | Separate steps | One deployed program, many state accounts |
| Upgrade mechanism | migrate entry point | Replace program bytecode in place |
| Upgrade authority | Admin address on instance | Program upgrade authority key |
| Immutability | Unset admin | Finalize upgrade authority |
State migration on Solana is your responsibility. If old accounts need a new layout, either pre-allocate space for forward compatibility or add a protected one-time migration instruction. A migration instruction is just another program instruction, but it should be tightly authorized and usually guarded by a state-version field.
14. Full Side-by-Side Example: Counter Contract
A counter contract is a useful migration seed because it forces you to model state layout, authority, initialization, and off-chain reads without too much noise. Authority means the account or PDA your program treats as allowed to perform an action, usually enforced with a signer check, PDA seed check, or stored public key comparison.
pub const COUNTS: Map<&Addr, i32> = Map::new("counts");
#[entry_point]
pub fn execute(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Increment {} => {
let count = COUNTS
.may_load(deps.storage, &info.sender)?
.unwrap_or(0)
.checked_add(1)
.ok_or(ContractError::Overflow {})?;
COUNTS.save(deps.storage, &info.sender, &count)?;
Ok(Response::new())
}
ExecuteMsg::Reset { count } => {
COUNTS.save(deps.storage, &info.sender, &count)?;
Ok(Response::new())
}
}
}The Anchor example below uses init_if_needed, which creates the PDA account if it does not exist and otherwise loads the existing account. That is convenient for a small example, but production code should still validate the existing account's authority, seeds, and state version before trusting it.
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: i64,
pub authority: Pubkey,
pub bump: u8,
}
#[derive(Accounts)]
pub struct ModifyCounter<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
init_if_needed,
payer = user,
space = 8 + Counter::INIT_SPACE,
seeds = [b"counter", user.key().as_ref()],
bump,
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}pub fn increment(ctx: Context<ModifyCounter>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
if counter.authority == Pubkey::default() {
counter.authority = ctx.accounts.user.key();
counter.bump = ctx.bumps.counter;
}
counter.count = counter.count
.checked_add(1)
.ok_or(CounterError::Overflow)?;
Ok(())
}#[repr(C)]
pub struct Counter {
pub count: i64,
pub authority: Address,
pub bump: u8,
}
pub fn process_increment(
program_id: &Address,
accounts: &mut [AccountView],
_instruction_data: &[u8],
) -> ProgramResult {
let [user, counter, ..] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
let (expected_counter, bump) =
Address::derive_program_address(&[b"counter", user.address().as_ref()], program_id)
.ok_or(ProgramError::InvalidSeeds)?;
if counter.address() != &expected_counter {
return Err(ProgramError::InvalidArgument);
}
let mut counter_data = counter.try_borrow_mut()?;
let counter_state = Counter::try_from_bytes_mut(&mut counter_data)?;
if counter_state.authority == Address::default() {
counter_state.authority = *user.address();
counter_state.bump = bump;
}
counter_state.count = counter_state
.count
.checked_add(1)
.ok_or(ProgramError::ArithmeticOverflow)?;
Ok(())
}The important translation is not the arithmetic. It is that the per-user map entry in CosmWasm becomes a per-user PDA account on Solana, and reads happen by deriving that PDA off-chain and fetching the account.
15. Porting a DEX: Lessons from the Real World
DEX ports expose the biggest architectural differences quickly because they touch factories, pools, vaults, LP tokens, pricing state, and multi-hop account graphs.
In Solana programs, a vault is usually a PDA-controlled token account that holds assets for a pool or escrow. A multi-hop account graph is the full list of accounts needed for every swap leg in one transaction.
| Pattern | CosmWasm | Solana |
|---|---|---|
| Factory | Stores child contract addresses | Derives pool PDAs from mint seeds |
| Pool creation | Instantiate one contract per pool | Create one pool account plus vault accounts |
| LP token | Instantiate a CW20 contract | Create an SPL mint, often PDA-controlled |
| Pool discovery | Read factory state | Filter program accounts off-chain |
The client-side complexity shifts noticeably. On Solana, the client must derive PDAs, create ATAs when needed, and assemble all accounts for the whole transaction. For discovery flows, clients often use getProgramAccounts or an indexer to find accounts owned by a program. The payoff is explicitness and parallel-friendly execution.
This is why a factory port is not just a contract rewrite. It is also a client rewrite and a state-discovery rewrite.
16. Common Pitfalls & Mental Traps
- Do not forget to include every required account in the transaction. If an account is not passed in, the program cannot touch it.
- Do not try to deploy a new program per user or per pool. One program usually serves many accounts.
- Treat
init_if_neededas a sharp tool. This Anchor constraint initializes an account only when it is missing, so pair it with clear authority, seed, and state-version validation so it cannot silently reuse an account in an invalid state. - Store bump seeds you will need later for PDA signing instead of recomputing them everywhere.
- Always include the 8-byte Anchor discriminator in space calculations.
- Do not use floating-point arithmetic. Use integers and scaled units such as basis points.
- Translate auth checks into account constraints, not just inline handler code.
- Remember that CPI nesting is limited. Very deep call graphs may need to be flattened or split across transactions.
- Account size is fixed unless you explicitly reallocate it.
- Use
clock.slotfor sequencing andclock.unix_timestampfor approximate wall-clock logic.
17. Migration Checklist
Use this checklist when porting a CosmWasm contract to Solana with Anchor.
Architecture
- Identify every
ItemandMapand decide which PDA or account type will replace it. - Replace
instantiatewith one or more initialization instructions. - Remove on-chain query assumptions. Reads happen through RPC.
- Map each
ExecuteMsgvariant to an instruction handler and an account graph.
State
- Define
#[account]structs with explicit sizing. - Add bump fields where PDA signing or validation will need them.
- Budget for rent and for variable-length data.
- Plan reallocation or forward-compatible spare space if layouts may evolve.
Tokens
- Replace bank sends with System Program transfers where the asset is SOL.
- Replace CW20 flows with SPL Token CPIs and token-account modeling.
- Add ATA creation for holders that need token accounts.
- Replace LP-token contracts with SPL mint accounts plus mint authority policy.
- For CW721-style assets, choose whether the Solana version uses Metaplex metadata, Metaplex Core, or another NFT standard before you finalize account layouts.
Security, testing, and deployment
- Move
info.senderchecks into signer and account constraints where possible. - Test PDA derivation, account substitution resistance, stale reads after CPI, and account initialization paths.
- Define a state-migration strategy before the first production upgrade.
- Decide whether upgrade authority will remain active, move to multisig, or be finalized away.
- Work through the production readiness guide before launch — redundant RPC, priority fees and compute budgets for transaction landing, retry and confirmation strategy, key management, and monitoring.
Quick Reference Table
Use this as a fast translation map when you are porting familiar CosmWasm primitives into Solana-native account and instruction patterns. The linked sections above define the Solana terms used in this table.
| CosmWasm | Anchor / Solana |
|---|---|
| Item<T> | Singleton PDA account |
| Map<K, V> | PDA per key with deterministic seeds |
| IndexedMap | PDAs plus off-chain filtering or secondary index accounts |
| SnapshotMap | Versioned accounts or custom history model |
| instantiate | initialize instruction with init or init_if_needed |
| execute | Instruction handlers in #[program] |
| query | RPC account reads and client-side decoding |
| migrate | Program upgrade plus explicit migration instruction |
| reply | Usually unnecessary because CPI is synchronous |
| BankMsg::Send | System Program transfer CPI or SPL token CPI |
| WasmMsg::Execute | Anchor CPI or raw invoke |
| ContractError | Anchor #[error_code] enum or ProgramError |
| env.block.time | Clock::get()?.unix_timestamp |
| env.block.height | Clock::get()?.slot |
| info.sender | Signer plus account constraints |
| CW20 | SPL mint plus token accounts or ATAs |
| CW721 | SPL mint plus Metaplex metadata or Core |
| cw-multi-test | anchor test, local validator, LiteSVM |
| Contract instance | Program plus one or more owned accounts |
| Contract admin | Stored authority plus upgrade authority policy |
| IBC contract assumptions | Bridge or interoperability project; no native IBC equivalent |
CosmWasm
Item<T>
Anchor / Solana
Singleton PDA account
CosmWasm
Map<K, V>
Anchor / Solana
PDA per key with deterministic seeds
CosmWasm
IndexedMap
Anchor / Solana
PDAs plus off-chain filtering or secondary index accounts
CosmWasm
SnapshotMap
Anchor / Solana
Versioned accounts or custom history model
CosmWasm
instantiate
Anchor / Solana
initialize instruction with init or init_if_needed
CosmWasm
execute
Anchor / Solana
Instruction handlers in #[program]
CosmWasm
query
Anchor / Solana
RPC account reads and client-side decoding
CosmWasm
migrate
Anchor / Solana
Program upgrade plus explicit migration instruction
CosmWasm
reply
Anchor / Solana
Usually unnecessary because CPI is synchronous
CosmWasm
BankMsg::Send
Anchor / Solana
System Program transfer CPI or SPL token CPI
CosmWasm
WasmMsg::Execute
Anchor / Solana
Anchor CPI or raw invoke
CosmWasm
ContractError
Anchor / Solana
Anchor #[error_code] enum or ProgramError
CosmWasm
env.block.time
Anchor / Solana
Clock::get()?.unix_timestamp
CosmWasm
env.block.height
Anchor / Solana
Clock::get()?.slot
CosmWasm
info.sender
Anchor / Solana
Signer plus account constraints
CosmWasm
CW20
Anchor / Solana
SPL mint plus token accounts or ATAs
CosmWasm
CW721
Anchor / Solana
SPL mint plus Metaplex metadata or Core
CosmWasm
cw-multi-test
Anchor / Solana
anchor test, local validator, LiteSVM
CosmWasm
Contract instance
Anchor / Solana
Program plus one or more owned accounts
CosmWasm
Contract admin
Anchor / Solana
Stored authority plus upgrade authority policy
CosmWasm
IBC contract assumptions
Anchor / Solana
Bridge or interoperability project; no native IBC equivalent