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.

  1. 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.
  2. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *