Writing Smart Contracts That Actually Work

The Current State of Smart Contract Programming Language

Solidity is what you will use unless you have a strong reason not to. It powers the majority of contracts deployed on EVM chains. Rust is the alternative for Solana. Vyper exists for Ethereum but sees far less adoption. The ecosystem is not evenly divided; most documentation, tooling, and security audits target Solidity first. That alone makes it the practical default for new projects. I write about this because I have seen too many teams pick a language based on aesthetics rather than reality. A smart contract is a program stored on a blockchain. It executes when someone calls it. The execution is deterministic and irreversible once confirmed. That last part matters more than people admit. A bug in traditional software means a hotfix. A bug in a contract usually means lost funds. The stakes change how you approach everything else.

What You Are Actually Writing

Solidity compiles to EVM bytecode. Your source code becomes opcodes that the Ethereum Virtual Machine runs. Between your code and those opcodes sits the compiler, which translates high-level constructs into storage slots, memory operations, and calldata handling. You do not manage memory manually the way you would in C. The EVM has a stack-based model, but the compiler abstracts most of that away. What you do manage is storage layout, and storage is the most expensive operation you will perform. Here is a minimal contract that actually does something useful. Not the greeting world example from documentation. A real escrow contract:

Basic Structure and Functionality

Example Code


pragma solidity ^0.8.20;

contract Escrow {
    address public buyer;
    address public seller;
    enum State { Pending, Released, Refunded }
    State public state;
    mapping(address => uint256) public deposits;

    modifier onlyBuyer() {
        require(msg.sender == buyer, "Not buyer");
        _;
    }

    modifier onlySeller() {
        require(msg.sender == seller, "Not seller");
        _;
    }

    constructor() payable {
        buyer = msg.sender;
        seller = 0xSellerAddress;
        state = State.Pending;
    }

    function release() external onlySeller {
        require(state == State.Pending, "Wrong state");
        state = State.Released;
        payable(seller).transfer(address(this).balance);
    }

    function refund() external onlyBuyer {
        require(state == State.Pending, "Wrong state");
        state = Refunded;
        payable(buyer).transfer(address(this).balance);
    }
}

This looks simple. The complexity hides in the details. The enum creates a storage variable. The mapping creates an indirect storage lookup. Each modifier adds a runtime check. The transfer call in release is the part that bites people. If seller is a contract without a fallback function, the transfer reverts and the entire contract freezes. You can fix that with call instead of transfer, but then you open a different class of problems. The reentrancy vulnerability that drained The DAO in 2016 still shows up in new contracts because the fundamental issue is subtle. Check-effects-interactions. Every function that moves funds should follow this order. First, validate the caller has permission. Second, update all internal state. Third, send the funds. I cannot stress this enough. Most reentrancy fixes boil down to ensuring the state update happens before any external call. If you send funds first and then update state, a malicious contract can call back into your function before the state changes and drain it repeatedly. Two years ago I deployed a staking contract on mainnet. The logic looked sound. I had reviewed the code three times. The issue was in a modifier chain. I used two modifiers on the same function: one checked ownership, the other checked a cooldown period. The order of modifiers in Solidity executes bottom-to-top. I assumed top-to-bottom. The cooldown check ran before the ownership check, which meant anyone could trigger a cooldown reset by calling the function with an invalid sender. The modifier itself did not revert. It just evaluated a condition that referenced uninitialized storage. That uninitialized storage happened to be zero, which passed the cooldown check. The ownership check never ran. I caught it during a routine audit two days after deployment. The workaround was simple: I added an explicit require at the top of the function body and restructured the modifiers to be self-contained. It cost me about six hours of incident response and a public post explaining what went wrong.

Get the Full Details

O que é o método SMART e como aplicá-lo - Débora Venâncio
O que é o método SMART e como aplicá-lo - Débora Venâncio

Storage writes cost 20,000 to 22,000 gas in current EIP-4844 pricing environments. Storage reads cost 2,100. Memory writes are nearly free. This is not theoretical. A function that writes ten storage variables will cost roughly 200,000 gas just in storage, before any logic runs. If your transaction needs to stay under 300,000 gas due to block limits or user expectations, you have about 100,000 gas for actual computation. Pack your data. Use tight storage layouts. Put multiple bool or uint8 variables into a single storage slot. The compiler does pack them automatically if they are separate state variables of compatible small types. It does not pack across different data structures. A mapping will always consume its own slot regardless of what else is in your contract. Integer overflow is not a real threat in Solidity 0.8.0 and above. The compiler reverts automatically. You do not need SafeMath. Claiming otherwise is either outdated information or deliberate confusion. Floating point numbers do not exist in Solidity. You work in fixed-point arithmetic using integers. If you need decimals, you multiply by a constant like 1e18 and divide at the end. Rounding errors accumulate. You will see discrepancies between what a frontend calculates and what the contract calculates. The contract is always right. The frontend is usually wrong. Events do not store data. They write to the transaction log. You cannot query events directly from the contract. You read them from the blockchain history using indexers or RPC calls. If you need to retrieve historical data later, build a proper indexing layer. Do not rely on events as a database.

When Solidity Is the Wrong Tool

Solidity has real limitations. It lacks native support for formal verification. Tools like Certora and Slither exist, but they add complexity and cost. If your contract handles life-critical systems or millions of dollars with zero tolerance for bugs, consider writing a formal specification first and then implementing in Solidity. Do not skip the specification step. Half the teams that skip it end up with contracts that are correct but inefficient, which is worse than being obviously wrong because you think you are safe. Upgradeable contracts introduce proxy patterns. The proxy stores the logic address and delegates calls. Your data stays in the proxy implementation. This means storage layout changes between versions can break everything. If you add a new state variable in v2 and it shifts the storage slots used by v1, existing data becomes corrupted. Use UUPS or transparent proxy patterns carefully. EIP-1967 storage slots are the standard. Do not write your own proxy scheme. You will get it wrong.

The Alternatives Worth Knowing

Rust on Solana is the strongest alternative for high-throughput applications. The program model is different. There is no EVM. Accounts are passed explicitly. This gives you more control but also more responsibility for memory safety. The compile-to-BPF process is slower than Solidity compilation. Debugging is harder. The tooling is improving but trails Solidity by a significant margin. Vyper is a Python-like language for Ethereum. It forbids some Solidity features that cause bugs: inheritance, inline assembly, and recursive calling. That is both its strength and its weakness. It prevents certain attack vectors by design. It also prevents legitimate use cases. If your contract requires complex inheritance or assembly optimization, Vyper will not work for you.

Saiba como e porque aplicar as metas SMART na organização
Saiba como e porque aplicar as metas SMART na organização

What I Actually Recommend

Start with Solidity 0.8.20 or later. Write tests using Hardhat or Foundry. Foundry is faster and uses Solidity for tests, which means your test code has the same type safety as your contract code. Hardhat has better TypeScript integration. Pick based on your team's stack. Run Slither as a linter. Use Mythril for symbolic execution on critical paths. Do not skip the testing. Unit tests should cover 90 percent of your branches. Integration tests should cover the happy path and the rejection paths. Fuzzing is where most real bugs live. Use Echidna or Foundry's fuzzing. It takes about an hour to set up and catches edge cases that manual review misses. The language is a tool. The contract is the product. The blockchain is the deployment environment, and it does not forgive. Write carefully. Test relentlessly. Deploy slowly. That is the practical path.