2.1 State Storage Models

Each platform has a fundamentally different approach to storing contract state:

The Three Models

  • CosmWasm: Key-value store using cw_storage_plus - you define storage items and maps
  • Solidity: Contract storage slots - state variables are stored in 32-byte slots
  • Solana: Account-based - each piece of state is a separate account (PDA)

CosmWasm State Definition

use cw_storage_plus::{Item, Map, IndexedMap};

// Single value storage
pub const CONFIG: Item<HubConfig> =
    Item::new("config");

pub const ADMIN: Item<Addr> =
    Item::new("admin");

// Map storage (key -> value)
pub const OFFERS: Map<u64, Offer> =
    Map::new("offers");

// Multi-key map
pub const USER_OFFERS: Map<(&Addr, u64), Offer> =
    Map::new("user_offers");

// Counter for IDs
pub const OFFER_COUNT: Item<u64> =
    Item::new("offer_count");

Solidity State Definition

contract Offer {
    // Single value (slot 0)
    HubConfig public config;

    // Address (slot 1)
    address public admin;

    // Mapping (hashed storage)
    mapping(uint256 => OfferData)
        public offers;

    // Nested mapping
    mapping(address => uint256[])
        public userOffers;

    // Counter
    uint256 public offerCount;

    // Set for efficient lookups
    using EnumerableSet for
        EnumerableSet.UintSet;
    EnumerableSet.UintSet private
        activeOfferIds;
}

Solana Account-Based State

// Each state is a separate account

// Global counter (1 account)
#[account]
pub struct OfferCounter {
    pub count: u64,
    pub bump: u8,
}

// Each offer is its own account
#[account]
pub struct Offer {
    pub id: u64,
    pub owner: Pubkey,
    pub offer_type: OfferType,
    pub state: OfferState,
    pub fiat_currency: [u8; 3],
    pub token_mint: Pubkey,
    pub min_amount: u64,
    pub max_amount: u64,
    pub rate: u64,
    pub bump: u8,
}

// PDA seeds determine address
// seeds = [b"offer", owner, offer_id]
💡
Key Difference: Account Model

In Solana, there's no "contract storage" like CosmWasm/Solidity. Each piece of state is a separate on-chain account. The program (contract) doesn't own data - it just has authority to modify accounts. This means clients must pass all required accounts explicitly in every transaction.

2.2 Reading State

How you read state differs significantly based on each platform's model:

CosmWasm Reading

// In query handler
#[entry_point]
pub fn query(
    deps: Deps,
    _env: Env,
    msg: QueryMsg,
) -> StdResult<Binary> {
    match msg {
        QueryMsg::Config {} => {
            let config = CONFIG.load(
                deps.storage
            )?;
            to_binary(&config)
        }
        QueryMsg::Offer { id } => {
            let offer = OFFERS.load(
                deps.storage, id
            )?;
            to_binary(&offer)
        }
        QueryMsg::OffersByOwner { owner } => {
            let offers: Vec<_> = OFFERS
                .idx
                .owner
                .prefix(&owner)
                .range(deps.storage, None, None, Order::Asc)
                .take(100)
                .filter_map(|r| r.ok())
                .map(|(_, o)| o)
                .collect();
            to_binary(&offers)
        }
    }
}

Solidity Reading

// View functions (free, no gas)

function getConfig()
    external view returns (HubConfig memory)
{
    return config;
}

function getOffer(uint256 id)
    external view returns (OfferData memory)
{
    return offers[id];
}

function getOffersByOwner(address owner)
    external view returns (uint256[] memory)
{
    return userOffers[owner];
}

// Can also read public vars directly
// contract.offers(id) auto-generated

// Efficient iteration with EnumerableSet
function getActiveOffers(uint256 limit)
    external view returns (uint256[] memory)
{
    uint256 len = activeOfferIds.length();
    uint256 size = len < limit ? len : limit;
    uint256[] memory result =
        new uint256[](size);

    for (uint256 i = 0; i < size; i++) {
        result[i] = activeOfferIds.at(i);
    }
    return result;
}

Solana Reading (Client-Side)

// Reading happens CLIENT-SIDE!
// No on-chain query functions

// TypeScript client code:
import { PublicKey } from "@solana/web3.js";

// Derive PDA address
const [offerPda] = PublicKey.findProgramAddressSync(
  [
    Buffer.from("offer"),
    owner.toBuffer(),
    new BN(offerId).toArrayLike(Buffer, "le", 8)
  ],
  programId
);

// Fetch account data
const offer = await program.account.offer
  .fetch(offerPda);

// Fetch all offers by owner
const allOffers = await program.account.offer
  .all([
    {
      memcmp: {
        offset: 8, // Skip discriminator
        bytes: owner.toBase58()
      }
    }
  ]);

// Or use getProgramAccounts
const accounts = await connection
  .getProgramAccounts(programId, {
    filters: [...]
  });
Solana Has No On-Chain Queries!

Unlike CosmWasm and Solidity, Solana programs cannot query state on-chain. All reads happen client-side by fetching account data directly from the network. This is a fundamental architectural difference!

2.3 Writing State

CosmWasm Writing

pub fn create_offer(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: CreateOfferMsg,
) -> Result<Response, ContractError> {
    // Load and increment counter
    let id = OFFER_COUNT.load(deps.storage)?;
    OFFER_COUNT.save(
        deps.storage,
        &(id + 1)
    )?;

    // Create offer struct
    let offer = Offer {
        id,
        owner: info.sender.clone(),
        offer_type: msg.offer_type,
        state: OfferState::Active,
        fiat_currency: msg.fiat_currency,
        min_amount: msg.min_amount,
        max_amount: msg.max_amount,
        rate: msg.rate,
        created_at: env.block.time,
    };

    // Save to indexed map
    OFFERS.save(deps.storage, id, &offer)?;

    Ok(Response::new()
        .add_attribute("action", "create_offer")
        .add_attribute("offer_id", id.to_string()))
}

Solidity Writing

function createOffer(
    OfferType offerType,
    string calldata fiatCurrency,
    uint256 minAmount,
    uint256 maxAmount,
    uint256 rate
) external whenNotPaused returns (uint256) {
    // Increment counter
    uint256 id = offerCount++;

    // Create offer in storage
    offers[id] = OfferData({
        id: id,
        owner: msg.sender,
        offerType: offerType,
        state: OfferState.Active,
        fiatCurrency: fiatCurrency,
        minAmount: minAmount,
        maxAmount: maxAmount,
        rate: rate,
        createdAt: block.timestamp
    });

    // Track user's offers
    userOffers[msg.sender].push(id);

    // Add to active set
    activeOfferIds.add(id);

    emit OfferCreated(
        id, msg.sender, offerType
    );

    return id;
}

Solana Writing

pub fn create_offer(
    ctx: Context<CreateOffer>,
    fiat_currency: [u8; 3],
    min_amount: u64,
    max_amount: u64,
    rate: u64,
) -> Result<()> {
    // Increment global counter
    let counter = &mut ctx.accounts.counter;
    let id = counter.count;
    counter.count += 1;

    // Initialize offer account (PDA)
    let offer = &mut ctx.accounts.offer;
    offer.id = id;
    offer.owner = ctx.accounts.owner.key();
    offer.offer_type = OfferType::Buy;
    offer.state = OfferState::Active;
    offer.fiat_currency = fiat_currency;
    offer.min_amount = min_amount;
    offer.max_amount = max_amount;
    offer.rate = rate;
    offer.bump = ctx.bumps.offer;

    Ok(())
}

#[derive(Accounts)]
#[instruction(fiat_currency: [u8; 3])]
pub struct CreateOffer<'info> {
    #[account(mut)]
    pub owner: Signer<'info>,

    #[account(
        mut,
        seeds = [b"counter"],
        bump = counter.bump
    )]
    pub counter: Account<'info, OfferCounter>,

    #[account(
        init,
        payer = owner,
        space = 8 + Offer::INIT_SPACE,
        seeds = [b"offer", owner.key().as_ref(),
                 &counter.count.to_le_bytes()],
        bump
    )]
    pub offer: Account<'info, Offer>,

    pub system_program: Program<'info, System>,
}

2.4 Indexed Storage for Queries

The Offer contract needs to query offers by owner, type, fiat currency, and state. Here's how each platform handles indexing:

CosmWasm IndexedMap

use cw_storage_plus::{IndexedMap, MultiIndex};

// Define index functions
pub struct OfferIndexes<'a> {
    pub owner: MultiIndex<
        'a, Addr, Offer, u64
    >,
    pub state: MultiIndex<
        'a, String, Offer, u64
    >,
    // Composite: type+fiat+state
    pub filter: MultiIndex<
        'a, String, Offer, u64
    >,
}

impl<'a> IndexList<Offer> for OfferIndexes<'a> {
    fn get_indexes(
        &'_ self
    ) -> Box<dyn Iterator<Item = &'_ dyn Index<Offer>> + '_> {
        let v: Vec<&dyn Index<Offer>> =
            vec![&self.owner, &self.state, &self.filter];
        Box::new(v.into_iter())
    }
}

// Create indexed map
pub fn offers<'a>() -> IndexedMap<'a, u64, Offer, OfferIndexes<'a>> {
    let indexes = OfferIndexes {
        owner: MultiIndex::new(
            |_, o| o.owner.clone(),
            "offers", "offers__owner"
        ),
        state: MultiIndex::new(
            |_, o| o.state.to_string(),
            "offers", "offers__state"
        ),
        filter: MultiIndex::new(
            |_, o| format!("{}-{}-{}",
                o.offer_type, o.fiat_currency, o.state),
            "offers", "offers__filter"
        ),
    };
    IndexedMap::new("offers", indexes)
}

Solidity Indexing Patterns

contract Offer {
    using EnumerableSet for
        EnumerableSet.UintSet;

    // Primary storage
    mapping(uint256 => OfferData) offers;

    // Index: owner -> offer IDs
    mapping(address => uint256[]) userOffers;

    // Index: state -> offer IDs (set)
    mapping(OfferState => EnumerableSet.UintSet)
        private offersByState;

    // Index: composite filter hash -> IDs
    mapping(bytes32 => EnumerableSet.UintSet)
        private offersByFilter;

    // Helper to compute filter hash
    function _filterHash(
        OfferType offerType,
        string memory fiat,
        OfferState state
    ) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked(
            offerType, fiat, state
        ));
    }

    // Query by filter
    function getOffersByFilter(
        OfferType offerType,
        string calldata fiat,
        OfferState state,
        uint256 limit
    ) external view returns (uint256[] memory) {
        bytes32 hash = _filterHash(
            offerType, fiat, state
        );
        EnumerableSet.UintSet storage set =
            offersByFilter[hash];
        // Return first `limit` items...
    }
}

Solana Client-Side Filtering

// Solana uses getProgramAccounts with
// memcmp filters for efficient queries

// Account layout (bytes):
// [0-8]   discriminator
// [8-16]  id (u64)
// [16-48] owner (Pubkey)
// [48-49] offer_type (u8)
// [49-50] state (u8)
// [50-53] fiat_currency ([u8; 3])

// TypeScript query by owner + state:
const offers = await connection
  .getProgramAccounts(programId, {
    filters: [
      // Filter by discriminator
      {
        memcmp: {
          offset: 0,
          bytes: bs58.encode(OFFER_DISCRIMINATOR)
        }
      },
      // Filter by owner
      {
        memcmp: {
          offset: 16,
          bytes: owner.toBase58()
        }
      },
      // Filter by state (Active = 0)
      {
        memcmp: {
          offset: 49,
          bytes: bs58.encode([0])
        }
      }
    ]
  });

// Anchor provides a nicer API:
const offers = await program.account.offer.all([
  { memcmp: { offset: 16, bytes: owner.toBase58() } }
]);
Indexing Aspect CosmWasm Solidity Solana
Where indexing happens On-chain (IndexedMap) On-chain (mappings/sets) Client-side (memcmp filters)
Storage cost High (duplicate data) High (explicit indexes) Low (no duplicate storage)
Query cost Free (query) Free (view) RPC call (no tx fee)
Composite indexes Built-in support Manual hash keys Multiple memcmp filters

Knowledge Check

Test Your Understanding

1. Where does Solana store contract state?

2. How do you read state in Solana?

3. What is an IndexedMap in CosmWasm used for?