Por BlockSec Team (@BlockSecTeam)

Na semana passada, o protocolo Compound apresentou um bug que acidentalmente enviou um grande número de tokens COMP para os usuários. A causa deste bug (bug 2 neste artigo) se deve à correção incorreta de outro bug (Bug 1 neste artigo) que havia sido descoberto anteriormente.
Neste artigo, vamos detalhar a causa raiz do primeiro bug e o motivo pelo qual a correção do primeiro bug causou o segundo bug.
Contexto
O protocolo Compound é baseado no Whitepaper do Compound. Por meio dos contratos cToken, contas na blockchain fornecem capital (Ether ou tokens ERC-20) para receber cTokens ou tomar emprestado ativos do protocolo (mantendo outros ativos como garantia). Os contratos cToken do Compound rastreiam esses saldos e definem algoritmicamente as taxas de juros para os tomadores.
Para incentivar os usuários, aqueles que fornecem liquidez ao Compound (fornecendo capital) podem receber os juros. Especificamente, os usuários fornecem ativos (por exemplo, Ether ou outros tokens ERC20) ao Compound e recebem os cTokens correspondentes. Quando o cToken é devolvido ao Compound, os ativos subjacentes (Ether ou tokens ERC20) e os juros serão retornados ao usuário, caso ele não possua nenhuma dívida no Compound. Por exemplo, se um usuário possui 1000 Ether, ele pode depositar o ativo no Compound através de cEth.mint(1000) para obter o cToken.

O cToken representa os ativos subjacentes que foram bloqueados no Compound. O usuário pode ainda usar o cToken como garantia para tomar emprestado outros ativos. Por exemplo, um usuário pode depositar 1000 Ether através de ceth.mint(1000) e então usar os cTokens obtidos para tomar emprestado x Dai equivalente a 75 Ether (supercolateralização — este número depende do fator de colateral) através de cDai.borrow(x).
A lógica central é implementada no contrato Comptroller. Ele mantém os estados de um usuário, por exemplo, quantos tokens foram depositados no Compound pelo usuário, quantos tokens foram tomados emprestados pelo usuário e se o usuário pode tomar emprestado mais tokens. As funções invocadas neste processo incluem getHypotheticalAccountLiquidityInternal(), borrowAllowed(), mintAllowed(), entre outras.
O Compound também possui o token de governança chamado COMP. O token COMP pode ser usado para votar em propostas. Além disso, o token COMP pode ser negociado em exchanges. Atualmente, o preço do COMP está em torno de $300.
Bug 1
Em 31 de setembro de 2021, houve uma nova proposta (Proposta 62) no Compound DAO, com o objetivo de corrigir um bug no Comptroller.

O bug está relacionado ao CompSpeed, que representa o número de tokens COMP que podem ser distribuídos aos usuários em cada bloco.
O Fluxo da Função mint
A seguir, usaremos a função mint para descrever a causa deste bug. A cadeia de invocação da função mint é: mint → mintInternal → mintFresh.

Na função mintFresh, ela invoca o mintAllowed e então atualiza o saldo de cToken do usuário.

Na função mintAllowed, ela primeiro invoca updateCompSupplyIndex e depois distributeSupplierComp para 1) atualizar o compSupplyState do mercado e 2) distribuir os tokens COMP aos usuários.
updateCompSupplyIndex

A função updateCompSupplyIndex atualizará o status de cada mercado, principalmente o compSupplyState[cToken].

Na estrutura CompMarketState, ela registra o número do bloco (block) desta atualização e o índice de bônus (index) que afetará o número de tokens COMP que devem ser distribuídos aos usuários (que possuem o cToken).
O que é o índice de bônus (index) para cada token? Este é o valor acumulado ao longo do tempo (mostrado na fórmula a seguir).

Isso mostra o número de COMP que deve ser distribuído aos usuários (para cada cToken que o usuário possui).
distributeSupplierComp
A outra função distributeSupplierComp é responsável por registrar o número de tokens COMP que devem ser distribuídos ao usuário (fornecedor) em compAccrued[supplier].

Especificamente, ela atualiza o índice de bônus global em compSupplyState (na função updateCompSupplyIndex). Então na função distributeSupplierComp, o supplyIndex registra o índice de bônus atual, e o supplierIndex mostra o último índice de bônus para o usuário (fornecedor). O valor delta (supplyIndex - supplierIndex) * saldo de cToken do usuário mostra o número de tokens COMP que devem ser distribuídos ao usuário.
A Causa do Bug 1
Há outra função setCompSpeed para ajustar o supplySpeed do mercado (compSpeeds[address[cToken]]).

Isso ocorre porque se definirmos o CompSpeed de um mercado como zero, significa que o token COMP não será distribuído aos usuários naquele mercado. Portanto, se quisermos primeiro desabilitar a distribuição de COMP para um mercado e depois reativá-la, podemos seguir estes passos:
- Passo I: Definir o
CompSpeed[cToken]como zero para desabilitar a distribuição de tokens COMP. - Passo II: Invocar a função
setCompSpeedpara definirCompSpeed[cToken]como um valor diferente de zero.

Passo I: Para mercados que tiveram a distribuição de tokens COMP desabilitada no Passo I (supplySpeed == 0), o bloco não é zero, pois o bloco é continuamente atualizado em updateCompSupplyIndex (else if (deltaBlocks > 0)).

Passo II: Ao executar a operação no Passo II, a função setCompSpeedInternal passará pela instrução else if (compSpeed != 0) (linha 1083). Então, nas linhas 1088 a 1093, há uma verificação if (compSupplyState[address(cToken)].index == 0 && compSupplyState[address(cToken)].block == 0) para inicializar o index e o block para um novo mercado. No entanto, como estamos reativando a distribuição de tokens COMP em um mercado existente (não um novo mercado), as instruções nas linhas 1090 e 1091 não serão executadas para inicializar o index e o block (já que compSupplyState[address(cToken)].block não é zero).
Em resumo, para um mercado atualmente desabilitado, o index é zero. No entanto, o block não é zero. Isso significa que quando reativamos o mercado desabilitado invocando setCompSpeed para definir CompSpeed[cToken] como um valor diferente de zero, o valor do índice NÃO será reinicializado para CompInitialIndex (1e36) (as linhas 1090 e 1091 não são executadas).
O Impacto do Bug 1
Investigamos mais a fundo a função distributeSupplierComp que é responsável por distribuir os tokens COMP.

O supplierIndex é compInitialIndex. No entanto, o supplyIndex ainda é zero devido ao bug, o que causará um underflow em Double memory deltaIndex = sub_(supplyIndex=0, supplierIndex=1e36).

Bug 2: Introduzido pela Correção do Bug 1
Para corrigir o bug, o dono do projeto altera a lógica do código. Especificamente, ele imediatamente inicializa o index para compInitialIndex ao inicializar um novo mercado.

Como o índice de bônus global (index) foi inicializado para compInitialIndex, o índice de bônus do usuário também deve ser inicializado para este valor. Vamos examinar a função distributeSupplierComp.

A condição if na linha 1234 não pode ser satisfeita mesmo que supplierIndex == 0, pois o supplyIndex é igual (não maior que) a compInitialIndex (1e36). Isso faz com que o supplierIndex NÃO seja devidamente inicializado para compInitialIndex (seu valor é 0). Então o deltaIndex (supplyIndex - supplierIndex) será compInitialIndex, em vez de zero. O supplierTokens se tornará um valor grande se o saldo de cToken do usuário não for zero.
Em resumo, se um usuário realizar a operação de mint antes da correção do bug 1, ele terá cTokens e o supplierIndex se tornará zero (já que o token COMP foi distribuído). Então, após a correção do bug 1 (que introduz o bug 2), quando o usuário invocar a função mint novamente, ele poderá obter um grande número de tokens COMP (1e36*ctoken.balanceOf(user)).
Mundo Real
Mostramos os mercados afetados a seguir:
0xF5DCe57282A584D2746FaF1593d3121Fcac444dC: cSAI
0x12392F67bdf24faE0AF363c24aC620a2f67DAd86: cTUSD
0x95b4eF2869eBD94BEb4eEE400a99824BF5DC325b: cMKR
0x4B0181102A0112A2ef11AbEE5563bb4a3176c9d7: cSUSHI
0xe65cdB6479BaC1e22340E4E755fAE7E509EcD06c: cAAVE
0x80a2AE356fc9ef4305676f7a3E2Ed04e12C33946: cYFI
Para o usuário (0xa7b95d2a2d10028cc4450e453151181cbcac74fc), o usuário obteve 4.466,542459954989867175 tokens COMP nesta transação (0x6416ed016c39ffa23694a70d8a386c613f005be18aa0048ded8094f6165e7308).

A depuração adicional da transação mostra que, devido ao bug 2, o deltaIndex é 1e36 e o usuário possuía o cToken naquele momento.



A Correção do Bug 2
A correção do bug 2 é simples. Ela altera a condição if na função distributeSupplierComp.

Lições
- Este é um bug causado pela correção de outro bug. Como revisar minuciosamente as alterações de código em projetos de alto perfil ainda é uma questão em aberto.
- O DAO pode eliminar o risco de centralização. No entanto, também torna o processo de resposta a incidentes de segurança mais lento.
- Os projetos DeFi de alto perfil podem adotar boas práticas de segurança de programas tradicionais, por exemplo, implantando um sistema de fuzzing eficiente com um processo de testes contínuo.
Sobre a BlockSec
A BlockSec é uma empresa pioneira em segurança blockchain, fundada em 2021 por um grupo de especialistas em segurança de renome mundial. A empresa está comprometida em aprimorar a segurança e a usabilidade para o emergente mundo Web3, visando facilitar sua adoção em massa. Para isso, a BlockSec oferece serviços de auditoria de segurança de contratos inteligentes e chains EVM, a plataforma Phalcon para desenvolvimento seguro e bloqueio proativo de ameaças, a plataforma MetaSleuth para rastreamento e investigação de fundos, e a extensão MetaSuites para construtores web3 que navegam com eficiência no mundo cripto.
Até o momento, a empresa atendeu mais de 300 clientes ilustres, como MetaMask, Uniswap Foundation, Compound, Forta e PancakeSwap, e recebeu dezenas de milhões de dólares americanos em duas rodadas de financiamento de investidores de destaque, incluindo Matrix Partners, Vitalbridge Capital e Fenbushi Capital.
Site oficial: https://blocksec.com/
Conta oficial no Twitter: https://twitter.com/BlockSecTeam



