Dados on-chain e SQL · Aula 5 de 9
O modelo de contas e a reconstrução de saldos
15 min de leitura
O Ethereum e as muitas blockchains que copiam seu projeto mantêm um saldo corrente para cada endereço, em vez de um conjunto de moedas discretas. Isso muda o que pode ser rastreado, o que uma "transação" significa e quem é o verdadeiro agente: com contas inteligentes, o endereço que assina, o endereço que paga o gas e o endereço no campo from frequentemente são três partes diferentes, e confundi-los atribui a atividade à parte errada. Isso também molda uma pergunta comum no trabalho pericial: "quanto este endereço detinha naquela data e como chegou a esse saldo?", à qual um perito cuidadoso responde de duas maneiras: calculando o saldo a partir do histórico e consultando-o no estado da blockchain, para então explicar qualquer diferença.
Estado, não moedas
Uma blockchain com modelo de contas armazena um estado global: um mapeamento de cada endereço de 20 bytes para um registro de conta. No Ethereum, esse registro tem quatro campos:
| Campo | Significado | Uso pelo analista |
|---|---|---|
nonce | Transações enviadas (EOA) ou contratos criados (contrato) | Contagem de atividades, ordenação, verificações de endereços novos |
balance | Moeda nativa, em wei | Saldo em um bloco |
codeHash | Hash do código da conta (vazio para uma EOA simples) | Diferenciação entre EOAs, contratos e EOAs delegadas |
storageRoot | Raiz do armazenamento de chave-valor da conta | Onde ficam os saldos de tokens e os dados do contrato |
Cada bloco vincula criptograficamente o estado resultante por meio do stateRoot em seu cabeçalho. Os saldos são lidos com eth_getBalance(address, block), e o armazenamento dos contratos, por meio de eth_call (por exemplo, o balanceOf de um token) ou eth_getStorageAt.
| Questão | Blockchains UTXO | Blockchains de contas |
|---|---|---|
| Unidade de valor | Saídas discretas, gastas integralmente | Um saldo por endereço |
| Troco | Uma nova saída, frequentemente para um novo endereço | Nenhum: o restante permanece no saldo |
| Vinculação de um pagamento de saída a um pagamento de entrada | Exata por saída; ambígua dentro de uma transação com múltiplas entradas | Nunca exata: todos os depósitos se misturam |
| Reutilização de endereços | Desaconselhada | Normal; um endereço por anos |
| Proteção contra repetição | Cada saída pode ser gasta uma vez | Cada transação deve usar o próximo nonce |
Quando 10 ETH chegam a um endereço que já detém 50 ETH, nada nos dados indica quais ETH saíram em seguida. Qualquer atribuição de um pagamento de saída a um depósito específico é uma convenção de rastreamento escolhida pelo analista, não um fato registrado (a aula 3.3 aborda os métodos).
EOAs, contratos e EOAs delegadas
Uma conta de propriedade externa (EOA) é controlada por uma chave privada e é o único tipo de conta que pode originar uma transação. Uma conta de contrato tem código e armazenamento e atua apenas quando é chamada. eth_getCode(address, block) retorna 0x para uma EOA e o bytecode implantado para um contrato. Três armadilhas tornam essa verificação menos simples do que parece:
- Momento. O código existe apenas a partir do bloco em que o contrato foi criado. Os endereços podem ser calculados antes da implantação (CREATE2 faz isso deliberadamente), portanto um endereço sem código no bloco N pode ser um contrato em N+1. Verifique o código no bloco relevante.
- Destruição. Antes da atualização Dencun (março de 2024),
SELFDESTRUCTexcluía o código de um contrato. Desde a EIP-6780, isso só ocorre quando é chamado na mesma transação que criou o contrato; caso contrário, ele apenas envia o saldo para outro endereço. - Delegação. Desde a atualização Pectra (maio de 2025), uma EOA pode conter o designador de delegação de 23 bytes da EIP-7702,
0xef0100, seguido de um endereço de delegado. Uma regra ingênua de "tem código, então é um contrato" a classifica incorretamente: a conta continua sendo controlada por sua chave privada.
Portanto, um classificador robusto retorna três classes (EOA, contrato, EOA delegada) referentes a um determinado bloco.
O nonce é um presente para os analistas. Para uma EOA, ele é igual ao número de transações enviadas, portanto nonce 0 significa que o endereço nunca enviou nada, e lacunas ou picos revelam padrões operacionais (aula 4.3). Duas ressalvas: cada autorização EIP-7702 que uma EOA assina também incrementa seu nonce, e o nonce de um contrato conta as criações de contratos, começando em 1 conforme a EIP-161. Os endereços dos contratos derivam de quem os implanta: CREATE usa o endereço e o nonce do implantador; CREATE2 usa o implantador, um salt e o hash do código de criação, razão pela qual os vínculos com o implantador são evidências fortes.
Onde fica o histórico
Uma consulta ao estado responde "quanto este endereço detinha no bloco N?". O histórico responde "como chegou a esse saldo?" e está distribuído por vários conjuntos de dados:
- transações de nível superior: valor enviado, mais o gas pago pelo remetente;
- rastros de chamadas internas: valor movimentado por contratos;
- logs: transferências de tokens e outros eventos;
- alterações de saldo que não são transações: taxas de prioridade creditadas ao destinatário das taxas do bloco, saques da camada de consenso creditados diretamente a endereços desde abril de 2023 (EIP-4895: sem gas, sem execução de código, sem hash de transação), alocações de gênese e, antes de setembro de 2022, recompensas de mineração.
Nós completos mantêm apenas o estado recente, portanto o estado em blocos antigos exige um nó de arquivo ou um provedor que ofereça esse recurso (aula 2.1).
Transações internas
No protocolo, uma transação é apenas a mensagem assinada de nível superior. Quando um contrato encaminha ETH durante a execução, isso é uma chamada de mensagem: sem hash, sem recibo e sem log, a menos que o código emita um. Os exploradores chamam essas chamadas de "transações internas", um rótulo conveniente, mas enganoso. Suponha que a transação de um usuário envie 0 ETH para um contrato de carteira 0x7c3E…12, que durante a execução chama 0xD4e5…3a com o valor 0xde0b6b3a7640000, que corresponde a 10^18 wei ou 1 ETH. A lista de transações de 0xD4e5…3a não mostra nada, mas seu saldo aumentou em 1 ETH. Apenas um rastro (aula 2.4) explica a alteração. Subchamadas revertidas não movimentam nada e devem ser excluídas.
Uma transferência ERC-20 é algo diferente: uma alteração no armazenamento do contrato do token anunciada por um evento Transfer, não uma transação interna (aula 3.5).
Contas inteligentes: quem assina, quem paga, quem atua
Uma conta inteligente é uma carteira implementada como contrato, com regras como múltiplos signatários, limites de gastos, recuperação ou gas patrocinado.
Carteiras de contrato (carteiras multiassinatura são o caso clássico) mantêm fundos em um endereço de contrato. Os proprietários assinam fora da blockchain e um deles, ou um retransmissor, submete a transação, portanto a EOA em from pode ser apenas o último signatário.
ERC-4337 adiciona abstração de contas sem alterações no protocolo. Os usuários assinam uma UserOperation (os campos incluem sender, nonce, callData, limites de gas, paymaster e signature) e a enviam para um mempool separado. Um bundler agrupa várias delas em uma única transação que chama handleOps em um contrato EntryPoint único, que valida e executa cada operação e paga ao bundler a partir do depósito da conta ou de um paymaster. Para cada operação, ele emite:
UserOperationEvent(bytes32 indexed userOpHash, address indexed sender, address indexed paymaster, uint256 nonce, bool success, uint256 actualGasCost, uint256 actualGasUsed)
O from da transação é o bundler, uma transação pode conter operações de usuários sem relação entre si, o recibo abrange todo o lote e o verdadeiro agente é o sender do evento. Usando o esquema genérico da aula 2.7:
-- Uma linha por UserOperation ERC-4337, com bundler, conta inteligente e paymaster
SELECT l.block_number,
l.transaction_hash,
t.from_address AS bundler,
'0x' || SUBSTR(l.topic2, 27, 40) AS smart_account, -- sender indexado
'0x' || SUBSTR(l.topic3, 27, 40) AS paymaster -- endereço zero = nenhum
FROM logs AS l
JOIN transactions AS t ON t.hash = l.transaction_hash
WHERE l.topic0 = '0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f'
AND l.contract_address IN (SELECT address FROM entrypoint_contracts);
topic0 é o hash Keccak-256 da assinatura do evento, e SUBSTR(topic, 27, 40) extrai um endereço indexado de seu tópico de 32 bytes preenchido com zeros (aula 2.9); alguns mecanismos chamam a função de SUBSTRING, e conjuntos de dados que armazenam tópicos em formato binário precisam de extração por bytes. Restringir a consulta a endereços EntryPoint conhecidos (um por versão, mantidos em uma pequena tabela do caso) evita correspondências com eventos semelhantes de outros contratos.
EIP-7702 permite que uma EOA existente adote o comportamento de uma conta inteligente. Uma transação do tipo 4 carrega autorizações [chain_id, address, nonce, y_parity, r, s]; cada uma define o código do signatário como o designador de delegação, que persiste até ser substituído ou removido, e uma autorização com chain_id 0 é válida em todas as blockchains. O endereço mantém seu histórico e sua chave, mas agora pode agrupar chamadas ou ter seu gas pago por terceiros. Os destinos da delegação também são evidências: um painel público de dados on-chain publicado pela empresa de negociação Wintermute em junho de 2025 constatou que mais de 80% das delegações iniciais apontavam para cópias de um único contrato "sweeper" que esvazia carteiras cujas chaves vazaram.
| Padrão | Quem autoriza | from da transação (paga o gas) | Onde encontrar o verdadeiro agente |
|---|---|---|---|
| Transação de EOA simples | Chave da EOA | A mesma EOA | from |
| Carteira de contrato | Assinaturas dos proprietários nos dados da chamada | Um proprietário ou um retransmissor | to (a carteira) e seus eventos |
| Metatransação (ERC-2771) | Assinatura do usuário | Um retransmissor por meio de um encaminhador confiável | Dados da chamada decodificados |
| ERC-4337 | Regras de validação da conta inteligente | Bundler, reembolsado pela conta ou pelo paymaster | UserOperationEvent.sender |
| EIP-7702 patrocinada | Autorização da EOA e carga útil assinada | Um patrocinador | A EOA delegada chamada |
Nunca equipare quem paga o gas ao agente antes de identificar o padrão. O financiamento do gas continua sendo uma evidência útil, mas, sob patrocínio, pode apontar para um serviço em vez de um proprietário.
Dois caminhos para obter um saldo
| Caminho | Como | Pontos fortes | Pontos fracos |
|---|---|---|---|
| Calcular a partir do histórico | Somar todos os créditos e débitos até um bloco | Explica cada movimentação; funciona em um data warehouse | É fácil deixar de fora uma categoria de movimentação |
| Ler a partir do estado | Consultar um nó para obter o saldo em um bloco | Fonte definitiva para aquela blockchain e aquele bloco | Sem explicação; blocos antigos exigem estado de arquivo |
O valor calculado entra no laudo porque conta a história; o valor do estado é a verificação. Quando coincidem, o histórico está completo. Quando diferem, algo está faltando, e o tamanho da diferença geralmente indica o quê.
Saldos UTXO
Em uma blockchain UTXO, o saldo de um conjunto de scripts na altura H é a soma das saídas destinadas a eles criadas em H ou antes e não gastas em H ou antes (aula 2.3). Para um endereço:
| Outpoint | Criado em | Quantia (BTC) | Gasto em |
|---|---|---|---|
4f3a…c21e:1 | 880.100 | 0,4000 | 880.900 |
a91c…3b07:0 | 880.450 | 1,2500 | não gasto |
77d2…e4a0:3 | 881.020 | 0,0800 | 885.300 |
c0b5…91f2:0 | 884.700 | 0,3000 | não gasto |
Em 882.000, as saídas não gastas são a91c…3b07:0 e 77d2…e4a0:3: 1,33 BTC. Em 886.000, são a91c…3b07:0 e c0b5…91f2:0: 1,55 BTC. O recebido (2,03) menos o enviado (0,48) resulta nos mesmos 1,55, uma verificação interna útil. Uma carteira não é um endereço: se os descritores de saída ou uma chave pública estendida forem conhecidos (por exemplo, fornecidos pelo proprietário em uma disputa), derive todos os endereços, incluindo os de troco, antes de somar. Transações no mempool não são saldos; saídas coinbase com menos de 100 blocos de idade existem, mas ainda não podem ser gastas. O scantxoutset do Bitcoin Core vê apenas o conjunto UTXO atual, portanto verificações históricas exigem um indexador ou um segundo conjunto de dados construído de forma independente.
Saldos nativos em blockchains de contas
saldo(N) = valor recebido em transações de nível superior bem-sucedidas
+ valor recebido em chamadas internas bem-sucedidas (rastros)
+ saques da camada de consenso e recompensas do destinatário das taxas
- valor enviado em transações de nível superior e chamadas internas bem-sucedidas
- taxas de gas pagas em toda transação enviada, bem-sucedida ou falha
Alocações de gênese e recompensas de mineração anteriores ao Merge adicionam termos para endereços mais antigos. Um exemplo resolvido para 0x6F2a…c19E no bloco N:
| Item | Fonte | Quantia (ETH) |
|---|---|---|
| Depósito de uma exchange | Transação | +5,000000 |
| Pagamento de um contrato | Rastro (chamada interna) | +1,000000 |
| Saques de validador | Lista de saques do bloco | +0,032000 |
| Pagamento enviado | Transação | −2,500000 |
| Gas desse pagamento: 21.000 × 20 gwei | Recibo | −0,000420 |
| Transação falha: valor não enviado | status do recibo = 0 | 0 |
| Gas da transação falha: 70.000 × 30 gwei | Recibo | −0,002100 |
| Saldo calculado | 3,529480 |
Se eth_getBalance(0x6F2a…c19E, N) retornar 3,529480 ETH, o histórico está completo. Falhas deixam marcas: ignorar rastros subestima o saldo exatamente pelo crédito interno (1,0); contar o valor da transação falha subestima o saldo por esse valor; esquecer seu gas superestima o saldo em 0,0021; omitir saques deixa uma diferença de quantias pequenas e regulares. O gas é o gas usado × o preço efetivo do gas informado no recibo, nunca o limite de gas, mais as taxas de blob para transações do tipo 3.
Saldos de tokens: eventos versus balanceOf
Os saldos de tokens ficam no armazenamento do contrato do token. Eles podem ser calculados pela soma dos eventos Transfer ou lidos com balanceOf(holder) por meio de eth_call no bloco N:
-- Saldos de tokens no bloco 21.000.000, calculados a partir de eventos Transfer
WITH moves AS (
SELECT to_address AS holder, amount_raw AS delta
FROM token_transfers
WHERE token_address = '0x5d3a…7c10' AND block_number <= 21000000
UNION ALL
SELECT from_address AS holder, -amount_raw AS delta
FROM token_transfers
WHERE token_address = '0x5d3a…7c10' AND block_number <= 21000000
)
SELECT holder, SUM(delta) / POWER(10, 6) AS balance -- o token tem 6 casas decimais
FROM moves
GROUP BY holder
HAVING SUM(delta) <> 0;
Cada transferência se torna um crédito e um débito. Emissões são transferências a partir do endereço zero, portanto o endereço zero termina com um saldo negativo igual a tudo o que foi emitido menos tudo o que foi queimado por envio a ele: uma verificação de oferta incorporada. Mantenha os inteiros brutos em um tipo decimal de alta precisão até a divisão final.
Eventos e balanceOf divergem por motivos conhecidos: tokens com rebase alteram todos os saldos por meio de um multiplicador global, sem evento por detentor; tokens que rendem juros calculam balanceOf a partir de um índice crescente; tokens com taxa de transferência creditam ao destinatário menos do que o remetente envia, e alguns emitem o valor bruto; e alguns contratos mais antigos ou controlados pelo emissor emitem tokens sem Transfer ou emitem um evento personalizado ao congelar e queimar. Para esses casos, o estado é a verdade e os eventos contam apenas a história das transferências explícitas.
Retratos de saldos e conciliação
Um saldo "em 31 de março" é ambíguo até ser vinculado a um bloco: use o último bloco cujo timestamp seja igual ou anterior ao momento de corte, em UTC, e registre sua altura e seu hash. Conforme a EIP-1898, eth_getBalance e eth_call aceitam um hash de bloco, o que garante que a resposta venha daquele bloco exato, mesmo que uma reorganização posteriormente substitua o bloco naquela altura. Para retratos de saldos em múltiplas blockchains, cada blockchain tem seu próprio bloco para o mesmo instante (os blocos de aproximadamente 10 minutos do Bitcoin oferecem intervalos menos precisos do que os slots de 12 segundos do Ethereum, e um bloco de rollup pode refletir depósitos que sua L1 acabou de incluir); informe o bloco escolhido em cada uma.
Uma reconstrução não está concluída até ser verificada: compare os saldos calculados com o estado (todos os endereços no escopo, ou uma amostra em vários blocos); verifique se os saldos de tokens calculados, excluindo o endereço zero, equivalem a totalSupply() no mesmo bloco; trate qualquer saldo corrente negativo como um crédito ausente; verifique os limites (zero antes da primeira atividade e o que uma varredura conhecida deixou); e estabeleça uma tolerância com antecedência, documentando a causa de cada diferença residual.
| Sintoma | Causa provável |
|---|---|
| Saldo calculado abaixo do correto por quantias correspondentes a pagamentos de contratos | Chamadas internas ausentes nos rastros |
| Saldo calculado acima do correto por pequenas quantias | Gas de transações falhas não deduzido |
| Pequenos créditos regulares sem explicação | Saques da camada de consenso ou recompensas do destinatário das taxas |
Saldos dos detentores de tokens divergem de balanceOf diariamente | Token com rebase ou que rende juros |
Soma dos saldos difere de totalSupply() | Emissões ou queimas sem eventos Transfer |
Foco no exame: espere questões de cenário sobre quem atuou e por que um saldo não concilia: os quatro campos da conta e o significado do nonce; por que saldos misturados tornam o rastreamento no nível de moedas uma convenção; classificação de EOAs, contratos e EOAs delegadas por EIP-7702 em um bloco (0xef0100, EIP-6780); transações internas como chamadas de mensagem visíveis apenas nos rastros; saques e recompensas de taxas como créditos que não são transações; os papéis no ERC-4337 eUserOperationEvent.sender; saldos calculados versus saldos do estado; os componentes do saldo nativo, especialmente o gas em transações falhas; saldos de tokens a partir de eventos e a verificação de oferta pelo endereço zero; tokens que invalidam as somas de eventos; fixação de retratos de saldos por altura e hash (EIP-1898); o que cada tipo de diferença sugere.