CosmWasm -> Solana / Anchor

Rewrite CosmWasm contracts as Solana programs

Use this guide when you are porting smart-contract logic from CosmWasm to a Solana program. It is organized around the real rewrite work: mental-model shifts, Solana accounts as the state objects your program reads and writes, CPI patterns for program-to-program calls, testing, program upgrades, and Solana-specific security rules.

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 strategyHorizontal app-chain ecosystem connected by IBCOne high-throughput chain with parallel execution
Typical block cadenceSecondsSub-second slots, which are Solana's leader-scheduled time windows
Execution VMWASM bytecodesBPF bytecode, Solana's executable program format
NondeterminismDisallowedDisallowed
Contract instancesMany instances per uploaded codeOne deployed program; many state accounts
Scaling consequenceApplication isolation by chainParallel 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

Bash
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

Bash
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

Rust
#[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

Rust
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:

TypeScript
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

Bash
rustup target add wasm32-unknown-unknown
cargo generate --git https://github.com/CosmWasm/cw-template
cargo build --target wasm32-unknown-unknown --release

Key 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

Bash
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:

Rust
#[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:

Rust
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:

Bash
solana rent 49 -um
# Rent-exempt minimum: 0.00123192 SOL
Rust
#[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.

Rust
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.

Rust
#[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:

Rust
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))
Rust
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
ExecutionAsynchronous message queueSynchronous call stack
Return valuesObserved through replyAvailable immediately
Accounts known up frontNoYes, across the whole call chain
Nesting limitFlexible application-level designCPI 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:

Rust
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 tokensinfo.funds or CW20 receive hookUser token account or ATA is passed explicitly
Sending tokensBankMsg::Send or CW20 executeSystem Program CPI or SPL Token CPI
Token standardCW20 contract per tokenShared token program plus mint accounts
Program escrowContract balance or tracked allowancesPDA-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:

Rust
#[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.

TypeScript
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:

TypeScript
const client = await SigningCosmWasmClient.connectWithSigner(rpcUrl, signer);
await client.execute(sender, contractAddress, { increment: {} }, "auto");
TypeScript
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.

Bash
npx codama init
npx codama run js

Anchor 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:

Rust
#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
    #[error("Unauthorized")]
    Unauthorized {},
}
Rust
#[error_code]
pub enum CounterError {
    #[msg("You are not authorized to perform this action")]
    Unauthorized,
    #[msg("Arithmetic overflow")]
    Overflow,
}
Rust
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.

Rust
let now_secs = env.block.time.seconds();
let height = env.block.height;
Rust
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.

Rust
let mut app = App::default();
let code = ContractWrapper::new(execute, instantiate, query);
let code_id = app.store_code(Box::new(code));

LiteSVM

Bash
cargo add --dev litesvm
cargo add --dev litesvm-utils
cargo add --dev litesvm-token
cargo add --dev anchor-litesvm
Rust
let 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

Bash
surfpool start

Surfpool 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.

Bash
# 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
Bash
# Solana / Anchor programs
anchor build
anchor deploy

solana program show PROGRAM_ID
solana program set-upgrade-authority PROGRAM_ID --final
Bash
# 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 --final

Costs, 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:

Bash
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:

Bash
solana-keygen grind --starts-with CNTR:1
solana program deploy ./target/deploy/my_program.so --program-id CNTR_KEYPAIR.json

For 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.

Bash
cargo install kora-deploy
kora-deploy --program-so ./target/deploy/my_program.so
Concern CosmWasm Solana
Upload vs instantiateSeparate stepsOne deployed program, many state accounts
Upgrade mechanismmigrate entry pointReplace program bytecode in place
Upgrade authorityAdmin address on instanceProgram upgrade authority key
ImmutabilityUnset adminFinalize 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.

Rust
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.

Rust
#[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>,
}
Rust
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(())
}
Rust
#[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
FactoryStores child contract addressesDerives pool PDAs from mint seeds
Pool creationInstantiate one contract per poolCreate one pool account plus vault accounts
LP tokenInstantiate a CW20 contractCreate an SPL mint, often PDA-controlled
Pool discoveryRead factory stateFilter 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

  1. Do not forget to include every required account in the transaction. If an account is not passed in, the program cannot touch it.
  2. Do not try to deploy a new program per user or per pool. One program usually serves many accounts.
  3. Treat init_if_needed as 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.
  4. Store bump seeds you will need later for PDA signing instead of recomputing them everywhere.
  5. Always include the 8-byte Anchor discriminator in space calculations.
  6. Do not use floating-point arithmetic. Use integers and scaled units such as basis points.
  7. Translate auth checks into account constraints, not just inline handler code.
  8. Remember that CPI nesting is limited. Very deep call graphs may need to be flattened or split across transactions.
  9. Account size is fixed unless you explicitly reallocate it.
  10. Use clock.slot for sequencing and clock.unix_timestamp for approximate wall-clock logic.

17. Migration Checklist

Use this checklist when porting a CosmWasm contract to Solana with Anchor.

Architecture

  • Identify every Item and Map and decide which PDA or account type will replace it.
  • Replace instantiate with one or more initialization instructions.
  • Remove on-chain query assumptions. Reads happen through RPC.
  • Map each ExecuteMsg variant 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.sender checks 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.

CosmWasmAnchor / Solana
Item<T>Singleton PDA account
Map<K, V>PDA per key with deterministic seeds
IndexedMapPDAs plus off-chain filtering or secondary index accounts
SnapshotMapVersioned accounts or custom history model
instantiateinitialize instruction with init or init_if_needed
executeInstruction handlers in #[program]
queryRPC account reads and client-side decoding
migrateProgram upgrade plus explicit migration instruction
replyUsually unnecessary because CPI is synchronous
BankMsg::SendSystem Program transfer CPI or SPL token CPI
WasmMsg::ExecuteAnchor CPI or raw invoke
ContractErrorAnchor #[error_code] enum or ProgramError
env.block.timeClock::get()?.unix_timestamp
env.block.heightClock::get()?.slot
info.senderSigner plus account constraints
CW20SPL mint plus token accounts or ATAs
CW721SPL mint plus Metaplex metadata or Core
cw-multi-testanchor test, local validator, LiteSVM
Contract instanceProgram plus one or more owned accounts
Contract adminStored authority plus upgrade authority policy
IBC contract assumptionsBridge 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

© 2026 Solana Foundation. Alle Rechte vorbehalten.
CosmWasm to Solana / Anchor Contract Migration Guide | Solana