Fundamentos de Solidity: contract, state e function
1. Visão geral
Solidity é a linguagem principal para escrever smart contracts na Ethereum e em cadeias compatíveis com EVM. Este artigo te ajuda a dominar os três blocos de construção mais fundamentais: contract, state variable e function — através de um contrato Wallet simples. Após ler, você será capaz de escrever contracts que armazenam estado, controlar acesso com modifier e escrever seu primeiro teste com Hardhat.
2. O que é um contract
Em Solidity, contract é similar a uma class em Java — ele define state (dados) e behavior (funções) que serão deployados na blockchain em um endereço único.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Wallet {
// state variables e funções vão ficar aqui
}
- A linha
// SPDX-License-Identifierdeclara a licença; o compilador avisará se faltar. pragma solidity ^0.8.24fixa a versão do compilador a partir de 0.8.24 (mas menor que 0.9.0).- Cada contract quando deployado terá seu próprio endereço (address), como um “object” que vive para sempre na chain.
Nota: Nunca use pragma muito genérico como
>=0.8.0 <0.9.0em produção — novos compiladores podem mudar comportamentos padrão (ex: verificação de overflow).
3. State variable — dados armazenados on-chain
State variable é uma variável gravada diretamente no storage da blockchain, persiste entre chamadas de função e consome gas para ser escrita.
contract Wallet {
address public owner;
uint256 public balance;
mapping(address => uint256) public allowance;
constructor() {
owner = msg.sender;
}
}
address public owner: armazena o endereço do proprietário do contrato. A palavra-chavepublicgera automaticamente um getterowner()sem custo.uint256 public balance: saldo, tipo inteiro sem sinal de 256 bits — o tipo de número mais comum em Solidity.mapping(address => uint256): similar aHashMap<Address, BigInteger>mas sem iteração de chaves, apenas busca por chave.constructor()executa exatamente uma vez no deploy;msg.senderé o endereço que chama a transação de deploy.
3.1. Diferenças entre storage, memory e calldata
function updateAllowance(address spender, uint256 amount) external {
// amount está em calldata (apenas leitura, mais barato em gas)
allowance[spender] = amount; // escreve em storage
}
storage: dados permanentes na chain (state variable).memory: dados temporários durante execução da função, apagados após sua conclusão.calldata: similar amemorymas apenas leitura, usado para parâmetros de funçõesexternal— economiza mais gas quememory.
Nota: Esquecer de declarar
memory/calldatapara parâmetros do tipoarray,struct,stringcausará erro do compilador — diferente de Java, onde você não precisa se preocupar com a memória.
4. Functions e visibility
Funções em Solidity obrigam a declarar a visibilidade explicitamente, não há padrão “package-private” como em Java.
contract Wallet {
address public owner;
uint256 public balance;
constructor() {
owner = msg.sender;
}
function deposit() external payable {
balance += msg.value;
}
function withdraw(uint256 amount) external {
require(msg.sender == owner, "Wallet: not owner");
require(amount <= balance, "Wallet: insufficient balance");
balance -= amount;
(bool success, ) = owner.call{value: amount}("");
require(success, "Wallet: transfer failed");
}
function getBalance() external view returns (uint256) {
return balance;
}
}
external: pode ser chamada apenas de fora do contract (ex: carteira MetaMask, outro contract) — mais barato em gas quepublicquando parâmetros são arrays/strings grandes.payable: permite que a função receba ETH junto com a transação;msg.valueé a quantidade de wei enviada.view: promete não alterar o estado, não consome gas quando chamada off-chain (viaeth_call).require(condition, "message"): verifica a condição, desfaz toda a transação com mensagem se falhar — similar a lançarIllegalArgumentException, mas reverte tanto o estado quanto o gas gasto (exceto o gas da verificação em si).owner.call{value: amount}(""): a forma recomendada de enviar ETH em vez de usartransfer()/send(), pois não tem limite fixo de 2300 gas.
Os quatro níveis de visibility:
| Visibility | Chamada de dentro do contract | Chamada de contract herdeiro | Chamada de fora |
|---|---|---|---|
private |
✅ | ❌ | ❌ |
internal |
✅ | ✅ | ❌ |
public |
✅ | ✅ | ✅ |
external |
❌ (deve usar this.foo()) |
❌ | ✅ |
5. Modifier — reutilizar verificações
Repetir require(msg.sender == owner, ...) em várias funções é um sinal de que deve ser extraído para um modifier.
contract Wallet {
address public owner;
uint256 public balance;
modifier onlyOwner() {
require(msg.sender == owner, "Wallet: not owner");
_;
}
constructor() {
owner = msg.sender;
}
function withdraw(uint256 amount) external onlyOwner {
require(amount <= balance, "Wallet: insufficient balance");
balance -= amount;
payable(owner).transfer(amount);
}
}
modifier onlyOwner()define um bloco de verificação;_;é o ponto onde o corpo da função original é inserido.- Usar
onlyOwnerapós a declaração dewithdrawfaz o código ler como “apenas o proprietário da carteira pode sacar” — mais claro que repetirrequireem cada função. - Modifiers podem receber parâmetros, ex:
modifier minAmount(uint256 amount) { require(amount > 0, "..."); _; }.
6. Event — registros para off-chain
Contracts não conseguem retornar dados históricos diretamente para o frontend; event é usado para registrar logs que dApps podem ouvir.
contract Wallet {
event Deposited(address indexed from, uint256 amount);
event Withdrawn(address indexed to, uint256 amount);
uint256 public balance;
function deposit() external payable {
balance += msg.value;
emit Deposited(msg.sender, msg.value);
}
}
event Deposited(...)declara a estrutura do log, com máximo de 3 parâmetros comindexedpara facilitar filtragem.emit Deposited(...)escreve o log no recibo da transação, com custo de gas muito menor que armazenar em storage.- Frontend usa bibliotecas como ethers.js para ouvir
contract.on("Deposited", ...)e atualizar a UI em tempo real.
7. Erros comuns para iniciantes em Solidity
- Confundir
transfer/send/callao enviar ETH:transferlimita a 2300 gas, falha facilmente com contratos receptores que têm lógica complexa (proxy, multisig). Usecalljunto comrequirepara verificar o resultado como no exemplo da seção 4. - Não usar
requirepara validar entrada: falta de validação deixa o contract vulnerável a exploração (ex: sacar valores negativos, overflow — embora Solidity 0.8+ reverta automaticamente em overflow, mas a lógica de negócio ainda precisa de validação própria). - Usar
externalvspublicno contexto errado: usarpublicpara funções chamadas apenas de fora consome mais gas quando parâmetros são arrays/strings, porque Solidity precisa copiar dados paramemoryem vez de ler diretamente decalldata. - Não declarar
view/purequando possível: faz ferramentas como block explorer, carteiras não reconhecerem funções de leitura, usuários pensam erroneamente que precisam pagar gas.
Quando NÃO deixar função como
public: se a função nunca é chamada de outro contract ou por herança, useexternalpara economizar gas e limitar a superfície de ataque (attack surface).
8. Escrever testes com Hardhat
Adicione a dependência ao seu projeto Hardhat (Node.js):
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
Teste o comportamento de withdraw que permite apenas o owner:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Wallet", function () {
it("apenas o owner pode sacar dinheiro", async function () {
const [owner, stranger] = await ethers.getSigners();
const Wallet = await ethers.getContractFactory("Wallet");
const wallet = await Wallet.deploy();
await wallet.connect(owner).deposit({ value: ethers.parseEther("1") });
await expect(
wallet.connect(stranger).withdraw(ethers.parseEther("1"))
).to.be.revertedWith("Wallet: not owner");
await expect(
wallet.connect(owner).withdraw(ethers.parseEther("1"))
).to.changeEtherBalance(owner, ethers.parseEther("1"));
});
});
ethers.getContractFactory("Wallet")compila e carrega o artefato do contract.wallet.connect(stranger)simula uma transação enviada de um endereço diferente do owner.revertedWith("...")afirma que a transação foi revertida com a mensagem correta dorequire.changeEtherBalanceverifica se o saldo de ETH do owner aumentou exatamente 1 ETH após o saque — forma comum de testar em vez de apenas verificar o valor retornado.
Execute os testes:
npx hardhat test
Wallet
✔ apenas o owner pode sacar dinheiro (120ms)
1 passing (120ms)
9. Conclusão
Você acaba de passar por quatro conceitos fundamentais do Solidity: contract como unidade de deployment, state variable para armazenar dados on-chain com três regiões de memória (storage/memory/calldata), funções com visibility e modifier para controlar acesso, e events para comunicação com o off-chain. Esses conceitos formam a base para tópicos mais avançados como herança, interfaces ou padrões ERC-20/ERC-721 — todos construídos sobre esses mesmos quatro blocos.