Module 6: Security Patterns
Access control, reentrancy guards, circuit breakers, and platform-specific vulnerabilities.
6.1 Access Control
Protecting admin functions and ensuring only authorized users can perform certain actions:
CosmWasm Access Control
// Simple owner check
pub const ADMIN: Item<Addr> = Item::new("admin");
pub fn assert_admin(
deps: Deps,
sender: &Addr,
) -> Result<(), ContractError> {
let admin = ADMIN.load(deps.storage)?;
if *sender != admin {
return Err(ContractError::Unauthorized {});
}
Ok(())
}
// Usage in execute
pub fn execute_update_config(
deps: DepsMut,
info: MessageInfo,
new_config: ConfigUpdate,
) -> Result<Response, ContractError> {
// Check admin first
assert_admin(deps.as_ref(), &info.sender)?;
// Proceed with update...
Ok(Response::new())
}
// Guard module pattern
pub mod guards {
pub fn assert_offer_owner(
offer: &Offer,
sender: &Addr,
) -> Result<(), ContractError> {
if offer.owner != *sender {
return Err(ContractError::NotOfferOwner {});
}
Ok(())
}
pub fn assert_trade_party(
trade: &Trade,
sender: &Addr,
) -> Result<(), ContractError> {
if *sender != trade.buyer && *sender != trade.seller {
return Err(ContractError::NotTradeParty {});
}
Ok(())
}
}
Solidity Access Control
import "@openzeppelin/contracts/access/AccessControl.sol";
contract Hub is AccessControl {
// Role definitions
bytes32 public constant ADMIN_ROLE =
keccak256("ADMIN_ROLE");
bytes32 public constant EMERGENCY_ROLE =
keccak256("EMERGENCY_ROLE");
constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(ADMIN_ROLE, admin);
}
// Modifier-based access control
modifier onlyAdmin() {
require(
hasRole(ADMIN_ROLE, msg.sender),
"Not admin"
);
_;
}
modifier onlyEmergency() {
require(
hasRole(EMERGENCY_ROLE, msg.sender),
"Not emergency role"
);
_;
}
function updateConfig(...)
external onlyAdmin
{ ... }
function emergencyPause()
external onlyEmergency
{ ... }
}
contract Trade {
// Custom modifiers
modifier onlyTradeParty(uint256 tradeId) {
TradeData storage t = trades[tradeId];
require(
msg.sender == t.buyer ||
msg.sender == t.seller,
"Not trade party"
);
_;
}
modifier onlyOfferOwner(uint256 offerId) {
require(
msg.sender == offers[offerId].owner,
"Not offer owner"
);
_;
}
}
Solana Access Control
// Access control via account constraints
#[derive(Accounts)]
pub struct UpdateConfig<'info> {
// Admin must sign
#[account(
constraint = admin.key() == hub_config.admin
@ HubError::Unauthorized
)]
pub admin: Signer<'info>,
#[account(mut)]
pub hub_config: Account<'info, HubConfig>,
}
// has_one constraint for ownership
#[derive(Accounts)]
pub struct UpdateOffer<'info> {
pub owner: Signer<'info>,
#[account(
mut,
has_one = owner @ OfferError::NotOwner
)]
pub offer: Account<'info, Offer>,
}
// Trade party validation
#[derive(Accounts)]
pub struct ConfirmFiat<'info> {
#[account(
constraint = caller.key() == trade.buyer ||
caller.key() == trade.seller
@ TradeError::NotTradeParty
)]
pub caller: Signer<'info>,
#[account(mut)]
pub trade: Account<'info, Trade>,
}
// In instruction handler
pub fn some_admin_action(
ctx: Context<AdminAction>
) -> Result<()> {
// Admin constraint enforced by derive(Accounts)
// If we reach here, caller is verified admin
Ok(())
}
6.2 Reentrancy Protection
Preventing attackers from calling back into the contract during execution:
CosmWasm (Safe by Design)
// CosmWasm is NOT vulnerable to reentrancy!
// The execution model prevents it:
//
// 1. Messages are queued, not executed inline
// 2. State changes commit only after success
// 3. Callbacks happen in separate transactions
pub fn execute_release(
deps: DepsMut,
trade_id: u64,
) -> Result<Response, ContractError> {
let mut trade = TRADES.load(deps.storage, trade_id)?;
// Update state first (safe, no reentrancy)
trade.state = TradeState::EscrowReleased;
TRADES.save(deps.storage, trade_id, &trade)?;
// Queue bank message (executes AFTER this fn)
let msg = BankMsg::Send {
to_address: trade.buyer.to_string(),
amount: vec![Coin {
denom: trade.denom,
amount: trade.amount,
}],
};
// Even if BankMsg calls back, state is updated
Ok(Response::new()
.add_message(CosmosMsg::Bank(msg)))
}
// For cross-contract calls that need responses,
// use SubMsg with Reply handling
let submsg = SubMsg::reply_on_success(
wasm_execute_msg,
REPLY_ID,
);
Solidity (Vulnerable!)
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Escrow is ReentrancyGuard {
// BAD - Vulnerable to reentrancy!
function releaseUnsafe(uint256 tradeId)
external
{
uint256 amount = escrowBalances[tradeId];
address buyer = trades[tradeId].buyer;
// External call BEFORE state update
IERC20(token).transfer(buyer, amount);
// State update AFTER - attacker can reenter!
escrowBalances[tradeId] = 0;
}
// GOOD - CEI Pattern (Checks-Effects-Interactions)
function releaseSafe(uint256 tradeId)
external
nonReentrant // OpenZeppelin guard
{
// CHECKS
uint256 amount = escrowBalances[tradeId];
require(amount > 0, "No balance");
// EFFECTS - update state first!
escrowBalances[tradeId] = 0;
trades[tradeId].state = TradeState.Released;
// INTERACTIONS - external call last
IERC20(token).safeTransfer(
trades[tradeId].buyer,
amount
);
}
}
// ReentrancyGuard uses a mutex
abstract contract ReentrancyGuard {
uint256 private _status;
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
modifier nonReentrant() {
require(_status != _ENTERED);
_status = _ENTERED;
_;
_status = _NOT_ENTERED;
}
}
Solana (Safe by Design)
// Solana is NOT vulnerable to reentrancy!
// Why?
//
// 1. Each instruction runs atomically
// 2. Account borrowing rules (single mutable ref)
// 3. CPI calls are synchronous but isolated
pub fn release_escrow(
ctx: Context<ReleaseEscrow>
) -> Result<()> {
let trade = &mut ctx.accounts.trade;
// Update state
trade.state = TradeState::EscrowReleased;
// CPI to token program
// Even though this is "external", the trade
// account is already borrowed mutably here
// - no one else can modify it
token::transfer(
ctx.accounts.into_transfer_context(),
trade.amount,
)?;
// No reentrancy possible because:
// - trade account is borrowed mutably
// - any attempt to call back would fail
// the account borrow check
Ok(())
}
// Solana's real risks are different:
// - Missing signer checks
// - Missing owner checks on accounts
// - Account confusion attacks
// - Missing account initialization checks
The DAO hack that led to Ethereum's fork was a reentrancy attack. Always use the CEI pattern and consider ReentrancyGuard for any function that makes external calls and modifies state.
6.3 Circuit Breakers & Pause Functionality
CosmWasm Circuit Breaker
#[cw_serde]
pub struct CircuitBreaker {
pub global_pause: bool,
pub pause_new_offers: bool,
pub pause_new_trades: bool,
pub pause_escrow_funding: bool,
pub pause_escrow_release: bool,
}
pub const CIRCUIT_BREAKER: Item<CircuitBreaker> =
Item::new("circuit_breaker");
pub fn assert_not_paused(
deps: Deps,
operation: Operation,
) -> Result<(), ContractError> {
let cb = CIRCUIT_BREAKER.load(deps.storage)?;
if cb.global_pause {
return Err(ContractError::Paused {});
}
match operation {
Operation::CreateOffer if cb.pause_new_offers =>
Err(ContractError::OperationPaused {}),
Operation::CreateTrade if cb.pause_new_trades =>
Err(ContractError::OperationPaused {}),
Operation::FundEscrow if cb.pause_escrow_funding =>
Err(ContractError::OperationPaused {}),
Operation::ReleaseEscrow if cb.pause_escrow_release =>
Err(ContractError::OperationPaused {}),
_ => Ok(()),
}
}
pub fn execute_create_offer(...) -> Result<...> {
assert_not_paused(deps.as_ref(), Operation::CreateOffer)?;
// ... rest of function
}
Solidity Circuit Breaker
import "@openzeppelin/contracts/security/Pausable.sol";
contract Hub is Pausable, AccessControl {
// Fine-grained operation pausing
bytes32 public constant OP_CREATE_OFFER =
keccak256("CREATE_OFFER");
bytes32 public constant OP_CREATE_TRADE =
keccak256("CREATE_TRADE");
bytes32 public constant OP_FUND_ESCROW =
keccak256("FUND_ESCROW");
bytes32 public constant OP_RELEASE_ESCROW =
keccak256("RELEASE_ESCROW");
mapping(bytes32 => bool) public operationPaused;
modifier whenOperationNotPaused(bytes32 op) {
require(!paused(), "Globally paused");
require(
!operationPaused[op],
"Operation paused"
);
_;
}
function pauseOperation(bytes32 op)
external onlyRole(EMERGENCY_ROLE)
{
operationPaused[op] = true;
emit OperationPaused(op);
}
function unpauseOperation(bytes32 op)
external onlyRole(ADMIN_ROLE)
{
operationPaused[op] = false;
emit OperationUnpaused(op);
}
// Global emergency pause
function emergencyPause()
external onlyRole(EMERGENCY_ROLE)
{
_pause();
}
}
Solana Circuit Breaker
#[account]
pub struct HubConfig {
pub admin: Pubkey,
// Circuit breaker flags
pub global_pause: bool,
pub pause_new_offers: bool,
pub pause_new_trades: bool,
pub pause_escrow_funding: bool,
pub pause_escrow_release: bool,
// ... other fields
}
// Check via constraint
#[derive(Accounts)]
pub struct CreateOffer<'info> {
#[account(
constraint = !hub_config.global_pause
@ HubError::GloballyPaused,
constraint = !hub_config.pause_new_offers
@ HubError::OperationPaused
)]
pub hub_config: Account<'info, HubConfig>,
// ... other accounts
}
// Admin instruction to toggle
pub fn set_circuit_breaker(
ctx: Context<SetCircuitBreaker>,
global_pause: Option<bool>,
pause_new_offers: Option<bool>,
pause_new_trades: Option<bool>,
) -> Result<()> {
let config = &mut ctx.accounts.hub_config;
if let Some(v) = global_pause {
config.global_pause = v;
}
if let Some(v) = pause_new_offers {
config.pause_new_offers = v;
}
if let Some(v) = pause_new_trades {
config.pause_new_trades = v;
}
Ok(())
}
6.4 Platform-Specific Vulnerabilities
| Vulnerability | CosmWasm | Solidity | Solana |
|---|---|---|---|
| Reentrancy | Safe (message queue) | HIGH RISK - use CEI + guards | Safe (account borrowing) |
| Integer Overflow | Safe (Rust panics) | Safe in 0.8+ (checked math) | Safe (Rust panics) |
| Front-running | Possible (mempool) | HIGH RISK (MEV) | Lower risk (fast blocks) |
| Signature Replay | Use nonces | Use nonces + EIP-712 | Built-in (recent blockhash) |
| Missing Access Control | Check info.sender |
Check msg.sender |
Check Signer + constraints |
| Account Confusion | N/A | N/A | HIGH RISK - verify account owners |
| Uninitialized Storage | Rare (Option types) | Check initialization | Check is_initialized |
In Solana, attackers can pass arbitrary accounts to your program. Always verify that accounts are owned by the expected program and have the correct type. Anchor's Account<'info, T> type and owner constraint help prevent this.
Knowledge Check
Test Your Understanding
1. Why is CosmWasm naturally protected from reentrancy attacks?
2. What is the CEI pattern in Solidity?
3. What is an "account confusion" attack in Solana?