Durante a semana passada (12/10/2026–18/10/2026), destacam-se os seguintes 3 incidentes de segurança notáveis, com perda total estimada em aproximadamente 3,87 milhões USD.
| Data | Incidente | Tipo | Perda estimada |
|---|---|---|---|
| 12/10/2026 | Autheo Network | Comprometimento de chave privada | ~1,76 milhões USD |
| 14/10/2026 | UNCX Network | Erro contábil | ~807 mil USD |
| 18/10/2026 | Qubic Wallet | Geração de seed fraca | ~1,30 milhões USD |
Os valores das perdas são estimativas baseadas no valor realizado.
- UNCX Network: Este incidente é analisado em detalhes porque seu cálculo incorreto da quantidade agregada de cotas oferece a lição mais clara da semana sobre a contabilidade de contratos.
Verificação de segurança gratuita
Uma verificação rápida de segurança com nosso mecanismo interno de análise automatizada.
Faça uma verificação gratuitaDestaque da semana: UNCX Network
Em 14/10/2026, atacantes exploraram os contratos Smart Token Vesting V2 da UNCX Network nas redes Ethereum, BNB Chain, Base e Robinhood Chain. O incidente afetou 10 ativos em 11 transações de drenagem confirmadas e resultou em aproximadamente 807,609 mil USD em valor realizado ou encaminhado [1]. A causa raiz foi um cálculo incorreto da quantidade agregada de cotas que misturava parcelas de vesting criadas com diferentes proporções entre tokens e cotas. Isso fez os registros de cada posição divergirem da oferta global de cotas, permitindo que posições controladas pelos atacantes sacassem mais tokens do que eles haviam fornecido.
Contexto
A UNCX Network é uma provedora de infraestrutura DeFi multichain cujo produto Smart Token Vesting V2 permite que projetos bloqueiem tokens ERC-20 segundo cronogramas de vesting baseados em tempo. Cada cronograma é representado por um ID de vesting e registra o token, o criador, o beneficiário, o montante total, o montante já liberado e uma ou mais parcelas de desbloqueio. Uma posição flexível que permite aportes adicionais pode receber novas parcelas após sua criação, enquanto seu criador pode cancelar a parte ainda não adquirida.
As posições de um mesmo token compartilham um único saldo no VestingManager, e o protocolo usa cotas para determinar quanto desse saldo pertence a cada posição. Sua contabilidade, portanto, tem duas camadas relacionadas:
Global pool, per token
create / topUp: receive tokens and mint shares
release: convert releasable shares into tokens
_totalTokenShares[token]: active shares across all positions
Position record, per vesting ID
_depositedShares[id]: share basis assigned to the position
totalAmount: token amount represented by its tranches
released: shares already paid to the beneficiary
Para uma liberação, o gerenciador primeiro calcula quanto do cronograma já foi adquirido. Ele converte essa fração de totalAmount em cotas adquiridas usando _depositedShares[vestingId], subtrai released e chama o resultado de releasableShares. Na fórmula abaixo, tokenBalance é o saldo ERC-20 atual do gerenciador para o token do cronograma, compartilhado por todas as posições desse token, enquanto _totalTokenShares[token] é a oferta global correspondente de cotas ativas. A quantidade de tokens paga depende da proporção entre ambos:
amount = releasableShares * tokenBalance / _totalTokenShares[token]
Depositar por meio das funções de vesting aumenta tanto o saldo de tokens quanto a oferta de cotas. Uma transferência direta de tokens ERC-20 para o gerenciador altera apenas o saldo de tokens e, portanto, muda a quantidade de tokens representada por cada cota. Esse modelo de cotas só pode sustentar posições agrupadas se todas as operações do ciclo de vida mantiverem conciliados os registros das posições e a oferta global.
Análise da vulnerabilidade
O VestingManager afetado na Ethereum, em 0xA98F...99F5, contém um cálculo incorreto da quantidade agregada de cotas que se torna explorável por meio de release(), topUpVesting() e cancel(). Há implantações equivalentes afetadas nas outras três redes.
Para facilitar a comparação das mudanças de estado, a análise abaixo introduz D, R e V como abreviações. Esses símbolos não aparecem no código do contrato; a tabela associa cada um à variável ou expressão correspondente do contrato. Os valores associados se dividem em dois tipos de quantidade:
| Símbolo | Variável / expressão do contrato | Tipo de quantidade | Significado |
|---|---|---|---|
D |
_depositedShares[vestingId] |
Quantidade de cotas | Base cumulativa de cotas atribuída a todos os depósitos registrados para a posição |
R |
schedule.released |
Quantidade de cotas | Cotas cumulativas já pagas ao beneficiário |
| - | schedule.totalAmount |
Montante de tokens | Montante total de tokens registrado nas parcelas atuais da posição |
| - | vestedAmount |
Montante de tokens | Parte de totalAmount adquirida segundo essas parcelas até o momento atual |
V |
vestedShare |
Quantidade de cotas | Cotas adquiridas cumulativas calculadas pela aplicação de uma única proporção combinada de tokens adquiridos a D |
Em release(), o contrato calcula releasableShares, incrementa schedule.released e subtrai a mesma quantidade de _totalTokenShares[schedule.token]. Ele não reduz _depositedShares[vestingId]. Assim, após uma liberação, a posição contribui com apenas D - R cotas ativas para a oferta global, embora sua base armazenada continue sendo D.
function release(uint256 vestingId) external nonReentrant {
...
uint256 releasableShares = _getReleasableShares(vestingId);
uint256 balance = IERC20(schedule.token).balanceOf(address(this));
uint256 amount =
(releasableShares * balance) / _totalTokenShares[schedule.token];
schedule.released += releasableShares;
_totalTokenShares[schedule.token] -= releasableShares;
IERC20(schedule.token).safeTransfer(msg.sender, amount);
}
cancel() aplica uma única proporção combinada de aquisição à base agregada de cotas da posição. Primeiro, calcula vestedAmount por meio de getVestedAmount(vestingId); depois, converte a fração adquirida, denominada em tokens, em cotas como V = (vestedAmount * D) / totalAmount. Por fim, a função trata D - V como as cotas não adquiridas que podem ser removidas da posição. V é um valor cumulativo, não a quantidade de cotas adquiridas que resta após pagamentos anteriores.
function cancel(uint256 vestingId) external nonReentrant {
...
uint256 vestedAmount = getVestedAmount(vestingId);
uint256 remainingAmount = schedule.totalAmount - vestedAmount;
uint256 vestedShare =
vestedAmount * _depositedShares[vestingId] / schedule.totalAmount;
uint256 sharesToRemove =
_depositedShares[vestingId] - vestedShare;
_totalTokenShares[schedule.token] -= sharesToRemove;
_depositedShares[vestingId] = vestedShare;
schedule.totalAmount = vestedAmount;
schedule.cancelled = true;
IERC20(schedule.token).safeTransfer(schedule.creator, remainingAmount);
}
A falha está no próprio cálculo combinado. Cada parcela tem seu próprio montante de tokens, progresso de aquisição e número de cotas emitidas segundo a proporção do pool vigente quando foi adicionada. Portanto, suas cotas adquiridas deveriam ser calculadas separadamente e depois somadas. Em vez disso, o contrato primeiro agrega os montantes de tokens e a base de cotas e depois aplica uma única proporção de aquisição à posição combinada, pois armazena apenas um D e um totalAmount. As duas abordagens só são equivalentes quando as parcelas têm a mesma proporção de cotas por token ou o mesmo progresso de aquisição. Uma transferência direta de tokens altera a proporção do pool antes de um aporte adicional; assim, a nova parcela recebe muito menos cotas por token do que a parcela original. Aplicar uma única proporção combinada de aquisição às duas parcelas pode, então, produzir um V que já não representa as cotas adquiridas pelas duas em conjunto.
Como R já foi pago a partir das cotas adquiridas cumulativas da posição, um cronograma consistente deve satisfazer R <= V. A violação dessa desigualdade é o sintoma direto do cálculo combinado. O código não mantém um saldo separado de cotas ativas para a posição; ele subtrai D - V diretamente do _totalTokenShares global. Suponha que outras posições contribuam com 1000 cotas, enquanto esta posição tenha D = 100 e R = 80. Antes do cancelamento, a oferta global contém 1,000 + (D - R) = 1,020 cotas. Se o cálculo combinado produzir V = 60, cancel() subtrai D - V = 40 e deixa apenas 980 cotas globais, 20 a menos do que as 1000 cotas contribuídas pelas outras posições. Se a oferta global contivesse apenas as 20 cotas ativas desta posição, subtrair 40 causaria um underflow e reverteria a transação; a dedução excedente só é bem-sucedida porque o pool compartilhado contém cotas contribuídas por outras posições. A função então armazena _depositedShares[vestingId] = 60, enquanto schedule.released permanece em 80, deixando o registro da posição no estado impossível D < R.
O código não impõe R <= V nem limita sharesToRemove às cotas ativas da posição. Portanto, o cancelamento pode subtrair cotas históricas que já não fazem parte da oferta global. Ao mesmo tempo, transfere totalAmount - vestedAmount diretamente em unidades de token, sem converter o reembolso pela proporção de cotas do pool. O cancelamento, por si só, não produz um pagamento excedente; ele apenas reduz a oferta global de cotas para abaixo das cotas ativas registradas nas posições, criando a condição para um resgate excessivo posterior.
Análise do ataque
A análise a seguir usa uma transação representativa na Ethereum, 0x352065...c23823. O contrato de ataque executou a mesma sequência principal contra cinco tokens dentro da transação, usando liquidez de flash loans para cada token.
-
Etapa 1: O atacante criou uma pequena posição com aquisição imediata,
#5058, e duas posições muito maiores com aquisição imediata, flexíveis e que permitiam aportes adicionais,#5059e#5060. A pequena posição seria mantida para o saque final, enquanto as grandes posições forneceriam os registros históricos de cotas necessários para corromper a contabilidade global. -
Etapa 2: O atacante liberou as duas grandes posições. Na proporção inicial do pool, próxima de 1:1, cada liberação devolveu aproximadamente seu depósito líquido. As cotas correspondentes saíram de
_totalTokenShares, mas os valores originais permaneceram nos registros de_depositedSharesdas posições#5059e#5060e foram registrados como liberados apenas no campo separadoreleasedde cada cronograma. -
Etapa 3: O atacante transferiu tokens diretamente para o
VestingManager. Isso recompôs o saldo de tokens sem restaurar as cotas liberadas, de modo que a quantidade de tokens representada por cada cota restante aumentou acentuadamente. -
Etapa 4: O atacante fez aportes adicionais nas grandes posições já liberadas. Como os aportes foram precificados pela proporção, agora elevada, de tokens por cota, depósitos substanciais de tokens emitiram apenas um pequeno número de novas cotas. As posições passaram a combinar uma grande base histórica desatualizada com uma pequena contribuição de cotas ativas.
-
Etapa 5: O atacante cancelou as duas posições que haviam recebido aportes adicionais. Cada cancelamento aplicou uma única proporção combinada de aquisição ao histórico agregado de cotas depositadas. O
vestedSharerecalculado ficou abaixo das cotas já liberadas, de modo quesharesToRemoveexcedeu a contribuição ativa da posição. A oferta global foi reduzida em cerca de5.0e25cotas fictícias por posição, embora cada posição tivesse adicionado apenas uma pequena quantidade de cotas ativas durante o aporte adicional. -
Etapa 6: Com o denominador muito subestimado, o atacante liberou a pequena posição mantida. No exemplo de token analisado, um depósito líquido de aproximadamente
5.73e22unidades brutas permitiu sacar aproximadamente1.07e26unidades brutas, cerca de 1,873 vezes o depósito. O atacante repetiu ciclos de criação e liberação explorando a proporção distorcida e quitou a liquidez obtida por flash loans.

O relatório oficial do incidente identificou o mesmo padrão em 11 transações de drenagem confirmadas. Algumas variantes foram concluídas atomicamente, enquanto outras realizaram a liberação final em uma transação vinculada. A UNCX calculou o valor aproximado de 807,609 mil USD a partir dos recursos realizados ou encaminhados, após excluir o capital dos atacantes, o volume temporário de flash loans e os ativos devolvidos [1].
Comece a usar o Phalcon Explorer
Explore as transações para agir com critério
Experimente gratuitamente agoraConclusão
A causa raiz foi uma falha na contabilidade das cotas agregadas. O protocolo combinou parcelas criadas com diferentes proporções entre tokens e cotas e, em seguida, aplicou uma única proporção de aquisição à base agregada de cotas dessas parcelas. Isso calculou incorretamente as cotas adquiridas cumulativas, permitiu que uma posição removesse mais cotas do que ainda contribuía e fez os registros de cada posição divergirem da oferta global de cotas. Os atacantes exploraram a oferta global subestimada para sacar do saldo compartilhado tokens que continuavam necessários para atender a outras posições de vesting.
Um modelo contábil que permita depósitos posteriores com proporções de cotas variáveis deve preservar o montante de tokens, a base de cotas e o progresso de aquisição de cada parcela, em vez de combiná-los sob uma única proporção. Também deve garantir que os pagamentos cumulativos nunca excedam as cotas adquiridas cumulativas e que uma posição não possa remover mais do que sua contribuição ativa.
Referências
[1] Relatório do incidente do Smart Token Vesting V2 da UNCX Network
Melhor auditoria de segurança para Web3
Valide o design, o código e a lógica de negócios antes do lançamento



