f in x
Solidity from Zero: Variables, Functions, Events, and Security Patterns — Write Smart Contracts That Don't Lose Money
> cd .. / HUB_EDITORIALE
Trend emergenti e tecnologie

Solidity from Zero: Variables, Functions, Events, and Security Patterns — Write Smart Contracts That Don't Lose Money

[2026-06-21] Author: Ing. Calogero Bono

Have you ever seen a smart contract go up in smoke due to a simple reentrancy? We have. A client had a DeFi pool with a reentrancy bug: in seconds, 40% of the liquidity pool was gone. It was written by an 'experienced' team, but without security patterns. That's why we decided to write this guide. We don't want you to lose money (and credibility).

This guide starts from zero: variables, functions, events. Then it dives into the security patterns every Solidity developer must know. It's not a theory lecture — it's what we at Meteora Web have applied in real projects, after years of accounting and ERP management: every line of code has a cost, every bug can burn capital. We're talking concrete stuff.

How Do You Declare Variables, Functions, and Events in Solidity?

State variables and basic types

In Solidity, state variables are stored on the blockchain. Each write costs gas. Here's a minimal example:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Base {
    // public state variable – automatically generates a getter
    uint256 public count;
    // private variable – only accessible inside the contract
    address private owner;
    // immutable variable – set once at deployment
    uint256 public immutable MAX_SUPPLY = 1000;
}

Watch out: use uint256 instead of uint for clarity. Avoid implicit var (deprecated). address and address payable are different: only the latter can receive ether.

Sponsored Protocol

Functions: visibility and modifiers

Functions in Solidity have four visibility levels: public, internal, external, private. Pay attention to public vs external: external costs less gas for external calls because parameters are not copied to memory.

contract Funzioni {
    uint256 public data;

    // external function – callable only from outside
    function setData(uint256 _newData) external {
        data = _newData;
    }

    // public function – callable internally as well
    function getData() public view returns (uint256) {
        return data;
    }

    // internal function – only derived contracts
    function _internalHelper() internal pure returns (string memory) {
        return "only children";
    }
}

Function modifiers: view (does not modify state), pure (neither reads nor writes state), payable (can receive ether). Always use them to document and optimize gas.

Events: tracking changes on chain

Events allow you to log actions efficiently (cheaper than state variables). Clients (frontends, DApps) listen to them to react.

contract Events {
    event Transfer(address indexed from, address indexed to, uint256 value);

    function transfer(address _to, uint256 _value) external {
        // transfer logic
        emit Transfer(msg.sender, _to, _value);
    }
}

indexed parameters (max 3) allow filtering events. Events are not accessible on-chain, but they are fundamental for auditing.

Sponsored Protocol

What Are the Essential Security Patterns for a Smart Contract?

After years of handling vulnerabilities (even in projects that came to us for repairs), we consider three patterns mandatory.

1. Checks-Effects-Interactions (CEI)

It's the number one pattern: first check conditions (checks), then modify state (effects), finally interact with external contracts (interactions). Prevents reentrancy.

contract CEIPattern {
    mapping(address => uint256) public balances;

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "no balance");
        // effects
        balances[msg.sender] = 0;
        // interactions
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "transfer failed");
    }
}

See? We set the balance to zero before sending ether. If the caller is a malicious contract and calls withdraw again in its receive, the balance is already zero. No reentry.

2. Guard Check with custom modifiers

Repeating require(owner == msg.sender) everywhere is tedious and risky. Create reusable modifiers.

contract GuardCheck {
    address public owner;

    modifier onlyOwner() {
        require(owner == msg.sender, "not owner");
        _;
    }

    modifier nonZeroAddress(address _addr) {
        require(_addr != address(0), "zero address");
        _;
    }

    function setOwner(address _newOwner) external onlyOwner nonZeroAddress(_newOwner) {
        owner = _newOwner;
    }
}

Rule of thumb: use _ to represent the function body. Modifiers chain in declaration order.

Sponsored Protocol

3. Pausability and emergency stop

When a bug is discovered, you must be able to stop the contract. The most common pattern is to inherit OpenZeppelin's Pausable.

import "@openzeppelin/contracts/security/Pausable.sol";

contract MyContract is Pausable {
    function doSomething() external whenNotPaused {
        // logic
    }

    function pause() external onlyOwner {
        _pause();
    }

    function unpause() external onlyOwner {
        _unpause();
    }
}

Use whenNotPaused for critical functions. The owner can pause the contract in an emergency. But beware: the owner becomes a centralization point. Consider a multisig for pausing.

How to Prevent Common Attacks Like Reentrancy or Overflow?

Reentrancy: beyond the CEI pattern

The classic reentrancy is solved by CEI, but variants like cross-function reentrancy exist. Use a reentrancy lock (mutex) for extra safety:

Sponsored Protocol

contract ReentrancyGuard {
    bool private locked;

    modifier noReentrant() {
        require(!locked, "reentrant call");
        locked = true;
        _;
        locked = false;
    }

    function withdraw() external noReentrant {
        // logic
    }
}

OpenZeppelin provides ReentrancyGuard already battle-tested. We always recommend it on contracts that handle funds.

Overflow and underflow

Before Solidity 0.8, overflows caused wraparound. Now the compiler checks by default and reverts. But beware: in unchecked blocks checks are disabled (useful for gas savings).

function safeMath() external pure {
    // in Solidity 0.8+ this reverts on overflow
    uint256 a = type(uint256).max;
    uint256 b = 1;
    // revert: a + b overflow
    // uint256 c = a + b;

    // unchecked allows overflow but be careful
    unchecked {
        uint256 c = a + b; // c = 0 (wraparound)
    }
}

Rule: use unchecked only where you are sure overflow cannot happen (e.g., increments that won't exceed a limit). Otherwise leave automatic checks.

Access control: beyond owner

The onlyOwner pattern is simple but often insufficient. For more granular roles use OpenZeppelin's AccessControl.

import "@openzeppelin/contracts/access/AccessControl.sol";

contract RoleBased is AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER");

    constructor() {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(MINTER_ROLE, msg.sender);
    }

    function mint() external onlyRole(MINTER_ROLE) {
        // logic
    }
}

This allows delegating management to multiple accounts, revoking roles, and having an on-chain audit trail of permissions. Much more robust than a single owner.

Sponsored Protocol

What to Do Now

Don't stop at reading. Here are three concrete actions:

  1. Write a test contract on Remix or Hardhat. Implement the three patterns (CEI, modifiers, pausability) and simulate a reentrancy attack. Our advice: use the official Hardhat tutorial for local environments.
  2. Integrate OpenZeppelin Contracts into your project. Don't reinvent the wheel: use the official libraries for ReentrancyGuard, AccessControl, Pausable, SafeERC20 (for tokens). Reduce bug risk to nearly zero.
  3. Run a preventive audit before deployment. We at Meteora Web have seen contracts with vulnerabilities that a simple static analyzer (Slither) would have found in 10 seconds. Run it on every contract.

Remember: every uninitialized variable, every unchecked call, every single owner without a multisig is a potential incident. Treat your code like money — because on the blockchain, it is.

To dive deeper, read our Pillar on Blockchain and Web3.

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere Informatico, co-fondatore di Meteora Web. Esperto in architetture software, sicurezza informatica e sviluppo sistemi scalabili.
[ Read Full Dossier ]

> METEORA_WEB // DIGITAL AGENCY

We build the digital presence your business deserves.

Websites, social media, online advertising, e-commerce and high-performance hosting, engineered with method by computer engineers in Sciacca, for all of Italy.

> MW_JOURNAL

> READ_ALL()