Visibilidade de funções é uma das fontes mais comuns de vulnerabilidades em contratos Solidity. Vamos explicar o que é, por que importa, exemplos práticos de exploração e como consertar. Vamos incluir código vulnerável e a versão corrigida, checklist para auditoria e ferramentas/testes recomendados.
- Conceito Rápido:
- EM Solidity, visibilidade define quem pode chamar uma função ou acessar uma variável. As principais visibilidades são:
- public: qualquer endereço (externo) e outros contratos podem chamar; também gera uatomaticamente um getter para variáveis públicas.
- external: só pode ser chamada de fora do contrato (por transação ou chamada externa); mais barata que public em alguns casos.
- internal: só pode ser chamada dentro do contrato e por contratos que herdam (como protected).
- private: só pode ser chamada dentro do contrato onde foi definida (não é visível para contratos filhos).
- Além disso, view/pure indicam que não alteram estado, mas não são visibilidade.
- Probla comum: deixar função que deveria ser private/internal como public/external – qualquer um pode chamá-la e manipular o estado do contrato.
- EM Solidity, visibilidade define quem pode chamar uma função ou acessar uma variável. As principais visibilidades são:
- Exemplos práticos de vulnerabilidades e explorações:
- Exemplo 1 – inicializados público (reentrância de propriedade)
// Vulnerável: initialize é public e pode ser re-executado
pragma solidity ^0.8.19;
contract Vault {
address public owner;
uint256 public balance;
function initialize(address _owner) public {
owner = _owner;
}
function deposit() public payable {
balance += msg.value;
}
function withdraw(uint256 amount) public {
require(msg.sender == owner, "not owner");
payable(owner).transfer(amount);
balance -= amount;
}
}
Exploração: Se initialize for esquecida e owner está 0x0, um atacante chama initialize(attacker) e se torna dono – pode sacar tudo.
Correção: tornar constructor/initializer internal ou proteger com um initialized flag / onlyOwner. Para proxys, usar initializer do OpenZeppelin.
Exemplo 2- função administrativa pública
// Vulnerável: setFee é public, qualquer um muda taxa para 100%
pragma solidity ^0.8.19;
contract Marketplace {
address public admin;
uint256 public feePercent;
constructor() {
admin = msg.sender;
}
function setFee(uint256 _fee) public {
feePercent = _fee;
}
}
Exploração: qualquer pessoa chama setFee(1000) e quebra economia do contrao.
Correção: adicionar onlyAdmin modifier e private / internal quando apropriado.
Exemplo 3 – função private pretendida mas public por engano
‘As vezes o desenvolvedor cria uma função auxiliar e esquece de marcar visibilitiy:
function _transferInternal(address from, address to, uint256 amount) public {
// deveria ser internal/private
// ...
}
Exploração: chamam essa função diretamente com parâmetros forjados (bypass de checks).
Exemplo 4 – getters acidentais para variáveis sensíveis
Variável marcada public cria um getter público automaticamente. Não faça isso para dados sensíveis (por exemplo, mapping de limites secretos).
3) Código vulnerável completo e ver~so corrigida
Vulnerável – contrato simples com várias falhas de visibilidade:
pragma solidity ^0.8.19;
contract BadToken {
mapping(address => uint256) public balances;
address public owner;
constructor() {
owner = msg.sender;
}
// deveria ser internal
function mint(address to, uint256 amount) public {
balances[to] += amount;
}
// deveria ser onlyOwner
function setOwner(address _owner) public {
owner = _owner;
}
// erroneamente public (bypass possível)
function _deduct(address from, uint256 amount) public {
balances[from] -= amount;
}
}
Corrigido – visibilidades + controle de acesso
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/access/Ownable.sol";
contract GoodToken is Ownable {
mapping(address => uint256) private balances;
// mint só o dono pode chamar
function mint(address to, uint256 amount) external onlyOwner {
balances[to] += amount;
}
// getter explícito, se quiser expor saldo
function balanceOf(address account) external view returns (uint256) {
return balances[account];
}
// internal helper, não exposto
function _deduct(address from, uint256 amount) internal {
require(balances[from] >= amount, "insufficient");
balances[from] -= amount;
}
}
Principais mudanças
- mint -> external onlyOwner
- balances -> private + balanceOf para controle
- helpers marcados internal
- uso de Ownable para acesso administrativo
4) Boas práticas e recomnedações (quick checklist)
- Sempre declarar visibilidade (public | external | internal | private). Não deixe em branco (o compilador pode inferir, mas seja explícito).
- Funções administrativas: onlyOwner / onlyRole (use OpenZeppelin Ownable / AccessControl).
- Helpers (utilitárias que não devem ser chamadas externamente): marque internal ou private.
- Construtores ; inicializadores:
- Para contratos normais, use constructor() (não público).
- Para proxies, use padrõesinitializer (OpenZeppelin) e implemente flag initialized para prevenir re-inicialização.
- Evite variáveis public para dados sensíveis – public gera getters automáticos.
- Use external para funções que só serão chamadas externamente (mais gas-econômico para calldata).
- Use view/pure quando aplicável – melhora clareza.
- Revisão de visibilidade em cada função durante auditoria – faça checklist linha-a-linha.
- Teste de fuzz / unit tests para chamadas externas inesperadas.
- Minimize superfície de ataque – funções que mudam estado e não precisam ser públicas devem ser privadas/internal.
5) Ferramentas * testes recomendados
- Statis analysis: Slither, Solhint, MythX, Security. *Pode testar nossa ferramenta que esta em desenvolvimento, falar com consultor agora!
- Fuzzing/property testing: Echidna, Fooundry / forge test.
- Unit tests: Hardhat / Foundry / Truffle – escrever testes que tentem chamar funções privadas via ABI (simular chamadas externas0.
- Review manual: checar explicitamente cada função: “quem precisa chamar isso?” -> escolher visibility.
- Upgrade / Proxy patterns: Verificar initialize e proteções para evitar re-initialization.
6) Exemplos de verificação rápida manual (mini-checklist para cada função)
Para cada função no contrato:
- Quem deve chamar? (usuário, dono, outro contrato, ó internamente).
- Precisa ser pública/external? Se não -> internal/private.
- Muda estado? Se sim, considerar checks onlyOwner / reentrância.
- É helper? Se sim, marque internal.
- Tem fallback logic? Verifique receive() / fallback() visibilidade e comportamento.
7) Casos reais de ataques relacionados a visibilidade (reumo rápido)
- Re-initialização de contratos de proxy: contrato não protegido permitiu que atacante chamasse initialize.
- SetOwner público: atacante muda administrador e rouba fundos.
- Funçoes auxiliares públicas: permitem manipular balances internamente e quebrar invariantes.
8) Resumo enxuto (prático)
- Erro mais comum: esquecer de marcar visibilidade ou marcar public quando não deveria.
- Regra de ouro: funções não necessárias externamente -> private/internal. Funções administrativas -> external + onlyOwner / ACL.
- Proteja inicializadores (especialmente em proxys).
- Use ferramentas e crie testes que tentem chamar APIs inesperadas.
