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
}
Solana 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?