Module 7: Advanced Patterns
Upgradeability, cross-contract calls, fee distribution, and oracle integration.
7.1 Contract Upgradeability
How to upgrade contracts without losing state:
CosmWasm Migration
// CosmWasm has built-in migration support
#[entry_point]
pub fn migrate(
deps: DepsMut,
_env: Env,
msg: MigrateMsg,
) -> Result<Response, ContractError> {
// Check version compatibility
let ver = cw2::get_contract_version(deps.storage)?;
if ver.contract != CONTRACT_NAME {
return Err(ContractError::WrongContract {});
}
// Handle version-specific migrations
if ver.version.as_str() < "0.2.0" {
// Migrate from v0.1.x to v0.2.x
migrate_v1_to_v2(deps.storage)?;
}
// Update version
cw2::set_contract_version(
deps.storage,
CONTRACT_NAME,
CONTRACT_VERSION,
)?;
Ok(Response::new()
.add_attribute("action", "migrate")
.add_attribute("from", ver.version)
.add_attribute("to", CONTRACT_VERSION))
}
// State migration helper
fn migrate_v1_to_v2(
storage: &mut dyn Storage,
) -> StdResult<()> {
// Read old state format
let old_config: ConfigV1 = OLD_CONFIG.load(storage)?;
// Convert to new format
let new_config = ConfigV2 {
admin: old_config.admin,
burn_fee_pct: old_config.burn_fee_pct,
// New field with default
max_active_offers: 10,
};
// Save new format
CONFIG.save(storage, &new_config)?;
Ok(())
}
// CLI to migrate:
// wasmd tx wasm migrate $CONTRACT $NEW_CODE_ID '{}'
// --from admin
Solidity UUPS Proxy
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract HubV1 is
Initializable,
UUPSUpgradeable,
AccessControlUpgradeable
{
// Storage slot 0
address public admin;
// Initialize instead of constructor
function initialize(address _admin)
external initializer
{
__UUPSUpgradeable_init();
__AccessControl_init();
admin = _admin;
}
// Required: authorization for upgrades
function _authorizeUpgrade(
address newImplementation
) internal override onlyRole(ADMIN_ROLE) {
// Timelock enforcement (security!)
require(
timelockController.isOperationReady(
upgradeProposalHash
),
"Upgrade not ready"
);
}
}
// V2 with new storage
contract HubV2 is HubV1 {
// NEW storage must come AFTER existing
uint256 public maxActiveOffers; // slot 1
// Reinitializer for V2-specific setup
function initializeV2(uint256 _max)
external reinitializer(2)
{
maxActiveOffers = _max;
}
}
// Deploy V2:
// 1. Deploy new implementation
// 2. Schedule upgrade via timelock
// 3. Wait timelock delay
// 4. Execute upgrade
// 5. Call initializeV2()
Solana Program Upgrades
// Solana programs CAN be upgraded if deployed
// with an upgrade authority
// Anchor.toml:
// [programs.mainnet]
// hub = "Hub11111111111111111111"
// Deploy with upgrade authority:
// $ anchor deploy --provider.cluster mainnet
// Upgrade to new code:
// $ anchor upgrade target/deploy/hub.so
// --program-id Hub11111111111111111111
// Account versioning for migrations:
#[account]
pub struct HubConfigV2 {
pub version: u8, // Always first!
pub admin: Pubkey,
pub burn_fee_bps: u16,
// New fields in V2
pub max_active_offers: u16,
pub bump: u8,
}
// Migration instruction
pub fn migrate_config(
ctx: Context<MigrateConfig>
) -> Result<()> {
let config = &mut ctx.accounts.config;
// Check version
require!(
config.version == 1,
HubError::AlreadyMigrated
);
// Set new fields with defaults
config.max_active_offers = 10;
config.version = 2;
Ok(())
}
// Make program immutable (for security):
// $ solana program set-upgrade-authority
// Hub1111111111111111111
// --final
Storage Layout Matters!
In Solidity, you MUST NOT change the order of existing storage variables or insert new ones between them. Always append new storage at the end. Use storage gaps (uint256[50] __gap;) for future-proofing.
7.2 Cross-Contract Communication
CosmWasm SubMessages
// Query another contract
pub fn query_hub_config(
deps: Deps,
hub_addr: &Addr,
) -> StdResult<HubConfig> {
deps.querier.query_wasm_smart(
hub_addr,
&HubQueryMsg::Config {},
)
}
// Execute on another contract
pub fn call_profile_update(
profile_addr: &Addr,
user: &Addr,
trade_count: u64,
) -> CosmosMsg {
WasmMsg::Execute {
contract_addr: profile_addr.to_string(),
msg: to_binary(&ProfileExecuteMsg::UpdateStats {
user: user.to_string(),
trade_count,
}).unwrap(),
funds: vec![],
}.into()
}
// SubMessage with reply handling
const REPLY_SWAP: u64 = 1;
pub fn execute_with_reply(...) -> Result<...> {
let swap_msg = WasmMsg::Execute { ... };
let submsg = SubMsg::reply_on_success(
swap_msg,
REPLY_SWAP,
);
Ok(Response::new().add_submessage(submsg))
}
#[entry_point]
pub fn reply(
deps: DepsMut,
_env: Env,
msg: Reply,
) -> Result<Response, ContractError> {
match msg.id {
REPLY_SWAP => handle_swap_reply(deps, msg),
_ => Err(ContractError::UnknownReply {}),
}
}
Solidity Interface Calls
// Define interface
interface IHub {
function getConfig() external view
returns (HubConfig memory);
function isOperationPaused(bytes32 op)
external view returns (bool);
}
interface IProfile {
function updateTradeStats(
address user,
uint256 tradeCount
) external;
}
contract Trade {
IHub public immutable hub;
IProfile public immutable profile;
constructor(address _hub, address _profile) {
hub = IHub(_hub);
profile = IProfile(_profile);
}
function completeTrade(uint256 tradeId)
external
{
// Query hub config
HubConfig memory config = hub.getConfig();
// Check pause status
require(
!hub.isOperationPaused(OP_RELEASE),
"Operation paused"
);
// Update profile via call
profile.updateTradeStats(
trades[tradeId].buyer,
1
);
// Low-level call for safety
(bool success, ) = address(profile).call(
abi.encodeWithSelector(
IProfile.updateTradeStats.selector,
user,
count
)
);
require(success, "Profile call failed");
}
}
Solana CPI
use anchor_lang::prelude::*;
use profile::{
cpi::accounts::UpdateStats,
program::Profile,
};
// CPI to profile program
pub fn complete_trade(
ctx: Context<CompleteTrade>
) -> Result<()> {
// Hub config is passed in as account
let hub = &ctx.accounts.hub_config;
// Verify not paused
require!(
!hub.pause_escrow_release,
TradeError::OperationPaused
);
// CPI to profile program
let cpi_program = ctx.accounts
.profile_program
.to_account_info();
let cpi_accounts = UpdateStats {
authority: ctx.accounts.trade.to_account_info(),
profile: ctx.accounts.buyer_profile.to_account_info(),
};
// Sign with trade PDA
let seeds = &[
b"trade".as_ref(),
&ctx.accounts.trade.id.to_le_bytes(),
&[ctx.accounts.trade.bump],
];
let signer = &[&seeds[..]];
let cpi_ctx = CpiContext::new_with_signer(
cpi_program,
cpi_accounts,
signer,
);
profile::cpi::update_stats(cpi_ctx, 1)?;
Ok(())
}
#[derive(Accounts)]
pub struct CompleteTrade<'info> {
// Hub config from another program
#[account(
seeds = [b"hub_config"],
bump,
seeds::program = hub_program.key()
)]
pub hub_config: Account<'info, HubConfig>,
pub hub_program: Program<'info, Hub>,
pub profile_program: Program<'info, Profile>,
// ... other accounts
}
7.3 Events & Logging
CosmWasm Attributes & Events
// Simple attributes
Ok(Response::new()
.add_attribute("action", "create_trade")
.add_attribute("trade_id", id.to_string())
.add_attribute("buyer", buyer.to_string())
.add_attribute("seller", seller.to_string())
.add_attribute("amount", amount.to_string()))
// Structured events (preferred)
use cosmwasm_std::Event;
let event = Event::new("trade_created")
.add_attribute("trade_id", id.to_string())
.add_attribute("buyer", buyer.to_string())
.add_attribute("seller", seller.to_string())
.add_attribute("amount", amount.to_string())
.add_attribute("fiat_amount", fiat.to_string());
Ok(Response::new()
.add_event(event)
.add_attribute("action", "create_trade"))
// Query events via:
// tx.logs[].events[].attributes[]
Solidity Events
// Event definitions
event TradeCreated(
uint256 indexed tradeId,
uint256 indexed offerId,
address indexed buyer,
address seller,
uint256 amount,
uint256 fiatAmount,
string fiatCurrency
);
event TradeStateChanged(
uint256 indexed tradeId,
TradeState fromState,
TradeState toState,
address actor
);
event EscrowFunded(
uint256 indexed tradeId,
uint256 amount
);
// Emit in function
function createTrade(...) external {
// ... logic
emit TradeCreated(
id,
offerId,
msg.sender, // buyer
offer.owner, // seller
amount,
fiatAmount,
fiatCurrency
);
}
// Index up to 3 parameters for filtering
// Query: contract.filters.TradeCreated(tradeId)
Solana Anchor Events
// Define event struct
#[event]
pub struct TradeCreated {
pub trade_id: u64,
pub offer_id: u64,
pub buyer: Pubkey,
pub seller: Pubkey,
pub amount: u64,
pub fiat_amount: u64,
pub fiat_currency: [u8; 3],
pub timestamp: i64,
}
#[event]
pub struct TradeStateChanged {
pub trade_id: u64,
pub from_state: TradeState,
pub to_state: TradeState,
pub actor: Pubkey,
pub timestamp: i64,
}
// Emit in instruction
pub fn create_trade(
ctx: Context<CreateTrade>,
...
) -> Result<()> {
let clock = Clock::get()?;
// ... logic
emit!(TradeCreated {
trade_id: trade.id,
offer_id: trade.offer_id,
buyer: trade.buyer,
seller: trade.seller,
amount: trade.amount,
fiat_amount: trade.fiat_amount,
fiat_currency: trade.fiat_currency,
timestamp: clock.unix_timestamp,
});
Ok(())
}
// Client: program.addEventListener("TradeCreated", (e) => {...})
7.4 Quick Reference Summary
| Feature | CosmWasm | Solidity | Solana |
|---|---|---|---|
| Language | Rust | Solidity | Rust (Anchor) |
| Entry Points | instantiate, execute, query |
Functions with visibility | #[program] instructions |
| State Storage | cw_storage_plus Items/Maps |
Contract storage slots | Account PDAs |
| Caller Identity | info.sender |
msg.sender |
Signer account |
| Funds Sent | info.funds |
msg.value |
Separate token transfer |
| Cross-Contract | SubMessages + Queries | Interface calls | CPI |
| Token Standard | CW20 / Bank | ERC-20 | SPL Token |
| Reentrancy | Safe by design | Use guards + CEI | Safe by design |
| Upgradeability | Built-in migration | Proxy patterns | Upgrade authority |
| Events | Attributes + Events | event + emit |
#[event] + emit! |
Congratulations!
Course Complete!
You've completed the LocalMoney Smart Contract Masterclass. You now understand:
- Contract foundations and project structure across all three platforms
- State management: storage, indexing, and queries
- Message/instruction handling and validation
- Escrow implementation and token custody
- Trade lifecycle and state machines
- Security patterns and platform-specific vulnerabilities
- Advanced patterns: upgradeability, CPI, and events
Use this knowledge to build, audit, or extend the LocalMoney protocol across any of these platforms!
Final Knowledge Check
Test Your Understanding
1. Why must new storage variables in Solidity upgrades be appended at the end?
2. What is CPI in Solana?
3. Which platform uses the reply entry point for handling cross-contract call responses?