Contratos inteligentes não enxergam o mundo fora da blockchain. Para saber quanto vale um ETH, um
protocolo de empréstimos depende de um oráculo, e o mais usado do ecossistema é a rede de
Data Feeds da Chainlink. Isso cria dois pontos de confiança bem distintos: de um lado, os
operadores de nó, que rodam a infraestrutura que assina e publica os preços; do outro, os
protocolos consumidores, que escreveram código assumindo que esses preços estão corretos, frescos e
dentro de limites razoáveis.
Este artigo apresenta duas ferramentas open-source em Go que desenvolvi, cada uma voltada a um desses
lados. O oraclint é um linter estático de segurança
para a configuração de nós Chainlink, que roda antes do deploy. O
oraclewatch é um daemon de monitoramento que lê
os feeds diretamente da chain e alerta quando as premissas do consumidor deixam de valer. Nenhuma das
duas é afiliada à Chainlink Labs.
Conceitos
Data Feeds e o contrato agregador
Um Data Feed é um par de contratos: um proxy, endereço estável que o consumidor chama, e um
aggregator, que recebe os relatórios dos nós e guarda o valor consolidado. O consumidor lê o preço
com latestRoundData(), que devolve o valor (answer), o identificador da rodada e os
timestamps de início e atualização. Um feed é atualizado quando o preço desvia mais que um limiar
(deviation threshold) ou quando passa o heartbeat, o intervalo máximo entre atualizações.
Os agregadores também possuem limites, minAnswer e maxAnswer. Relatórios fora desse
intervalo são rejeitados, e não "achatados" no limite: o feed simplesmente congela no último valor
aceito. Esse detalhe, confirmado no código-fonte dos agregadores implantados, é o centro de um dos
incidentes descritos mais adiante.
Sequencer de L2
Em rollups como Arbitrum, Base e OP Mainnet, um único sequencer ordena as transações. Se ele
cai, os preços param de ser atualizados, e quando volta, as transações enfileiradas são executadas
primeiro, possivelmente contra preços velhos. Por isso a Chainlink publica sequencer uptime
feeds, e auditorias costumam exigir que o consumidor verifique o sequencer e respeite um período de
carência (grace period) após a recuperação.
O nó Chainlink
Do lado do operador, um nó é um processo que guarda chaves, assina transações e relatórios, e expõe
uma interface de operação (porta 6688) que gerencia jobs, chaves e bridges. Sua configuração fica em um
config.toml e um secrets.toml, normalmente acompanhados de um docker-compose, manifests
Kubernetes e arquivos .env. Boa parte dos comprometimentos de nós não envolve exploits, e sim
configuração: interface publicada na internet, banco sem TLS, senha de keystore curta, chave de RPC
commitada no repositório.
oraclint: linter de configuração para nós
Ferramentas como tfsec, checkov e kube-bench fazem para Terraform e Kubernetes
exatamente o que faltava para nós Chainlink: apontar configurações inseguras antes do deploy, com
localização exata e correção sugerida. O oraclint preenche essa lacuna. Ele lê os arquivos do nó e da
infraestrutura ao redor, roda totalmente offline e nunca toca em um nó em execução.
Regras verificadas contra o código do nó
São 35 regras distribuídas entre configuração do nó, secrets, permissões de arquivo,
docker-compose, Kubernetes e dotenv. Cada uma foi verificada contra o código-fonte e a documentação do
nó na versão v2.69.0: nome da chave, valor padrão e comportamento real. Uma regra cujo
comportamento não pôde ser confirmado é marcada como experimental ou simplesmente não existe. O README
inclusive lista os candidatos descartados e o motivo, como a complexidade da senha do banco (o próprio
nó já recusa senhas fracas na inicialização) e o pprof (sempre atrás de autenticação no router
do nó).
Ciente dos valores padrão
Uma chave ausente não é tratada como "não configurada": ela é avaliada com o valor padrão real do nó.
Quando há múltiplos arquivos de configuração, eles são mesclados com a mesma semântica de override do
nó. Isso evita tanto falsos positivos quanto falsos negativos, que são o principal motivo pelo qual
linters genéricos de TOML não servem aqui.
Arquitetura
O oraclint é um pipeline linear de pacotes pequenos:
cli → loader → model → engine ← rules (35)
│
▼
suppress → redact → filter/sort → report (text | JSON | SARIF)
O loader descobre e classifica os arquivos por nome e conteúdo, e o model mantém cada valor associado ao seu arquivo:linha:coluna. Cada regra é um tipo Go com dois métodos e uma função pura do alvo, em seu próprio arquivo. Antes da renderização, toda mensagem passa pelo redact: o oraclint nunca imprime um segredo, com duas camadas de redação garantidas por um teste de fuzzing em todos os formatos de saída. Entradas hostis também são tratadas: alias bombs em YAML, aninhamento profundo, arquivos binários e symlinks que escapam da raiz do scan (a varredura usa os.Root).
oraclint na prática
A instalação pode ser feita com Go, binário de release ou Docker:
go install github.com/brennocm/oraclint/cmd/oraclint@latest
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/src:ro" -w /src \
ghcr.io/brennocm/oraclint:latest scan .
O repositório inclui um nó propositalmente inseguro em examples/insecure-node. Um trecho da saída:
oraclint scan examples/insecure-node
examples/insecure-node/config.toml
2:1 HIGH NS-KS-001 InsecureFastScrypt = true makes every keystore use weak scrypt
parameters, so a leaked database can be brute-forced offline
fix: Remove InsecureFastScrypt (default false) and rotate keys created while it was enabled.
14:1 MEDIUM NS-WEB-003 WebServer.AllowOrigins contains '*', which allows any website to
make cross-origin requests to the node API
fix: Set WebServer.AllowOrigins to the exact URL(s) used to reach the operator UI.
54:1 HIGH NS-RPC-001 EVM chain "1" node "mainnet-infura": HTTPURL uses unencrypted
http:// to mainnet.example-rpc.com
fix: Use https:// and wss:// endpoints, or keep the RPC node on a private network.
examples/insecure-node/docker-compose.yml
6:5 HIGH NS-DKR-003 service "chainlink" runs privileged, which gives it full access to the host
fix: Remove privileged, network_mode: host, pid: host and the listed cap_add entries.
9:9 HIGH NS-DKR-001 service "chainlink" publishes operator port 6688 on all host interfaces
fix: Publish the port on loopback only ("127.0.0.1:6688:6688") and use an SSH tunnel or VPN.
11:7 HIGH NS-KS-002 CL_PASSWORD_KEYSTORE is shorter than 16 characters
A regra NS-DKR-001 merece destaque, pois conversa diretamente com o problema discutido no artigo
sobre a bridge Tor: o Docker publica portas inserindo suas próprias regras
de firewall, então uma porta publicada costuma ser alcançável mesmo com o ufw configurado para
bloqueá-la. A correção é publicar a interface de operação apenas em loopback e acessá-la por túnel SSH.
Supressões com justificativa obrigatória
Toda supressão exige um motivo, seja inline ou no .oraclint.yaml. Uma diretiva sem motivo não é
aplicada e vira um achado informativo. Os achados suprimidos continuam visíveis no JSON e no SARIF, de
modo que a justificativa fica registrada:
[WebServer.TLS]
# oraclint:ignore NS-WEB-001 reason="TLS is terminated at the load balancer"
HTTPSPort = 0
Ao escanear conteúdo não confiável, como pull requests de forks, a flag --ignore-suppressions
garante que apenas a sua política seja aplicada, já que o próprio PR poderia trazer um
.oraclint.yaml silenciando tudo.
Integração com CI
O repositório é também uma GitHub Action. Com saída em SARIF 2.1.0, os achados aparecem na aba de
code scanning do GitHub:
jobs:
scan:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v7
- uses: brennocm/oraclint@v0.1.0 # fixe por SHA em produção
with:
path: .
format: sarif
output: oraclint.sarif
fail-on: high
- uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: oraclint.sarif
Os códigos de saída seguem o padrão de linters: 0 sem achados acima do limiar, 1 com achados e 2 para erro de uso. Arquivos que não podem ser parseados geram um achado parse-error de severidade alta, para que um arquivo quebrado falhe o build em vez de passar silenciosamente. No deploy de exemplo (5 arquivos, 35 regras), um scan completo leva cerca de 4 ms.
oraclewatch: monitoramento do lado consumidor
A Chainlink opera e monitora seus Data Feeds. O risco de usar um feed, porém, é do protocolo que o consome. O oraclewatch roda continuamente as mesmas verificações que auditores pedem on-chain (freshness do updatedAt, answer > 0, uptime do sequencer e grace period), além de algumas que só fazem sentido off-chain, e aciona um humano quando algo sai do esperado. Três incidentes reais motivaram o catálogo:
- LUNA/USD, maio de 2022. O preço de mercado caiu abaixo do minAnswer do agregador. Como relatórios fora do intervalo são rejeitados, o feed congelou em cerca de US$ 0,107 enquanto a LUNA era negociada a US$ 0,01. Atacantes compraram a mercado, depositaram como colateral pelo preço do oráculo e tomaram empréstimos. Venus e Blizz Finance perderam cerca de US$ 11M e US$ 8,3M. Os checks FW-DEV-001 (desvio de ~970%), FW-STALE-001 e FW-CB-001 sinalizam essa condição.
- Moonwell wrsETH, novembro de 2025. Um feed de taxa de câmbio reportou 1 wrsETH = 1,65 milhão de ETH, e o ataque levou 30 segundos. O FW-JUMP-001 sinaliza o salto, e o caso mostra por que um monitor complementa, e não substitui, limites de sanidade on-chain.
- Queda do sequencer da Arbitrum, dezembro de 2023. Cerca de 78 minutos fora do ar, cobertos por FW-SEQ-001, FW-SEQ-002 e FW-CHAIN-001.
O catálogo de checks
São 16 checks com IDs estáveis, cada um documentado com a lógica exata, os parâmetros padrão e os falsos
positivos conhecidos. Agrupando por propósito:
- Saúde do feed: feed parado além do heartbeat (FW-STALE-001), dados de rodada inválidos (FW-INVALID-001), proximidade dos limites do circuit breaker (FW-CB-001) e saltos implausíveis entre rodadas (FW-JUMP-001, experimental).
- Mercado: desvio em relação à mediana de várias exchanges (FW-DEV-001), com um estado explícito de "referência não confiável" quando poucas exchanges respondem ou elas discordam (FW-REF-001), e divergência do mesmo par entre redes (FW-XCHAIN-001).
- Mudanças no contrato: proxy apontando para um novo agregador (FW-AGG-001) e divergência de decimals ou descrição (FW-META-001).
- Confiança na fonte: RPCs que retornam dados diferentes para o mesmo bloco (FW-RPC-001), visões de fork (FW-RPC-002), chain ID errado (FW-RPC-003) e head parado (FW-CHAIN-001).
- L2: sequencer fora do ar (FW-SEQ-001) e ainda dentro do grace period (FW-SEQ-002).
- O próprio monitor: FW-SELF-001 dispara quando não há leituras bem-sucedidas por N ciclos. Sem dados, o oraclewatch se declara cego em vez de adivinhar.
Um bloco fixo por ciclo
A decisão de design central é que todo valor comparado por um check vem do mesmo bloco. Há uma
goroutine por rede, e cada ciclo começa fixando um bloco: min(head) - confirmations entre todos os
endpoints disponíveis. Endpoints cujo head está atrasado são excluídos, o hash do bloco é comparado
entre eles (a maioria vence) e então cada feed é lido em todos os endpoints naquele bloco exato.
Isso é o que torna correta a regra do FW-RPC-001 de que qualquer diferença é crítica: duas RPCs
honestas, consultadas no mesmo bloco, precisam devolver bytes idênticos. Também mantém a medição de
staleness ancorada no relógio da própria chain, e não no relógio da máquina que roda o monitor.
Os checks são funções puras, sem I/O e sem relógio, com aritmética exata em math/big. Os
resultados alimentam uma máquina de estados de alertas (OK → PENDING → FIRING → RESOLVING) com
for:, histerese, cooldown e agrupamento de tempestades de alertas, enviados para Discord,
Telegram ou stdout, além de métricas Prometheus e um dashboard Grafana.
oraclewatch na prática
O modo check roda um único ciclo em todas as redes e sai com código 0 (ok), 1 (resultado igual ou acima de --fail-on) ou 2 (erro), servindo para CI ou cron. As URLs de RPC são referenciadas como ${VAR} e expandidas do ambiente apenas em campos de segredo:
cp .env.example .env # duas RPCs de provedores diferentes por rede, notificadores
make build && set -a && . ./.env && set +a
./bin/oraclewatch check --config configs/example.yaml
Uma execução real contra ETH/USD na Ethereum e na Arbitrum, usando RPCs públicas (saída resumida):
STATUS CHECK NETWORK FEED SUMMARY
ok FW-CB-001 ethereum ETH/USD bounds are nominal
ok FW-DEV-001 arbitrum ETH/USD within 0.0495% of the market
ok FW-DEV-001 ethereum ETH/USD within 0.2728% of the market
ok FW-REF-001 ethereum ETH/USD reference trusted
ok FW-RPC-001 arbitrum ETH/USD 2 endpoints agree
ok FW-RPC-002 ethereum endpoints agree on block 26250692
ok FW-RPC-003 arbitrum every endpoint serves the configured chain
ok FW-SEQ-001 arbitrum sequencer up
ok FW-SEQ-002 arbitrum up for 3720h50m50s
ok FW-STALE-001 arbitrum ETH/USD updated 3m10s ago
ok FW-STALE-001 ethereum ETH/USD updated 53m0s ago
ok FW-XCHAIN-001 * ETH/USD ETH/USD consistent across 2 networks
Repare no FW-STALE-001: o feed da Ethereum foi atualizado há 53 minutos e ainda está saudável,
porque o heartbeat de ETH/USD na mainnet é de uma hora. É por isso que cada feed carrega seus próprios
parâmetros, copiados dos arquivos oficiais, em vez de um limite global.
Para o daemon com Prometheus, Grafana e TLS, há um compose pronto, e a documentação cobre um deploy em
uma instância arm64 do Oracle Cloud Always Free, a mesma base usada no
OCI-SecOps:
docker compose -f deploy/docker-compose.yml --env-file .env up -d
A configuração de exemplo monitora ETH/USD, BTC/USD e USDC/USD na Ethereum, Arbitrum, Base e OP Mainnet, mais os feeds de uptime de sequencer das três L2s. Nenhum endereço foi digitado à mão: todos vieram dos arquivos de dados da página oficial e foram verificados on-chain contra duas RPCs no mesmo bloco, e um teste garante que o exemplo continue idêntico às listas verificadas.
Segurança das próprias ferramentas
Ferramentas de segurança que rodam ao lado de infraestrutura sensível precisam ser elas mesmas um alvo difícil. Alguns princípios são comuns às duas:
- Superfície mínima. O oraclint não acessa a rede e tem três dependências diretas de runtime (cobra, go-toml, yaml). O oraclewatch usa apenas eth_chainId, eth_getBlockByNumber e eth_call; não existe chave privada no programa, e um teste falha se o binário linkar qualquer pacote de gerenciamento de chaves ou assinatura de transações.
- Segredos nunca saem do processo. Redação em strings, erros (incluindo *url.Error, que costuma vazar a URL completa da RPC com a API key) e logs, verificada por testes ponta a ponta.
- Supply chain. Actions fixadas por SHA, Dependabot, releases pelo GoReleaser com checksums assinados via cosign (keyless, Sigstore), SBOM SPDX e proveniência SLSA verificável com gh attestation verify.
- Documentação de ameaças. Cada repositório tem um THREAT_MODEL.md e decisões de design registradas em ADRs.
Limitações
Ambas são deliberadamente honestas sobre o que não fazem. O oraclint é análise estática: ele vê
arquivos, não o nó em execução, e as regras valem para a v2.69.0, podendo errar em outras versões. Helm
charts precisam ser renderizados com helm template antes.
O oraclewatch tem latência de detecção igual ao intervalo de polling (60 s por padrão) mais as
confirmações. Um atacante observando o mesmo feed age dentro de um bloco; o monitor avisa humanos, não
impede transações. Ele não substitui a validação on-chain no contrato consumidor, que é a primeira
linha de defesa, e não cobre preços derivados que o protocolo compõe a partir de vários feeds.
Considerações finais
Oráculos concentram confiança de duas formas: na infraestrutura que produz os dados e na suposição de
quem os consome de que esses dados continuam válidos. O oraclint atua antes do deploy, transformando
boas práticas de operação de nó em regras verificáveis que rodam no CI. O oraclewatch atua depois,
verificando continuamente se as premissas do consumidor ainda se sustentam.
A lição comum aos dois projetos é que uma ferramenta de segurança vale tanto quanto a precisão das suas
premissas. Por isso cada regra foi verificada contra o código do nó, cada check tem um incidente real ou
uma pesquisa documentada por trás, e ambos preferem dizer "não sei" (parse-error,
FW-SELF-001) a passar silenciosamente.