Module 3: Messages & Instructions
How contracts receive and process inputs - ExecuteMsg vs functions vs Anchor instructions.
3.1 Defining Contract Messages
Each platform has a different approach to defining the interface for interacting with contracts:
CosmWasm - Enum Messages
use cosmwasm_schema::{cw_serde, QueryResponses};
// All execute operations in one enum
#[cw_serde]
pub enum ExecuteMsg {
// Offer operations
CreateOffer {
offer_type: OfferType,
fiat_currency: FiatCurrency,
min_amount: Uint128,
max_amount: Uint128,
rate: Decimal,
},
UpdateOffer {
id: u64,
rate: Option<Decimal>,
min_amount: Option<Uint128>,
max_amount: Option<Uint128>,
},
PauseOffer { id: u64 },
ResumeOffer { id: u64 },
DeleteOffer { id: u64 },
}
// Query messages with response types
#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
#[returns(Offer)]
Offer { id: u64 },
#[returns(Vec<Offer>)]
OffersByOwner {
owner: String,
limit: Option<u32>,
},
}
Solidity - Function Signatures
interface IOffer {
// Events define the ABI
event OfferCreated(
uint256 indexed id,
address indexed owner,
OfferType offerType
);
// Offer operations as functions
function createOffer(
OfferType offerType,
string calldata fiatCurrency,
uint256 minAmount,
uint256 maxAmount,
uint256 rate
) external returns (uint256);
function updateOffer(
uint256 id,
uint256 rate,
uint256 minAmount,
uint256 maxAmount
) external;
function pauseOffer(uint256 id) external;
function resumeOffer(uint256 id) external;
function deleteOffer(uint256 id) external;
// View functions (queries)
function getOffer(uint256 id)
external view returns (OfferData memory);
function getOffersByOwner(address owner)
external view returns (uint256[] memory);
}
Solana - Anchor Instructions
#[program]
pub mod offer {
use super::*;
// Each instruction is a function
pub fn create_offer(
ctx: Context<CreateOffer>,
offer_type: OfferType,
fiat_currency: [u8; 3],
min_amount: u64,
max_amount: u64,
rate: u64,
) -> Result<()> { ... }
pub fn update_offer(
ctx: Context<UpdateOffer>,
rate: Option<u64>,
min_amount: Option<u64>,
max_amount: Option<u64>,
) -> Result<()> { ... }
pub fn pause_offer(
ctx: Context<PauseOffer>,
) -> Result<()> { ... }
pub fn resume_offer(
ctx: Context<ResumeOffer>,
) -> Result<()> { ... }
pub fn delete_offer(
ctx: Context<DeleteOffer>,
) -> Result<()> { ... }
}
// No query functions - reads are client-side
Pattern Comparison
CosmWasm uses a single enum to define all messages, with pattern matching to route them. Solidity uses individual functions with visibility modifiers. Anchor uses functions decorated with #[program], each with its own account context struct.
3.2 Processing Messages
How each platform routes incoming messages to handlers:
CosmWasm - Pattern Matching
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
// Route to handler based on variant
match msg {
ExecuteMsg::CreateOffer {
offer_type,
fiat_currency,
min_amount,
max_amount,
rate,
} => execute_create_offer(
deps, env, info,
offer_type, fiat_currency,
min_amount, max_amount, rate
),
ExecuteMsg::UpdateOffer {
id, rate, min_amount, max_amount
} => execute_update_offer(
deps, info, id, rate,
min_amount, max_amount
),
ExecuteMsg::PauseOffer { id } =>
execute_pause_offer(deps, info, id),
ExecuteMsg::ResumeOffer { id } =>
execute_resume_offer(deps, info, id),
ExecuteMsg::DeleteOffer { id } =>
execute_delete_offer(deps, info, id),
}
}
Solidity - Direct Dispatch
contract Offer is IOffer {
// EVM routes by function selector
// (first 4 bytes of keccak256(sig))
function createOffer(
OfferType offerType,
string calldata fiatCurrency,
uint256 minAmount,
uint256 maxAmount,
uint256 rate
) external
whenNotPaused
nonReentrant
returns (uint256) {
// Validate inputs
require(
minAmount < maxAmount,
"Invalid amounts"
);
require(rate > 0, "Invalid rate");
// Create offer...
uint256 id = _createOffer(
msg.sender,
offerType,
fiatCurrency,
minAmount,
maxAmount,
rate
);
emit OfferCreated(id, msg.sender, offerType);
return id;
}
// Each function is independently callable
function pauseOffer(uint256 id)
external onlyOfferOwner(id)
{
offers[id].state = OfferState.Paused;
emit OfferPaused(id);
}
}
Solana - Instruction Discriminator
// Anchor generates discriminators from
// function names (first 8 bytes of sha256)
#[program]
pub mod offer {
pub fn create_offer(
ctx: Context<CreateOffer>,
offer_type: OfferType,
fiat_currency: [u8; 3],
min_amount: u64,
max_amount: u64,
rate: u64,
) -> Result<()> {
// Validate
require!(
min_amount < max_amount,
OfferError::InvalidAmounts
);
require!(rate > 0, OfferError::InvalidRate);
// Initialize offer account
let offer = &mut ctx.accounts.offer;
offer.owner = ctx.accounts.owner.key();
offer.offer_type = offer_type;
offer.fiat_currency = fiat_currency;
offer.min_amount = min_amount;
offer.max_amount = max_amount;
offer.rate = rate;
offer.state = OfferState::Active;
// Emit event
emit!(OfferCreated {
id: offer.id,
owner: offer.owner,
offer_type,
});
Ok(())
}
}
3.3 Solana Account Contexts
Solana's most unique feature is the Account Context - defining ALL accounts an instruction needs upfront:
CosmWasm - Implicit Access
// CosmWasm accesses storage implicitly
// through deps.storage
pub fn execute_update_offer(
deps: DepsMut,
info: MessageInfo,
id: u64,
rate: Option<Decimal>,
min_amount: Option<Uint128>,
max_amount: Option<Uint128>,
) -> Result<Response, ContractError> {
// Load offer from storage
let mut offer = OFFERS.load(
deps.storage, id
)?;
// Check ownership
if offer.owner != info.sender {
return Err(
ContractError::Unauthorized {}
);
}
// Update fields
if let Some(r) = rate {
offer.rate = r;
}
if let Some(min) = min_amount {
offer.min_amount = min;
}
if let Some(max) = max_amount {
offer.max_amount = max;
}
// Save back
OFFERS.save(deps.storage, id, &offer)?;
Ok(Response::new())
}
Solidity - Implicit Access
// Solidity accesses storage implicitly
// through state variables
function updateOffer(
uint256 id,
uint256 rate,
uint256 minAmount,
uint256 maxAmount
) external {
// Load from mapping
OfferData storage offer = offers[id];
// Check ownership
require(
offer.owner == msg.sender,
"Not offer owner"
);
// Update fields (0 = no change)
if (rate > 0) {
offer.rate = rate;
}
if (minAmount > 0) {
offer.minAmount = minAmount;
}
if (maxAmount > 0) {
offer.maxAmount = maxAmount;
}
// Storage automatically persisted
emit OfferUpdated(id);
}
Solana - Explicit Accounts
// ALL accounts must be declared upfront!
#[derive(Accounts)]
pub struct UpdateOffer<'info> {
// Signer (owner)
#[account(mut)]
pub owner: Signer<'info>,
// The offer account to update
#[account(
mut,
seeds = [
b"offer",
owner.key().as_ref(),
&offer.id.to_le_bytes()
],
bump = offer.bump,
has_one = owner @ OfferError::NotOwner
)]
pub offer: Account<'info, Offer>,
}
pub fn update_offer(
ctx: Context<UpdateOffer>,
rate: Option<u64>,
min_amount: Option<u64>,
max_amount: Option<u64>,
) -> Result<()> {
// Account validation done by derive!
// Owner check: has_one = owner
// PDA validation: seeds + bump
let offer = &mut ctx.accounts.offer;
if let Some(r) = rate {
offer.rate = r;
}
if let Some(min) = min_amount {
offer.min_amount = min;
}
if let Some(max) = max_amount {
offer.max_amount = max;
}
Ok(())
}
Anchor provides powerful declarative constraints like has_one, constraint, and seeds that validate accounts before your instruction runs. This moves validation from imperative code to declarations.
3.4 Input Validation
CosmWasm Validation
pub fn execute_create_offer(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: CreateOfferMsg,
) -> Result<Response, ContractError> {
// Query hub for config
let hub_config: HubConfig = deps.querier
.query_wasm_smart(
hub_addr,
&HubQueryMsg::Config {}
)?;
// Validate amounts
if msg.min_amount >= msg.max_amount {
return Err(
ContractError::InvalidAmounts {}
);
}
// Check against hub limits
if msg.min_amount < hub_config.min_trade_amount {
return Err(
ContractError::AmountTooSmall {}
);
}
// Validate rate
if msg.rate.is_zero() {
return Err(
ContractError::InvalidRate {}
);
}
// Validate fiat currency
if !FiatCurrency::is_valid(&msg.fiat_currency) {
return Err(
ContractError::InvalidFiatCurrency {}
);
}
// Proceed with creation...
Ok(Response::new())
}
Solidity Validation
contract Offer {
// Custom errors (gas efficient)
error InvalidAmounts();
error AmountTooSmall(uint256 min);
error InvalidRate();
function createOffer(
OfferType offerType,
string calldata fiatCurrency,
uint256 minAmount,
uint256 maxAmount,
uint256 rate
) external returns (uint256) {
// Get hub config
IHub.HubConfig memory hubConfig =
IHub(hub).getConfig();
// Validate with require
if (minAmount >= maxAmount) {
revert InvalidAmounts();
}
if (minAmount < hubConfig.minTradeAmount) {
revert AmountTooSmall(
hubConfig.minTradeAmount
);
}
if (rate == 0) {
revert InvalidRate();
}
// Validate fiat (length check)
require(
bytes(fiatCurrency).length == 3,
"Invalid fiat code"
);
// Proceed...
}
}
Solana Validation
// Error enum
#[error_code]
pub enum OfferError {
#[msg("Min amount must be less than max")]
InvalidAmounts,
#[msg("Amount below minimum")]
AmountTooSmall,
#[msg("Rate must be positive")]
InvalidRate,
#[msg("Not the offer owner")]
NotOwner,
}
#[derive(Accounts)]
pub struct CreateOffer<'info> {
// Hub config for validation
#[account(
seeds = [b"hub_config"],
bump = hub_config.bump,
seeds::program = hub_program.key()
)]
pub hub_config: Account<'info, HubConfig>,
// Other accounts...
}
pub fn create_offer(
ctx: Context<CreateOffer>,
min_amount: u64,
max_amount: u64,
rate: u64,
) -> Result<()> {
let hub = &ctx.accounts.hub_config;
// Validate with require!
require!(
min_amount < max_amount,
OfferError::InvalidAmounts
);
require!(
min_amount >= hub.min_trade_amount,
OfferError::AmountTooSmall
);
require!(rate > 0, OfferError::InvalidRate);
// Proceed...
Ok(())
}
| Validation Aspect | CosmWasm | Solidity | Solana |
|---|---|---|---|
| Error Type | Custom enum with thiserror |
Custom errors or require |
#[error_code] enum |
| Return Early | return Err(...) |
revert / require |
require! macro |
| Cross-contract Query | deps.querier.query_wasm_smart |
Direct interface call | Pass account in context |
Knowledge Check
Test Your Understanding
1. What is a "discriminator" in Anchor?
2. Why must Solana instructions declare all accounts upfront?
3. What does has_one = owner do in an Anchor account constraint?