Notional Finance hackeada por US$1,73 milhão devido a vulnerabilidade de transbordamento de inteiro

iconMetaEra
Compartilhar
AI summary iconResumo
Um exploit de US$ 1,73 milhão atingiu a Notional Finance em 4 de setembro de 2026, quando atacantes exploraram um transbordamento de inteiro no contrato Escrow. A falha na função ExchangeRate._convertToETH permitiu a criação de posições fCash sem colateral adequado. O incidente destaca a importância de uma boa relação risco-recompensa em estratégias DeFi. Traders que utilizam TA para cripto devem permanecer atentos, pois tais vulnerabilidades podem alterar rapidamente a dinâmica do mercado.

Autor: Jiu Jiu

Edição: 77

Contexto

Em 4 de setembro de 2026, a conhecida plataforma de empréstimos descentralizada Notional Finance sofreu um ataque, com perdas de aproximadamente US$ 1,73 milhão. Abaixo está a análise detalhada da equipe de segurança SlowMist sobre este incidente:

Conhecimentos prévios

No Notional Finance V1, o fCash pode ser entendido como um crédito em dinheiro com data de vencimento. O recebedor de caixa recebe o pagamento no vencimento, e o pagador de caixa paga no vencimento. O protocolo registra ambos os posicionamentos no Portfolio e avalia a saúde de uma conta para determinar se ela pode abrir um posicionamento por meio da verificação de ativos colaterais livres.

O safeTransferFrom do ERC1155Trade desempenha aqui a função de contabilidade de ativos. Quando o tipo de ativo passado pertence ao cash receiver, o contrato chama Portfolios.mintfCashPair, registrando ao mesmo tempo uma dívida para o payer e um crédito equivalente para o receiver. Essa chamada parece uma transferência ERC1155, mas resulta em uma cunhagem em par de fCash.

Após a nova posição ser escrita no Portfolio, é necessário calcular e verificar os ativos livres. O contrato RiskFramework primeiro soma as obrigações de fCash do pagador como um int256 com sinal e, em seguida, converte os saldos de cada moeda em ETH para somar. Números negativos representam caixa a ser pago, enquanto números positivos representam caixa ou créditos detidos pela conta. Esse resultado determina se a transação pode ser permitida.

Após a expiração da posição, o Portfolio chamará a função portfolioSettleCash do Escrow para converter o fCash vencido em saldo em dinheiro. A conta então retirará os ativos DAI ou USDC correspondentes por meio da função de saque do Escrow.

Causa raiz

A vulnerabilidade central deste ataque reside na função ExchangeRate._convertToETH utilizada no contrato de implementação do Escrow, onde um trecho de código converte diretamente o valor absoluto de um inteiro assinado para uint128.

O atacante primeiro criou duas posições de dívida com o mesmo payer, com valores de 1 e uint128.max. A soma dessas duas dívidas resulta exatamente em 2^128. Como os pagamentos do payer são registrados como números negativos no cálculo de risco, o parâmetro crítico passado para o convertBalancesToETH do Escrow é -2^128.

No entanto, a versão do Solidity utilizada neste contrato é a 0.6.x, portanto, seu valor absoluto 2^128, ao ser convertido forçadamente para uint128, transbordará e se tornará 0, fazendo com que esse passivo não entre no resultado denominado em ETH.

Por fim, na função mintfCashPair, apenas é exigido que o ativo livre do pagador seja maior ou igual a zero, permitindo que a verificação seja aprovada mesmo após o estouro do resultado para 0, permitindo que uma conta sem ativos suficientes para cobrir os passivos ainda estabeleça uma posição.

Análise dos passos do ataque

1. Na transação anterior (0xe1589a19…d60a), o atacante primeiro criou vários contratos auxiliares e chamou a função setApprovalForAll do contrato ERC1155Trade para autorizar os contratos auxiliares; além disso, consultou antecipadamente as datas de vencimento de dois CashMarket e calculou os valores dos três parâmetros AssetId necessários para as operações subsequentes.

2. Em seguida, o atacante invoca a função safeTransferFrom do contrato ERC1155Trade para cunhar um par fCash no valor de 1, com o cashGroupId correspondente ao ativo de títulos igual a 2 e o carimbo de data/hora de vencimento igual a 1788480000 (4 de setembro de 2026, 8:00). Isso invoca a função interna _upsertAsset para atualizar, respectivamente, os passivos e a receita esperada dos endereços from (contrato atacante) e to (contrato receptor 1).

Após a cunhagem do par ser concluída, os Portfolios verificam imediatamente os ativos de garantia livre do contrato de ataque; como a quantidade da primeira cunhagem é muito pequena, ao ser convertida para ETH com a taxa e precisão atuais, ela é arredondada para zero, permitindo que a primeira verificação seja aprovada.

3. O atacante chamou novamente a função safeTransferFrom do contrato ERC1155Trade para cunhar um par fCash com valor de uint128.max, onde o cashGroupId do ativo de títulos correspondente é 2, mas o carimbo de data/hora de vencimento é 1796256000 (3 de dezembro de 2026, 8:00).

Há um detalhe: o atacante cunhou ativos de títulos com datas de vencimento diferentes em dois endereços distintos. Isso ocorre porque, ao atualizar os passivos do endereço "from", se os ativos de títulos forem iguais, eles são diretamente acumulados, o que causa um revert durante a operação de adição devido a estouro (o contrato utiliza a biblioteca SafeMath).

4. Após atualizar os passivos e a receita esperada para o endereço de origem (contrato de ataque) e o endereço de destino (contrato de recebimento 2), a função mintfCashPair chama a função freeCollateral para calcular a posição líquida de colateral do endereço de origem e verificar se ela é maior ou igual a zero, garantindo que esteja saudável.

Será somado primeiro o total das dívidas das duas cunhagens do endereço from na função getRequirement do contrato RiskFramework:

O resultado final é -2^128, em seguida, a função convertBalancesToETH do contrato Escrow é chamada para converter esse valor para uma avaliação em ETH, e a função convertBalancesToETH por sua vez chama a função _convertToETH da biblioteca ExchangeRate para processamento de cálculo:

Ao rastrear até a função _convertToETH, é possível observar que ela primeiro pega o valor absoluto do balance e depois chama uint128 para converter de 256 bits para 128 bits. O valor total da dívida de "from" acima é -2^128; ao tomar o valor absoluto, 2^128, ao ser forçadamente convertido com uint128(), excede o limite superior e transborda para 0. Isso significa que o protocolo erroneamente considera a posição de colateral livre do endereço "from" como 0, o que a torna saudável, permitindo que passe pela verificação final na função mintfCashPair.

5. Em seguida, o contrato 2 recebeu e dividiu sua posição entre outros dois contratos auxiliares; esses safeTransferFrom ainda entrarão na função mintfCashPair. No entanto, como agora o pagador é o contrato 2, recebedor da segunda cunhagem, que detém a enorme posição de receita esperada obtida no passo anterior, o cálculo de risco a considera como um ativo positivo, permitindo que passe pela verificação de garantia. Assim, os dois contratos auxiliares obtiveram receiver fCash que poderá ser convertido em caixa ao vencimento.

6. O atacante então realizou uma segunda transação de lucro oficial, na qual os direitos correspondentes aos dois posições mantidas na transação anterior foram liquidados nos contratos auxiliares, aumentando os saldos em caixa dos ativos correspondentes, e posteriormente chamou a função withdraw do contrato Escrow para sacar seus ativos e realizar o lucro.

Resumo

O ponto-chave deste ataque é que o atacante primeiro utilizou duas posições separadas para somar os débitos do payer até 2^128, e depois fez com que o Escrow convertesse esse débito em ETH. Aproveitando-se de um vazamento de transbordamento na conversão de tipo, o resultado foi truncado para zero, permitindo que as verificações de risco fossem aprovadas.

A equipe de segurança SlowMist recomenda que os projetos realizem verificações de intervalo antes de realizar conversões de tipo, rejeitando diretamente resultados que excedam type(uint128).max. Ao mesmo tempo, todos os limites envolvendo quantidades assinadas, escala de precisão e valor nominal de ativos devem ser testados com valores extremos, especialmente cobrindo entradas como uint128.max, uint128.max + 1 e valores absolutos negativos. Pode-se consultar ou utilizar a biblioteca SafeCast da OpenZeppelin para processar as conversões.

Aviso legal: as informações nesta página podem ter sido obtidas de terceiros e não refletem necessariamente os pontos de vista ou opiniões da KuCoin. Este conteúdo é fornecido apenas para fins informativos gerais, sem qualquer representação ou garantia de qualquer tipo, nem deve ser interpretado como aconselhamento financeiro ou de investimento. A KuCoin não é responsável por quaisquer erros ou omissões, ou por quaisquer resultados do uso destas informações. Os investimentos em ativos digitais podem ser arriscados. Avalie cuidadosamente os riscos de um produto e a sua tolerância ao risco com base nas suas próprias circunstâncias financeiras. Para mais informações, consulte nossos termos de uso e divulgação de risco.