Module 2: State Management
How to define, store, query, and index state across CosmWasm, Solidity, and Solana.
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]
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: [...]
});
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?