Back to Blog

Vulnerabilidades do Revest Finance: Mais do que Reentrada

Code Auditing
March 31, 2022
9 min read

Em 27 de março de 2022, o projeto DeFi de staking Revest Finance na Ethereum foi atacado devido ao mecanismo de call-back do ERC-1155, o que causou o roubo de tokens no valor de aproximadamente $2M (especificamente BLOCKS, ECO, LYXe e RENA). Analisamos o ataque em primeiro lugar e publicamos no Twitter nossa análise naquela noite (UTC+8).

Na verdade, no momento em que escrevemos o Twitter, ainda tínhamos algumas dúvidas sobre uma função no contrato TokenVault da Revest. Investigamos o contrato tentando entender sua funcionalidade. Posteriormente, descobrimos que se trata de outra vulnerabilidade crítica de dia zero, que pode ser explorada de uma forma muito mais simples e pode causar as mesmas enormes perdas (como o ataque que aconteceu).

Em seguida, contatamos imediatamente a equipe da Revest Finance, e eles responderam rapidamente e propuseram uma solução alternativa para a vulnerabilidade. Após confirmar que a vulnerabilidade não podia ser acionada, decidimos publicar este blog.

A seguir, este blog é composto por três partes: o mecanismo da Revest Finance, o ataque de re-entrância original e a nova vulnerabilidade de dia zero.

O que é o FNFT da Revest Finance

O Financial Non-Fungible Token (FNFT) da Revest Finance torna possível a transferência sem necessidade de confiança de direitos futuros sobre ativos bloqueados. O contrato de entrada (contrato Revest) fornece três interfaces diferentes para cunhar FNFTs bloqueando ativos subjacentes:

  • mintTimeLock: o ativo subjacente será desbloqueado após um período de tempo.
  • mintValueLock: o ativo subjacente será desbloqueado quando seu valor subir acima ou cair abaixo de um valor predeterminado.
  • mintAddressLock: o ativo subjacente será desbloqueado por uma conta predeterminada.

O contrato Revest conecta os outros três contratos para bloquear e desbloquear ativos subjacentes.

  • FNFTHandler: herdado do token ERC-1155. Ele cria um novo FNFT com o fnftId incremental para cada bloqueio. O bloqueio determina o fornecimento total do novo FNFT na criação. O FNFT não pode ser cunhado de nenhuma outra forma, mas pode ser queimado para desbloquear ativos subjacentes.

  • LockManager: registra as condições de desbloqueio para cada bloqueio durante a criação e decide se o bloqueio pode ser desbloqueado no momento do desbloqueio.

  • TokenVault: recebe e envia os ativos subjacentes e registra os metadados para cada FNFT, como o valor de um FNFT específico.

Usamos mintAddressLock como exemplo para ilustrar o processo de cunhagem de FNFTs.

Figura 1
Figura 2

As duas figuras acima descrevem como um FNFT é criado, cunhado e queimado. Especificamente, o usuário A bloqueia 100 WETH na Revest Finance, criando o FNFT correspondente com fnftId igual a 1. Por fim, ele cunha 100 1-FNFT para destinatários específicos com participações específicas.

Observe que, uma vez que o ativo subjacente é desbloqueado, cada 1-FNFT pode ser queimado para receber um (*1e18) WETH. Conforme mostrado na Figura 2, o usuário B retira 25 (* 1e18) WETH ao queimar 25 1-FNFT.

Além disso, o contrato Revest fornece outra interface, chamada depositAdditionalToFNFT, que apresenta duas vulnerabilidades que serão discutidas a seguir.

Primeiro usamos as duas figuras a seguir para descrever o uso normal dessa função.

Figura 3
Figura 4

A função depositAdditionalToFNFT bloqueia mais ativos subjacentes em um bloqueio existente (especificado por fnftId). Razoavelmente (Figura 3), ela exige que a quantidade especificada seja igual ao fornecimento total do FNFT especificado e, em seguida, distribui uniformemente os ativos adicionados a cada FNFT especificado.

Caso contrário (Figura 4), ela cria um novo bloqueio com o fnftId mais recente, queima as quantidades especificadas do FNFT antigo e cunha a quantidade especificada do novo FNFT, e então registra o depositAmount do novo bloqueio como a soma do depositAmount do bloqueio antigo e o valor especificado, conforme mostrado no código a seguir.

// Agora, transferimos para o cofre de tokens
if(fnft.asset != address(0)){
    IERC20(fnft.asset).safeTransferFrom(_msgSender(), vault, quantity * amount);
}

ITokenVault(vault).handleMultipleDeposits(fnftId, newFNFTId, fnft.depositAmount + amount);

emit FNFTAddionalDeposited(_msgSender(), newFNFTId, quantity, amount);

Como o depositAmount registrado no contrato TokenVault indica a quantidade do ativo subjacente que um FNFT específico pode retirar, essa operação transfere o valor das quantidades especificadas do FNFT antigo do bloqueio antigo para o novo bloqueio.

(A quantidade especificada maior que o fornecimento total reverterá a transação)

O que é a Vulnerabilidade de Re-entrância

Nesta parte, ilustraremos como o ataque de re-entrância funciona e discutiremos a causa raiz e o método de correção.

Figura 5
Figura 6
Figura 7

As três figuras acima descrevem basicamente todo o processo do ataque de re-entrância. Especificamente, o atacante primeiro bloqueia zero tokens RENA para cunhar 2 1-FNFT que não têm valor. Em seguida, o atacante bloqueia zero tokens RENA novamente, mas cunha 360.000 2-FNFT que também não têm valor (por enquanto). Durante a última etapa, o atacante re-entra na função depositAdditionalToFNFT do contrato Revest por meio do mecanismo de call-back do FNFTHandler herdado do padrão de token ERC-1155, o que sobrescreve o depositAmount do bloqueio com fnftId igual a 2 antes da atualização do fnftId. Como resultado, o atacante obtém 360.001 2-FNFT com depositAmount igual a 1e18, o que significa que ele pode retirar 360.001 * 1e18 RENA do contrato TokenVault. Além disso, o único custo é 1e18 RENA.

Método de Correção

Os códigos da Revest Finance estão completamente alinhados com o padrão clássico de re-entrância: usar fnftId -> chamada externa com mecanismo de callback -> atualizar fnftId. Portanto, a maneira mais direta de corrigir os problemas é quebrar esse padrão. O código corrigido é mostrado abaixo:

function mint(
    address account, 
    uint id, 
    uint amount, 
    bytes memory data
) external override onlyRevestController {
    require(amount > 0, "Invalid amount");
    require(supply[id] == 0, "Repeated mint for the same FNFT");
    supply[id] += amount;
    fnftsCreated += 1;
    _mint(account, id, amount, data);
}

Primeiro, ele move a operação de atualização para antes da chamada externa (_mint), o que pode evitar o ataque. Segundo, como o sistema não permite cunhar zero FNFT nem cunhar repetidamente o mesmo FNFT, ele adiciona duas verificações para garantir que o sistema funcione conforme esperado, o que pode melhorar a segurança do sistema.

A Nova Vulnerabilidade de Dia Zero

Ao analisar o código da Revest Finance, a função handleMultipleDeposits no contrato TokenVault sempre nos confundiu, cujo código é mostrado abaixo.

function handleMultipleDeposits(
    uint fnftId,
    uint newFNFTId,
    uint amount
) external override onlyRevestController {
    require(amount >= fnfts[fnftId].depositAmount, 'E003');
    IRevest.FNFTConfig storage config = fnfts[fnftId];
    config.depositAmount = amount;
    mapFNFTToToken(fnftId, config);
    if(newFNFTId != 0) {
        mapFNFTToToken(newFNFTId, config);
    }
}

Durante a chamada à função depositAdditionalToFNFT, a função handleMultipleDeposits altera o depositAmount do bloqueio antigo ou registra o do novo. Quando o newFNFTId é zero, ela não registra o depositAmount do novo bloqueio, pois esta é uma operação para adicionar ativos extras ao bloqueio existente.

De acordo com o senso comum, quando o newFNFTId não é zero, ela apenas registra o depositAmount do novo bloqueio, mas não altera o depositAmount do antigo. No entanto, o código nos mostra que ele não só registra o depositAmount do novo bloqueio, mas também altera o depositAmount do antigo.

Acreditamos que essa é uma séria vulnerabilidade lógica de dia zero e então escrevemos um PoC para verificá-la. As três figuras a seguir descrevem como o PoC funciona.

Figura 8
Figura 9
Figura 10

Especificamente, o atacante primeiro bloqueia zero RENA para cunhar 360.000 1-FNFT. Depois disso, o atacante invoca diretamente a função depositAdditionalToFNFT para criar um novo bloqueio. Devido ao bug lógico, o contrato TokenVault altera incorretamente o depositAmount do bloqueio antigo de zero para 1e18. Como resultado, o atacante obtém 359.999 1-FNFT no valor de 359.999 RENA. Obviamente, o PoC é muito mais simples do que o ataque de re-entrância real.

A Solução Alternativa para Corrigir a Vulnerabilidade

Este é um bug lógico, e recomendamos usar o seguinte código para corrigi-lo.

function handleMultipleDeposits(
    uint fnftId,
    uint newFNFTId,
    uint amount
) external override onlyRevestController {
    require(amount >= fnfts[fnftId].depositAmount, 'E003');
    IRevest.FNFTConfig memory config = fnfts[fnftId];
    config.depositAmount = amount;
    if(newFNFTId != 0) {
        mapFNFTToToken(newFNFTId, config);
    } else {
        mapFNFTToToken(fnftId, config);
    }
}

Como os dois contratos vulneráveis: TokenVault e FNFTHandler armazenam muitos estados críticos, o projeto não pode reimplantar o contrato TokenVault e o contrato FNFTHandler sem migrar os estados. Para evitar ataques futuros a essa vulnerabilidade, o projeto reimplantou uma versão simplificada do contrato Revest, que desativa funções mais complexas para reduzir as superfícies disponíveis a qualquer potencial atacante. Após verificar a solução alternativa, acreditamos que o contrato Revest simplificado pode mitigar os possíveis ataques mencionados neste blog.

Conclusão

Tornar um projeto DeFi seguro não é uma tarefa fácil. Além da auditoria de código, acreditamos que a comunidade deve adotar um método proativo para monitorar o status do projeto e bloquear o ataque antes mesmo que ele ocorra.

Sobre a BlockSec

A BlockSec é uma empresa pioneira em segurança blockchain estabelecida 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, a fim de facilitar sua adoção em massa. Para tanto, a BlockSec fornece serviços de auditoria de segurança de contratos inteligentes e cadeias EVM, a plataforma Phalcon para desenvolvimento de segurança e bloqueio proativo de ameaças, a plataforma MetaSleuth para rastreamento e investigação de fundos, e a extensão MetaDock para construtores web3 navegarem 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 proeminentes, incluindo Matrix Partners, Vitalbridge Capital e Fenbushi Capital.

Site oficial: https://blocksec.com/

Conta oficial no Twitter: https://twitter.com/BlockSecTeam

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit