Module 1: Contract Foundations
Understanding project structure, entry points, and basic contract anatomy across CosmWasm, Solidity, and Solana.
1.1 Project Structure
Each platform has its own conventions for organizing smart contract projects. Let's compare how the LocalMoney protocol is structured on each.
CosmWasm (Cargo Workspace)
contracts/cosmwasm/
├── Cargo.toml # Workspace root
├── packages/
│ └── protocol/ # Shared types
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs
│ ├── hub.rs
│ ├── offer.rs
│ ├── trade.rs
│ └── ...
└── contracts/
├── hub/
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs
│ ├── contract.rs
│ ├── state.rs
│ └── error.rs
├── offer/
├── trade/
└── ...
Solidity (Hardhat/Foundry)
contracts/evm/
├── package.json
├── hardhat.config.ts
├── contracts/
│ ├── Hub.sol
│ ├── Offer.sol
│ ├── Trade.sol
│ ├── Escrow.sol
│ ├── Profile.sol
│ └── interfaces/
│ ├── IHub.sol
│ ├── IOffer.sol
│ └── ...
├── scripts/
│ └── deploy.ts
└── test/
├── Hub.test.ts
└── ...
Solana (Anchor)
contracts/solana/
├── Anchor.toml # Workspace config
├── Cargo.toml
├── programs/
│ ├── hub/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── lib.rs
│ │ ├── instructions/
│ │ ├── state/
│ │ └── errors.rs
│ ├── offer/
│ ├── trade/
│ └── escrow/
├── tests/
│ └── localMoney.ts
└── migrations/
└── deploy.ts
Key Insight: Workspace Organization
CosmWasm uses Cargo workspaces with a shared protocol package for types. Solidity uses interfaces for contract interactions. Solana/Anchor also uses Cargo workspaces but each program is fully independent - shared types must be duplicated or use a shared crate.
1.2 Contract Entry Points
Every smart contract needs entry points - the functions that can be called from outside. The approach differs significantly across platforms.
CosmWasm Entry Points
use cosmwasm_std::{
entry_point, Binary, Deps, DepsMut,
Env, MessageInfo, Response, StdResult
};
// Called once when contract is deployed
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> { ... }
// Called for state-changing operations
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> { ... }
// Called for read-only queries
#[entry_point]
pub fn query(
deps: Deps,
env: Env,
msg: QueryMsg,
) -> StdResult<Binary> { ... }
Solidity Entry Points
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract Hub {
// Called once when contract is deployed
constructor(
address _admin,
uint256 _burnFee
) {
admin = _admin;
burnFee = _burnFee;
}
// State-changing function
function updateConfig(
uint256 newBurnFee
) external onlyAdmin {
burnFee = newBurnFee;
emit ConfigUpdated(newBurnFee);
}
// Read-only function (free)
function getConfig()
external view returns (
uint256
)
{
return burnFee;
}
}
Solana/Anchor Entry Points
use anchor_lang::prelude::*;
declare_id!("Hub1111111111111111111");
#[program]
pub mod hub {
use super::*;
// Called to initialize config PDA
pub fn initialize(
ctx: Context<Initialize>,
burn_fee: u16,
) -> Result<()> {
let config = &mut ctx.accounts.config;
config.admin = ctx.accounts.admin.key();
config.burn_fee = burn_fee;
Ok(())
}
// Update config instruction
pub fn update_config(
ctx: Context<UpdateConfig>,
new_burn_fee: u16,
) -> Result<()> {
ctx.accounts.config.burn_fee = new_burn_fee;
Ok(())
}
}
| Aspect | CosmWasm | Solidity | Solana |
|---|---|---|---|
| Initialization | instantiate |
constructor |
initialize instruction |
| State Changes | execute |
Any non-view function | Any instruction |
| Read-only | query (separate entry) |
view functions |
Client-side account reads |
| Caller Info | info.sender |
msg.sender |
ctx.accounts.signer |
| Funds Sent | info.funds |
msg.value |
Separate token transfer |
1.3 The Hub Contract - A Complete Example
Let's look at the Hub contract - the central configuration for the protocol - implemented across all three platforms.
Hub Config Structure
CosmWasm
// packages/protocol/src/hub.rs
#[cw_serde]
pub struct HubConfig {
pub offer_addr: Addr,
pub trade_addr: Addr,
pub profile_addr: Addr,
pub price_addr: Addr,
// Fees (basis points)
pub burn_fee_pct: Decimal,
pub chain_fee_pct: Decimal,
pub warchest_fee_pct: Decimal,
// Trading limits
pub min_trade_amount: Uint128,
pub max_trade_amount: Uint128,
// Timers (seconds)
pub trade_expiration: u64,
pub dispute_timer: u64,
}
Solidity
// contracts/Hub.sol
contract Hub {
struct HubConfig {
address offerContract;
address tradeContract;
address profileContract;
address priceOracle;
// Fees (basis points, 100 = 1%)
uint16 burnFeeBps;
uint16 chainFeeBps;
uint16 warchestFeeBps;
// Trading limits
uint256 minTradeAmount;
uint256 maxTradeAmount;
// Timers (seconds)
uint64 tradeExpiration;
uint64 disputeTimer;
}
HubConfig public config;
}
Solana/Anchor
// programs/hub/src/state/config.rs
#[account]
pub struct HubConfig {
pub admin: Pubkey,
pub offer_program: Pubkey,
pub trade_program: Pubkey,
pub profile_program: Pubkey,
pub price_program: Pubkey,
// Fees (basis points)
pub burn_fee_bps: u16,
pub chain_fee_bps: u16,
pub warchest_fee_bps: u16,
// Trading limits (lamports)
pub min_trade_amount: u64,
pub max_trade_amount: u64,
// Timers (seconds)
pub trade_expiration: i64,
pub dispute_timer: i64,
pub bump: u8, // PDA bump
}
Notice the bump field in Solana. PDAs (Program Derived Addresses) require a bump seed to find a valid off-curve address. This must be stored to recreate the PDA address later.
Initialization Logic
CosmWasm
#[entry_point]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
// Validate fee percentages
if msg.burn_fee_pct > Decimal::percent(5) {
return Err(StdError::generic_err(
"Burn fee cannot exceed 5%"
));
}
let config = HubConfig {
offer_addr: deps.api.addr_validate(
&msg.offer_addr
)?,
burn_fee_pct: msg.burn_fee_pct,
// ... other fields
};
// Save to storage
CONFIG.save(deps.storage, &config)?;
ADMIN.save(deps.storage, &info.sender)?;
Ok(Response::new()
.add_attribute("action", "instantiate")
.add_attribute("admin", info.sender))
}
Solidity (UUPS Upgradeable)
function initialize(
address _admin,
address _offerContract,
uint16 _burnFeeBps
) external initializer {
// OpenZeppelin initializers
__UUPSUpgradeable_init();
__AccessControl_init();
__ReentrancyGuard_init();
// Validate fee
require(
_burnFeeBps <= MAX_BURN_FEE,
"Burn fee exceeds maximum"
);
// Grant admin role
_grantRole(ADMIN_ROLE, _admin);
// Set config
config = HubConfig({
offerContract: _offerContract,
burnFeeBps: _burnFeeBps,
// ... other fields
});
emit Initialized(_admin);
}
Solana/Anchor
pub fn initialize(
ctx: Context<Initialize>,
burn_fee_bps: u16,
) -> Result<()> {
// Validate fee
require!(
burn_fee_bps <= MAX_BURN_FEE,
HubError::FeeTooHigh
);
let config = &mut ctx.accounts.config;
config.admin = ctx.accounts.admin.key();
config.burn_fee_bps = burn_fee_bps;
config.bump = ctx.bumps.config;
// ... other fields
Ok(())
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub admin: Signer<'info>,
#[account(
init,
payer = admin,
space = 8 + HubConfig::INIT_SPACE,
seeds = [b"hub_config"],
bump
)]
pub config: Account<'info, HubConfig>,
pub system_program: Program<'info, System>,
}
Account Contexts in Solana
The #[derive(Accounts)] struct is unique to Anchor. It defines ALL accounts the instruction needs, with constraints validated automatically. The seeds attribute defines the PDA derivation path. This is a major paradigm shift from CosmWasm/Solidity where you just access storage directly.
1.4 Compilation & Deployment
CosmWasm
# Build optimized WASM
$ cargo build --release \
--target wasm32-unknown-unknown
# Or use optimizer (recommended)
$ docker run --rm -v "$(pwd)":/code \
cosmwasm/rust-optimizer:0.14.0
# Store code on chain
$ wasmd tx wasm store \
artifacts/hub.wasm \
--from wallet --gas auto
# Instantiate contract
$ wasmd tx wasm instantiate \
$CODE_ID \
'{"offer_addr":"...", ...}' \
--label "hub" \
--admin $ADMIN \
--from wallet
Solidity (Hardhat)
// Compile
$ npx hardhat compile
// Deploy script (deploy.ts)
async function main() {
const Hub = await ethers
.getContractFactory("Hub");
// Deploy proxy (UUPS)
const hub = await upgrades
.deployProxy(Hub, [
admin,
offerAddr,
500 // 5% burn fee
], { kind: 'uups' });
await hub.waitForDeployment();
console.log(`Hub: ${hub.target}`);
}
// Run deployment
$ npx hardhat run scripts/deploy.ts \
--network mainnet
Solana (Anchor)
# Build all programs
$ anchor build
# Deploy to devnet
$ anchor deploy --provider.cluster devnet
# Or use Anchor.toml config
[programs.devnet]
hub = "Hub111111111111111111"
[provider]
cluster = "devnet"
wallet = "~/.config/solana/id.json"
# Initialize via TypeScript
const tx = await program.methods
.initialize(500) // burn fee bps
.accounts({
admin: wallet.publicKey,
config: configPda,
systemProgram: SystemProgram.programId,
})
.rpc();
Knowledge Check
Test Your Understanding
1. In Solana/Anchor, what is a PDA?
2. What is the CosmWasm equivalent of Solidity's constructor?
3. How do you get the caller's address in each platform?