Back to Blog

Relatório de Testes de Segurança para Radiant V2

Code Auditing
March 23, 2023
36 min read

Manifesto do Relatório

Item Descrição
Cliente Radiant Capital
Alvo Radiant V2

Histórico de Versões

Versão Data Descrição
1.0 15 de março de 2023 Primeira Versão
2.0 21 de março de 2023 Segunda Versão

1. Introdução

1.1 Sobre Testes de Segurança

Fomos convidados pela Radiant Capital para realizar um teste de segurança (como equipe red team) nos contratos inteligentes do Radiant V2 para identificar riscos potenciais. Como uma equipe responsável, a Radiant Capital leva a segurança a sério. Por isso, a equipe decidiu dedicar mais esforços para proteger esses contratos inteligentes, embora eles já tenham sido auditados por múltiplas empresas de segurança ^1.

Observe que testes de segurança diferem de auditoria de segurança tanto em objetivos quanto em requisitos. Especificamente, os testes de segurança visam descobrir pontos vulneráveis extras/incomuns ao imitar atacantes para comprometer o programa/protocolo, enquanto a auditoria de segurança visa fornecer uma verificação de segurança relativamente abrangente ao enumerar as possíveis superfícies de ataque. Sendo assim, os testes de segurança podem não ser capazes de cobrir alguns bugs de lógica complexa que poderiam ser identificados por uma auditoria de segurança, devido ao tempo e recursos limitados.

1.2 Sobre os Contratos Alvo

Informação Descrição
Tipo Contrato Inteligente
Linguagem Solidity
Abordagem Análise estática, análise dinâmica, verificação semi-automática e manual

O repositório alvo é o Radiant_v2.1.1. Os valores de SHA do commit durante os testes de segurança são apresentados a seguir. Nosso relatório é responsável apenas pela versão inicial (ou seja, Versão 1), bem como pelos novos códigos para corrigir problemas no relatório.

Observe que este relatório cobre apenas os contratos inteligentes na pasta radiant_v2.1.1/contracts deste repositório, incluindo:

  • bounties
  • deployments
  • flashloan
  • leverage
  • lock
  • oracles
  • staking
  • zap
  • eligibility
  • misc
  • oft
  • protocol
  • stargate

Após a atualização na Versão 8, os arquivos cobertos neste teste de segurança incluem:

  • lending/AaveOracle.sol
  • lending/AaveProtocolDataProvider.sol
  • lending/ATokensAndRatesHelper.sol
  • lending/StableAndVariableTokensHelper.sol
  • lending/UiPoolDataProviderV2V3.sol
  • lending/UiPoolDataProvider.sol
  • lending/WETHGateway.sol
  • lending/WalletBalanceProvider.sol
  • lending/configuration
  • lending/flashloan
  • lending/lendingpool
  • lending/tokenization
  • radiant/accessories
  • radiant/eligibility
  • radiant/oracles
  • radiant/staking
  • radiant/token
  • radiant/zap

1.3 Modelo de Segurança

Para avaliar o risco, seguimos os padrões ou sugestões amplamente adotados tanto pela indústria quanto pela academia, incluindo a Metodologia de Avaliação de Risco da OWASP ^2 e a Enumeração de Fraquezas Comuns ^3. A severidade geral do risco é determinada pela probabilidade e pelo impacto. Especificamente, a probabilidade é usada para estimar com que frequência uma determinada vulnerabilidade pode ser descoberta e explorada por um atacante, enquanto o impacto é usado para medir as consequências de uma exploração bem-sucedida.

Neste relatório, tanto a probabilidade quanto o impacto são categorizados em duas classificações, ou seja, alto e baixo, respectivamente, e suas combinações são mostradas na Tabela 1.1.

Consequentemente, a severidade medida neste relatório é classificada em três categorias: Alto, Médio, Baixo. Por questão de completude, Indeterminado também é usado para cobrir circunstâncias em que o risco não pode ser bem determinado.

Além disso, o status de um item descoberto se enquadrará em uma das seguintes quatro categorias:

  • Indeterminado Sem resposta ainda.

  • Reconhecido O item foi recebido pelo cliente, mas ainda não confirmado.

  • Confirmado O item foi reconhecido pelo cliente, mas ainda não corrigido.

  • Corrigido O item foi confirmado e corrigido pelo cliente.

2. Testes de Segurança Automatizados

2.1 Testes de Segurança Estáticos Automatizados

Utilizamos nossa ferramenta de análise estática interna baseada no Slither para verificar a existência das vulnerabilidades. Após verificar os resultados manualmente, nenhum problema foi encontrado. Os resultados detalhados dos testes podem ser encontrados na Tabela 4.1 no Apêndice.

2.2 Testes de Segurança Dinâmicos Automatizados

Utilizamos técnicas de fuzzing para testar a robustez, confiabilidade e precisão dos contratos alvo. Especificamente, a semente inicial no processo de fuzzing é determinada com base na semântica das funções e nos scripts de teste do contrato. Para simular o ambiente on-chain, também mantemos um conjunto de endereços que interagiram com os contratos LendingPool e MultiFeeDistribution.

Nosso fuzzer também considera a semântica das funções durante a geração de sequências de transações. Por exemplo, a função stake no contrato MultiFeeDistribution e a função deposit no contrato LendingPool provavelmente serão invocadas primeiro na sequência. A mutação dos parâmetros das funções e da sequência é guiada pela cobertura de código do contrato. Se um determinado parâmetro ou sequência atingir uma cobertura de código maior, ele terá maior prioridade para ser mutado na próxima rodada de fuzzing. Para explorar alguns caminhos restritos por números mágicos, coletamos os valores lidos do armazenamento (ou seja, a instrução SLOAD) em tempo de execução e os usamos para gerar parâmetros de função durante o processo de mutação.

No total, geramos 100.000 casos de teste e utilizamos 31 oráculos, que são usados para detectar se uma falha ocorreu. Para cada caso de teste, ele contém 30 transações com ordens especificadas. Por fim, descobrimos um problema crítico (ou seja, Seção 3.2.6), que também foi descoberto em nosso processo de testes de segurança manual. Os resultados detalhados dos testes podem ser encontrados nas Tabelas 4.2, 4.3 e 4.4 no Apêndice.

3. Testes de Segurança Manuais

Envolvemos esforços manuais para entender o design geral e as interações entre diferentes módulos, e então conduzimos os testes de segurança com base em nosso conhecimento das potenciais superfícies de ataque derivadas de nossas pesquisas e experiências anteriores.

No total, encontramos dezessete problemas potenciais. Além disso, temos três recomendações e uma nota, conforme segue:

  • Risco Alto: 2

  • Risco Médio: 8

  • Risco Baixo: 7

  • Recomendações: 3

  • Notas: 1

ID Severidade Descrição Categoria Status
1 Médio Nenhuma Interface Reservada para Redefinição de Ponteiros de Função Segurança de Software Corrigido
2 Médio Cálculo Incorreto do Oráculo Segurança DeFi Corrigido
3 Alto Possível Drenagem de Fundos através do BaseBounty Segurança DeFi Corrigido
4 Baixo Possíveis Cronogramas de Emissão Inválidos Segurança DeFi Corrigido
5 Baixo Cronogramas de Emissão Ignoráveis Segurança DeFi Confirmado
6 Médio Taxa de Câmbio Alterável durante a Migração Segurança DeFi Corrigido
7 Alto Implementação Incorreta de _transfer() (I) Segurança DeFi Corrigido
8 Baixo Falta de Verificação do Período em UniV2TwapOracle Segurança DeFi Corrigido
9 Médio Tokens Residuais Não Reembolsáveis Segurança DeFi Corrigido
10 Médio Implementação Incorreta de _transfer() (II) Segurança DeFi Corrigido
11 Médio Recompensas de Compound Manipuláveis Segurança DeFi Corrigido
12 Médio Falta de Controle de Acesso em setLeverager() Segurança DeFi Corrigido
13 Médio Nenhuma Verificação de Slippage em addLiquidityWETHOnly() Segurança DeFi Confirmado
14 Baixo Falta de Verificação do borrowRatio em loopETH() Segurança DeFi Corrigido
15 Baixo Falta de Verificação do Comprimento entre assets e poolIDs em setPoolIDs() Segurança DeFi Corrigido
16 Baixo Falta de Revogação do Privilégio de mint em addBountyContract() Segurança DeFi Confirmado
17 Baixo Minters Só Podem Ser Atribuídos Uma Vez Segurança DeFi Confirmado
18 - Otimização de Gas (zapVestingToLp() em Mfd) Recomendação Corrigido
19 - Reserva de Bounty Não Vazia em BountyManager Recomendação Corrigido
20 - Nomenclatura Inconsistente em requiredUsdValue() Recomendação Confirmado
21 - Nota sobre MFDPlus Obsoleto Nota Confirmado

Os detalhes são fornecidos nas seções a seguir.

3.1 Segurança de Software

3.1.1 Problema Potencial 1: Nenhuma Interface Reservada para Redefinição de Ponteiros de Função

Item Descrição
Severidade Médio
Status Corrigido na Versão 7
Introduzido pela Versão 1

Descrição Três funções, getLpMfdBounty(), getChefBounty() e getAutoCompoundBounty(), são invocadas através de ponteiros de função no contrato BountyManager. Enquanto isso, a herança de OwnableUpgradable indica que este contrato seria a implementação de um proxy. Isso indica que o contrato de implementação pode ser atualizado no futuro, o que traz um problema relacionado aos ponteiros de função.

function initialize(
        address _rdnt,
        address _weth,
        address _lpMfd,
        address _mfd,
        address _chef,
        address _priceProvider,
        address _eligibilityDataProvider,
        uint256 _hunterShare,
        uint256 _baseBountyUsdTarget,
        uint256 _maxBaseBounty,
        uint256 _bountyBooster
    ) external initializer {
        require(_rdnt != address(0));
        require(_weth != address(0));
        require(_lpMfd != address(0));
        require(_mfd != address(0));
        require(_chef != address(0));
        require(_priceProvider != address(0));
        require(_eligibilityDataProvider != address(0));
        require(_hunterShare <= 10000);
        require(_baseBountyUsdTarget != 0);
        require(_maxBaseBounty != 0);
 
        rdnt = _rdnt;
        weth = _weth;
        lpMfd = _lpMfd;
        mfd = _mfd;
        chef = _chef;
        priceProvider = _priceProvider;
        eligibilityDataProvider = _eligibilityDataProvider;
 
        HUNTER_SHARE = _hunterShare;
        baseBountyUsdTarget = _baseBountyUsdTarget;
        bountyBooster = _bountyBooster;
        maxBaseBounty = _maxBaseBounty;
 
        bounties[1] = getLpMfdBounty;
        bounties[2] = getChefBounty;
        bounties[3] = getAutoCompoundBounty;
        bountyCount = 3;
 
        slippageLimit = 10;
        minDLPBalance = uint256(5).mul(10 ** 18);
 
 
        __Ownable_init();
        __Pausable_init();
    } 

Listagem 3.1: BountyManager.sol

Impacto Quando os deslocamentos das três funções mencionadas acima são alterados, os ponteiros de função não podem funcionar conforme esperado e toda a lógica do contrato pode ser alterada.

Sugestão O contrato deve fornecer interfaces para redefinir os ponteiros de função.

3.2 Segurança DeFi

3.2.1 Problema Potencial 2: Cálculo Incorreto do Oráculo

Item Descrição
Severidade Médio
Status Corrigido na Versão 11
Introduzido pela Versão 1 e Versão 4

Descrição A função consult() no contrato ComboOracle é usada para calcular o preço médio de várias fontes. Na implementação da versão 1, ela usa a média aritmética para calcular o preço final, que pode ser manipulado ao influenciar um dos oráculos de origem.

function consult() public view override returns (uint256 price) {
        require(sources.length != 0);

        uint256 sum;
        for (uint256 i = 0; i < sources.length; i++) {
            uint256 price = sources[i].consult();
            require(price != 0, "source consult failure");
            sum = sum.add(price);
        }
        price = sum.div(sources.length);
    }

Listagem 3.2: ComboOracle.sol

Na implementação da versão 4, quando o preço médio é maior que o menor preço×1,025, o menor preço será retornado. No entanto, o valor de retorno ainda pode ser manipulado se o resultado retornado de um dos oráculos de origem for anormalmente baixo.

/**
    * @notice Calculated price
    * @return price Average price of several sources.
    */
   function consult() public view override returns (uint256 price) {
       require(sources.length != 0);

       uint256 sum;
       uint256 lowestPrice;
       for (uint256 i = 0; i < sources.length; i++) {
           uint256 price = sources[i].consult();
           require(price != 0, "source consult failure");
           if (lowestPrice == 0) {
               lowestPrice = price;
           } else {
               lowestPrice = lowestPrice > price ? price : lowestPrice;
           }
           sum = sum.add(price);
       }
       price = sum.div(sources.length);
       price = price > ((lowestPrice * 1025) / 1000) ? lowestPrice : price;
   }

Listagem 3.3: ComboOracle.sol

Impacto O preço retornado pelo ComboOracle pode ser manipulado, o que permite ao atacante obter lucro a partir disso.

Sugestão Sugerimos usar o valor mediano em vez do valor médio. Se houver apenas dois oráculos de origem e uma diferença bastante grande ocorrer, é mais razoável reverter a transação quando o preço médio for consideravelmente maior que o menor preço.

Feedback Haverá apenas dois oráculos de origem. Se ocorrer uma diferença bastante grande, utilizaremos um OZ Defender Sentinel para pausar os contratos associados.

Nota O contrato ComboOracle foi removido e não é mais utilizado.

3.2.2 Problema Potencial 3: Possível Drenagem de Fundos através do BaseBounty

Item Descrição
Severidade Alto
Status Corrigido na Versão 4
Introduzido pela Versão 1

Descrição Um usuário pode bloquear tokens (ou seja, RDNT) por uma duração fixa para ganhar recompensas. Quando o bloqueio expira, outros usuários podem invocar a função executeBounty() para rebloquearem os tokens para esse usuário e ganhar o BaseBounty, caso este usuário tenha o AutoRelock habilitado. Durante o processo de rebloqueio, os bloqueios expirados serão limpos e recolocados no pool na função interna _cleanWithdrawableLocks(). No entanto, há uma variável maxLockWithdrawPerTxn que limita o número máximo de bloqueios que podem ser limpos. Nesse caso, bloqueios expirados não limpos podem ainda existir mesmo após a execução da função executeBounty(). Isso pode contornar a verificação na linha 106 da função claimBounty() no contrato MFDPlus. O issueBaseBounty será definido como verdadeiro e retornado.

**
    * @notice Withdraw all lockings tokens where the unlock time has passed
    */
   function _cleanWithdrawableLocks(
       address user,
       uint256 totalLock,
       uint256 totalLockWithMultiplier
   ) internal returns (uint256 lockAmount, uint256 lockAmountWithMultiplier) {
       LockedBalance[] storage locks = userLocks[user];

       if (locks.length != 0) {
           uint256 length = locks.length <= maxLockWithdrawPerTxn ? locks.length : maxLockWithdrawPerTxn;
           for (uint256 i = 0; i < length; ) {
               if (locks[i].unlockTime <= block.timestamp) {
                   lockAmount = lockAmount.add(locks[i].amount);
                   lockAmountWithMultiplier = lockAmountWithMultiplier.add(
                       locks[i].amount.mul(locks[i].multiplier)
                   );
                   locks[i] = locks[locks.length - 1];
                   locks.pop();
                   length = length - 1;
               } else {
                   i = i + 1;
               }
           }
           if (locks.length == 0) {
               lockAmount = totalLock;
               lockAmountWithMultiplier = totalLockWithMultiplier;
               delete userLocks[user];

               userlist.removeFromList(user);
           }
       }
   }

Listagem 3.4: MultiFeeDistribution.sol

Especificamente, o atacante pode fazer stake de 1 wei de token com o mesmo tempo de expiração múltiplas vezes, o que é consideravelmente maior que maxLockWithdrawPerTxn. Depois disso, o atacante pode definir a ação como getLpMfdBounty e invocar executeBounty() repetidamente. Como a quantidade de bloqueios limpos é limitada pelo maxLockWithdrawPerTxn, o BaseBounty no contrato BountyManager pode ser drenado pelo atacante.

Impacto O atacante pode drenar todos os fundos no contrato BountyManager em uma única transação, levando à interrupção dos mecanismos de bounty projetados.

Sugestão Garantir que a função _cleanWithdrawableLocks() possa limpar todos os bloqueios expirados e definir um valor mínimo de staking na função _stake().

3.2.3 Problema Potencial 4: Possíveis Cronogramas de Emissão Inválidos

Item Descrição
Severidade Baixo
Status Corrigido na Versão 10
Introduzido pela Versão 1

Descrição No contrato ChefIncentivesController, a função setEmissionSchedule() é invocada pelo proprietário para definir cronogramas para diferentes taxas de recompensa. Nesse caso, o tempo de início para cada cronograma (_startTimeOffsets[i] + startTime) deve ser validado para ser maior que o timestamp atual. No entanto, ele só verifica o primeiro elemento em _startTimeOffsets, o que não é suficiente. Além disso, o _startTimeOffsets[i] é convertido de uint256 para uint128 quando é adicionado ao emissionSchedule, o que pode ser truncado se a entrada original for muito grande.

function setEmissionSchedule(
        uint256[] calldata _startTimeOffsets,
        uint256[] calldata _rewardsPerSecond
    ) external onlyOwner {
        uint256 length = _startTimeOffsets.length;
        require(length > 0 && length == _rewardsPerSecond.length, "empty or mismatch params");
        if (startTime > 0) {
            require(_startTimeOffsets[0] > block.timestamp.sub(startTime), "invalid start time");
        }
 
        for (uint256 i = 0; i < length; i++) {
            emissionSchedule.push(
                EmissionPoint({
                    startTimeOffset: uint128(_startTimeOffsets[i]),
                    rewardsPerSecond: uint128(_rewardsPerSecond[i])
                })
            );
        }
        emit EmissionScheduleAppended(_startTimeOffsets, _rewardsPerSecond);
    } 

Listagem 3.5: ChefIncentivesController.sol

Impacto Se _startTimeOffsets não estiver em ordem crescente, algumas recompensas prometidas não serão distribuídas aos usuários. Se _startTimeOffsets[i] estiver fora do intervalo de uint128, um cronograma de emissão inválido será adicionado.

Sugestão Garantir que _startTimeOffsets esteja em ordem crescente e que todos os elementos estejam dentro do intervalo de uint128.

3.2.4 Problema Potencial 5: Cronogramas de Emissão Ignoráveis

Item Descrição
Severidade Baixo
Status Confirmado
Introduzido pela Versão 1

Descrição No contrato ChefIncentivesController, a função setScheduleRewardsPerSecond() irá iterar emissionSchedule para localizar o cronograma alvo com o maior índice que já foi iniciado, e atualizar a taxa de recompensa adequadamente. No entanto, nesse caso, alguns cronogramas de emissão podem ser ignorados.

function setScheduledRewardsPerSecond() internal {
		if (!persistRewardsPerSecond) {
			uint256 length = emissionSchedule.length;
			uint256 i = emissionScheduleIndex;
			uint128 offset = uint128(block.timestamp.sub(startTime));
			for (; i < length && offset >= emissionSchedule[i].startTimeOffset; i++) {}
			if (i > emissionScheduleIndex) {
				emissionScheduleIndex = i;
				_massUpdatePools();
				rewardsPerSecond = uint256(emissionSchedule[i - 1].rewardsPerSecond);
			}
		}
	}

Listagem 3.6: ChefIncentivesController.sol

Impacto Se a função setScheduledRewardsPerSecond() não for invocada por um longo período, algumas recompensas prometidas podem não ser distribuídas aos usuários.

Sugestão A função setScheduledRewardsPerSecond() é invocada dentro da função claim() e _handleActionAfterForToken(), portanto, a única maneira de o cronograma de emissões ser ignorado seria nenhuma pessoa interagir com o protocolo durante uma época de emissões.

3.2.5 Problema Potencial 6: Taxa de Câmbio Alterável durante a Migração

Item Descrição
Severidade Médio
Status Corrigido na Versão 5
Introduzido pela Versão 1

Descrição O contrato Migration é implementado para que os usuários troquem do tokenV1 para o tokenV2 com uma exchangeRate especificada. No entanto, durante o processo de migração, esta exchangeRate ainda pode ser ajustada pelo proprietário através da função setExchangeRate().

/**
    * @notice Migrate from V1 to V2
    * @param amount of V1 token
    */
   function exchange(uint256 amount) external whenNotPaused {
       uint256 v1Decimals = tokenV1.decimals();
       uint256 v2Decimals = tokenV2.decimals();

       uint256 outAmount = amount.mul(1e4).div(exchangeRate).mul(10**v2Decimals).div(10**v1Decimals);
       tokenV1.safeTransferFrom(_msgSender(), address(this), amount);
       tokenV2.safeTransfer(_msgSender(), outAmount);

       emit Migrate(_msgSender(), amount, outAmount);
   }

Listagem 3.7: Migration.sol

Impacto Será injusto para os outros usuários se a exchangeRate for alterada durante o processo de migração.

Sugestão Uma vez que a migração começa, a exchangeRate deve ser fixada.

3.2.6 Problema Potencial 7: Implementação Incorreta de _transfer() (I)

Item Descrição
Severidade Alto
Status Corrigido na Versão 7
Introduzido pela Versão 1

Descrição No contrato IncentivizedERC20, a função _transfer() não considera a situação em que o remetente e o destinatário podem ser a mesma conta (a chamada autotransferência). Especificamente, se o remetente for igual ao destinatário, o saldo do remetente será sobrescrito ao atualizar o saldo do destinatário. Nesse caso, o hacker é capaz de aumentar seu próprio saldo infinitamente ao transferir para a sua própria conta repetidamente.

function _transfer(
        address sender,
        address recipient,
        uint256 amount
      ) internal virtual {
        require(sender != address(0), 'ERC20: transfer from the zero address');
        require(recipient != address(0), 'ERC20: transfer to the zero address');
    
        _beforeTokenTransfer(sender, recipient, amount);
    
        uint256 senderBalance = _balances[sender].sub(amount, 'ERC20: transfer amount exceeds balance');
        uint256 recipientBalance = _balances[recipient].add(amount);
    
        if (address(_getIncentivesController()) != address(0)) {
          // uint256 currentTotalSupply = _totalSupply;
          _getIncentivesController().handleActionBefore(sender);
          if (sender != recipient) {
            _getIncentivesController().handleActionBefore(recipient);
          }
        }
    
        _balances[sender] = senderBalance;
        _balances[recipient] = recipientBalance;
    
        if (address(_getIncentivesController()) != address(0)) {
          uint256 currentTotalSupply = _totalSupply;
          _getIncentivesController().handleActionAfter(sender, senderBalance, currentTotalSupply);
          if (sender != recipient) {
            _getIncentivesController().handleActionAfter(recipient, recipientBalance, currentTotalSupply);
          }
        }
      }

Listagem 3.8: IncentivizedERC20.sol

Impacto Tokens podem ser cunhados infinitamente.

Sugestão Implementar a função _transfer() corretamente. Por exemplo, a implementação padrão de _transfer() do ERC20 no OpenZeppelin.

_balances[sender] = _balances[sender].sub(amount, 'ERC20: transfer amount exceeds balance');
_balances[recipient] = _balances[recipient].add(amount);

Listagem 3.9: ERC20.sol no OpenZeppelin

3.2.7 Problema Potencial 8: Falta de Verificação do Período em UniV2TwapOracle

Item Descrição
Severidade Baixo
Status Corrigido na Versão 9
Introduzido pela Versão 1

Descrição No contrato UniV2TwapOracle, o atributo _period não é validado nas funções initialize() e setPeriod().

function initialize(
        address _pair,
        address _rdnt,
        address _ethChainlinkFeed,
        uint _period,
        uint _consultLeniency,
        bool _allowStaleConsults
    ) external initializer {
        __Ownable_init();

        pair = IUniswapV2Pair(_pair);
        token0 = pair.token0();
        token1 = pair.token1();
        price0CumulativeLast = pair.price0CumulativeLast(); // Fetch the current accumulated price value (1 / 0)
        price1CumulativeLast = pair.price1CumulativeLast(); // Fetch the current accumulated price value (0 / 1)
        uint112 reserve0;
        uint112 reserve1;
        (reserve0, reserve1, blockTimestampLast) = pair.getReserves();
        require(reserve0 != 0 && reserve1 != 0, 'UniswapPairOracle: NO_RESERVES'); // Ensure that there's liquidity in the pair

        PERIOD = _period;
        CONSULT_LENIENCY = _consultLeniency;
        ALLOW_STALE_CONSULTS = _allowStaleConsults;

        baseInitialize(_rdnt, _ethChainlinkFeed);
    }

    function setPeriod(uint _period) external onlyOwner {
        PERIOD = _period;
    }

Listagem 3.10: UniV2TwapOracle.sol

Impacto Nesse caso, o oráculo pode retornar um valor inesperado se o _period for muito pequeno.

Sugestão Definir um limite mínimo para _period nas funções initialize e setPeriod.

3.2.8 Problema Potencial 9: Tokens Residuais Não Reembolsáveis

Item Descrição
Severidade Médio
Status Corrigido na Versão 5
Introduzido pela Versão 1

Descrição No contrato UniswapPoolHelper, a função zapWETH() foi projetada para ajudar o usuário a converter tokens WETH em tokens LP. Ela invocará a função addLiquidityWETHOnly() para adicionar liquidez no pool para tokens LP. Nesse processo, podem existir tokens residuais que deveriam ser devolvidos aos usuários. No entanto, o UniswapPoolHelper não implementa tal funcionalidade para lidar com esses tokens residuais.

function zapWETH(uint256 amount)
    public
    returns (uint256 liquidity)
{
    IWETH WETH = IWETH(wethAddr);
    WETH.transferFrom(msg.sender, address(liquidityZap), amount);
    liquidity = liquidityZap.addLiquidityWETHOnly(amount, address(this));
    IERC20 lp = IERC20(lpTokenAddr);
    
    liquidity = lp.balanceOf(address(this));
    lp.safeTransfer(msg.sender, liquidity);
}

Listagem 3.11: UniswapPoolHelper.sol

Impacto Os tokens residuais permanecerão no contrato, podendo ser extraídos por outros através da função zapTokens(0,0).

Sugestão Implementar a função para retornar os tokens residuais após adicionar liquidez.

3.2.9 Problema Potencial 10: Implementação Incorreta de _transfer() (II)

Item Descrição
Severidade Médio
Status Corrigido na Versão 9
Introduzido pela Versão 7

Descrição No contrato IncentivizedERC20, a função _transfer() invocará a função handle_ActionAfter() para atualizar o status do usuário no contrato ChefIncentivesController adequadamente. No entanto, o parâmetro senderBalance não será atualizado se o remetente for igual ao destinatário, o que está incorreto.

function _transfer(
        address sender,
        address recipient,
        uint256 amount
      ) internal virtual {
        require(sender != address(0), 'ERC20: transfer from the zero address');
        require(recipient != address(0), 'ERC20: transfer to the zero address');
    
        _beforeTokenTransfer(sender, recipient, amount);
    
        uint256 senderBalance = _balances[sender].sub(amount, 'ERC20: transfer amount exceeds balance');
    
        if (address(_getIncentivesController()) != address(0)) {
          // uint256 currentTotalSupply = _totalSupply;
          _getIncentivesController().handleActionBefore(sender);
          if (sender != recipient) {
            _getIncentivesController().handleActionBefore(recipient);
          }
        }
    
        _balances[sender] = senderBalance;
        uint256 recipientBalance = _balances[recipient].add(amount);
        _balances[recipient] = recipientBalance;
    
        if (address(_getIncentivesController()) != address(0)) {
          uint256 currentTotalSupply = _totalSupply;
          _getIncentivesController().handleActionAfter(sender, senderBalance, currentTotalSupply);
          if (sender != recipient) {
            _getIncentivesController().handleActionAfter(recipient, recipientBalance, currentTotalSupply);
          }
        }
      }

Listagem 3.12: IncentivizedERC20.sol

Impacto Quando os usuários transferem para si mesmos, seu estado no contrato ChefIncentivesController não será atualizado corretamente, o que trará problemas adicionais para as recompensas.

Sugestão Corrigir o senderBalance na função handleActionAfter().

3.2.10 Problema Potencial 11: Recompensas de Compound Manipuláveis

Item Descrição
Severidade Médio
Status Corrigido na Versão 10
Introduzido pela Versão 5

Descrição No contrato MFDPlus, a função _convertPendingRewardsToWeth() troca as recompensas do usuário por WETH através do roteador Uniswap para rebloqueio. No entanto, não há verificação de slippage após a troca.

IERC20(underlying).safeApprove(uniRouter, removedAmount);
    uint256[] memory amounts = IUniswapV2Router02(uniRouter)
    .swapExactTokensForTokens(
         removedAmount,
         0, // slippage handled after this function
         mfdHelper.getRewardToBaseRoute(underlying),
         address(this),
         block.timestamp + 10
     );

Listagem 3.13: MFDPlus.sol

Impacto O atacante pode realizar um ataque de front-run para manipular o preço e obter lucro.

Sugestão Adicionar a verificação de slippage na função claimCompound().

3.2.11 Problema Potencial 12: Falta de Controle de Acesso em setLeverager()

Item Descrição
Severidade Médio
Status Corrigido na Versão 9
Introduzido pela Versão 1

Descrição A função setLeverager() no contrato LendingPool não possui controle de acesso.

uint256[] memory amounts = IUniswapV2Router02(uniRouter)
    .swapExactTokensForTokens(
         removedAmount,
         0, // slippage handled after this function
         mfdHelper.getRewardToBaseRoute(underlying),
         address(this),
         block.timestamp + 10
     );

Listagem 3.14: LendingPool.sol

Impacto Se o leverager não fosse definido no início, um atacante poderia definir o leverager para qualquer endereço, obtendo assim controle sobre a lógica da função depositWithAutoDLP().

Sugestão Definir o leverager na função initialize() ou adicionar controle de acesso para a função setLeverager().

3.2.12 Problema Potencial 13: Nenhuma Verificação de Slippage em addLiquidityWETHOnly()

Item Descrição
Severidade Médio
Status Confirmado
Introduzido pela Versão 1

Descrição O usuário pode usar tokens WETH emprestados (ou seus próprios tokens ETH) ou tokens RDNT em processo de vesting nos contratos MFD para obter tokens LP (ou seja, WETH-RDNT).

No entanto, ao adicionar a liquidez ao pool, o cálculo dos tokens necessários é baseado na quantidade de reservas no pool, que pode ser manipulada. Nesse caso, se o usuário tiver apenas tokens WETH, a função addLiquidityWETHOnly() será invocada para trocar metade dos tokens WETH por tokens RDNT no pool desequilibrado sem verificar o slippage.

function addLiquidityWETHOnly(uint256 _amount, address payable to)
    public
    returns (uint256 liquidity)
{
    require(to != address(0), "LiquidityZAP: Invalid address");
    uint256 buyAmount = _amount.div(2);
    require(buyAmount > 0, "LiquidityZAP: Insufficient ETH amount");

    (uint256 reserveWeth, uint256 reserveTokens) = getPairReserves();
    uint256 outTokens = UniswapV2Library.getAmountOut(
        buyAmount,
        reserveWeth,
        reserveTokens
    );

    _WETH.transfer(_tokenWETHPair, buyAmount);

    (address token0, address token1) = UniswapV2Library.sortTokens(
        address(_WETH),
        _token
    );
    IUniswapV2Pair(_tokenWETHPair).swap(
        _token == token0 ? outTokens : 0,
        _token == token1 ? outTokens : 0,
        address(this),
        ""
    );

    return _addLiquidity(outTokens, buyAmount, to);
}

Listagem 3.15: LiquidityZap.sol

function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
       require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
       require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
       uint amountInWithFee = amountIn.mul(997);
       uint numerator = amountInWithFee.mul(reserveOut);
       uint denominator = reserveIn.mul(1000).add(amountInWithFee);
       amountOut = numerator / denominator;
   }  

Listagem 3.16: UniswapV2Library.sol

Impacto O atacante pode realizar um ataque de front-run para manipular o preço e obter lucro.

Sugestão Verificar o slippage na função addLiquidityWETHOnly() ou garantir que ela só possa ser invocada pelo UniswapPoolHelper.

3.2.13 Problema Potencial 14: Falta de Verificação do borrowRatio em loopETH()

Item Descrição
Severidade Baixo
Status Corrigido na Versão 10
Introduzido pela Versão 1

Descrição A função loopETH() é usada para alavancagem de empréstimo e recebe um parâmetro borrowRatio para especificar a taxa de empréstimo. No entanto, o borrowRatio não é verificado antes do início do loop.

function loopETH(
        uint256 interestRateMode,
        uint256 borrowRatio,
        uint256 loopCount
    ) external payable {
        uint16 referralCode = 0;
        uint256 amount = msg.value;
        if (IERC20(address(weth)).allowance(address(this), address(lendingPool)) == 0) {
            IERC20(address(weth)).safeApprove(address(lendingPool), type(uint256).max);
        }
        if (IERC20(address(weth)).allowance(address(this), address(treasury)) == 0) {
            IERC20(address(weth)).safeApprove(treasury, type(uint256).max);
        }

        uint256 fee = amount.mul(feePercent).div(RATIO_DIVISOR);
        _safeTransferETH(treasury, fee);
        
        amount = amount.sub(fee);

        weth.deposit{value: amount}();
        lendingPool.deposit(address(weth), amount, msg.sender, referralCode);

        for (uint256 i = 0; i < loopCount; i += 1) {
            amount = amount.mul(borrowRatio).div(RATIO_DIVISOR);
            lendingPool.borrow(address(weth), amount, interestRateMode, referralCode, msg.sender);
            weth.withdraw(amount);

            fee = amount.mul(feePercent).div(RATIO_DIVISOR);
            _safeTransferETH(treasury, fee);

            weth.deposit{value: amount.sub(fee)}();
            lendingPool.deposit(address(weth), amount.sub(fee), msg.sender, referralCode);
        }

        zapWETHWithBorrow(wethToZap(msg.sender), msg.sender);
    }

Listagem 3.17: Leverager.sol

Impacto O borrowRatio pode ser maior que RATIO_DIVISOR, o que é inconsistente com o design original.

Sugestão Garantir que o borrowRatio seja menor ou igual ao RATIO_DIVISOR.

3.2.14 Problema Potencial 15: Falta de Verificação do Comprimento entre assets e poolIDs em setPoolIDs()

Item Descrição
Severidade Baixo
Status Corrigido na Versão 10
Introduzido pela Versão 1

Descrição A função setPoolIDs() permite que o proprietário defina diferentes poolIDs para diferentes ativos. No entanto, os comprimentos desses dois arrays não são verificados para serem iguais.

// Set pool ids of assets
    function setPoolIDs(address[] memory assets, uint256[] memory poolIDs) external onlyOwner {
        for (uint256 i = 0; i < assets.length; i += 1) {
            poolIdPerChain[assets[i]] = poolIDs[i];
        }
        emit PoolIDsUpdated(assets, poolIDs);
    } 

Listagem 3.18: StarBorrow.sol

Impacto Os ativos não serão atribuídos aos poolIDs corretos.

Sugestão Garantir que os comprimentos de assets e poolIDs sejam iguais.

3.2.15 Problema Potencial 16: Falta de Revogação do Privilégio de mint em addBountyContract()

Item Descrição
Severidade Baixo
Status Confirmado
Introduzido pela Versão 1

Descrição A função addBountyContract() é usada para definir o novo BountyManager. No entanto, o contrato de bounty original ainda mantém o privilégio de mint, o que vai contra o design original.

function addBountyContract(address _bounty) external onlyOwner {
       BountyManager = _bounty;
       minters[_bounty] = true;
   }

Listagem 3.19: Leverager.sol

Impacto O BountyManager obsoleto ainda possui privilégios de mint.

Sugestão Revogar o privilégio de mint do contrato BountyManager original.

Feedback A função addBountyContract será chamada apenas uma vez para inicializar o BountyManager.

3.2.16 Problema Potencial 17: Minters Só Podem Ser Atribuídos Uma Vez

Item Descrição
Severidade Baixo
Status Confirmado
Introduzido pela Versão 1

Descrição Os minters são usados para registrar aqueles que têm permissão para acessar as funções mint() e addReward(). No entanto, quando um dos minters (por exemplo, o contrato ChefIncentivesController) é atualizado, os minters desatualizados não podem ser removidos.

function setMinters(address[] memory _minters) external onlyOwner {
        require(!mintersAreSet);
        for (uint256 i; i < _minters.length; i++) {
            minters[_minters[i]] = true;
        }
        mintersAreSet = true;
    }

Listagem 3.20: MultiFeeDistribution.sol

Impacto Os minters desatualizados não podem ser removidos quando são atualizados.

Sugestão Implementar uma função privilegiada para modificar os minters.

Feedback Como o BountyManager, ChefIncentivesController e MultiFeeDistribution serão atualizáveis, os minters sempre mantêm o mesmo endereço de proxy.

3.3 Recomendações Adicionais

3.3.1 Problema Potencial 18: Otimização de Gas (zapVestingToLp() em Mfd)

Item Descrição
Status Corrigido na Versão 10
Introduzido pela Versão 1

Descrição A função zapVestingToLp() só pode ser invocada pelo contrato LockZap para transferir os ganhos bloqueados do usuário. Ela itera o array de ganhos do usuário a partir do índice 0 e verifica se o unlockTime é maior que o timestamp atual. Se for, esse ganho será removido do array e transferido. No entanto, como o unlockTime no array aumenta com o índice, será mais eficiente iniciar a iteração do final do array para o início. Se o unlockTime for menor que o timestamp atual, o loop pode ser interrompido.

function zapVestingToLp(address _user)
        external
        override
        returns (uint256 zapped)
    {
        require(msg.sender == lockZap);

        LockedBalance[] storage earnings = userEarnings[_user];
        uint256 length = earnings.length;

        for (uint256 i = 0; i < length; ) {
            // only vesting, so only look at currently locked items
            if (earnings[i].unlockTime > block.timestamp) {
                zapped = zapped.add(earnings[i].amount);
                // remove + shift array size
                earnings[i] = earnings[earnings.length - 1];
                earnings.pop();
                length = length.sub(1);
            } else {
                i = i.add(1);
            }
        }

        rdntToken.safeTransfer(lockZap, zapped);

        Balances storage bal = balances[_user];
        bal.earned = bal.earned.sub(zapped);
        bal.total = bal.total.sub(zapped);

        return zapped;
    }

Listagem 3.21: MultiFeeDistribution.sol

Sugestão Iniciar a iteração do final de earnings para o início. Se o unlockTime for menor que o timestamp atual, o loop pode ser interrompido.

3.3.2 Problema Potencial 19: Reserva de Bounty Não Vazia em BountyManager

Item Descrição
Status Corrigido na Versão 10
Introduzido pela Versão 1

Descrição Na função _sendBounty(), se não houver tokens RDNT suficientes para a transferência no contrato BountyManager, o evento BountyReseveEmpty() será emitido e o contrato será pausado. No entanto, é possível que ainda haja alguns tokens RDNT restantes, o que é inconsistente com o evento emitido.

function _sendBounty(address _to, uint256 _amount)
		internal
		returns (uint256)
	{
		if (_amount == 0) {
			return 0;
		}

		uint256 bountyReserve = IERC20(rdnt).balanceOf(address(this));
		if(_amount > bountyReserve) {
			emit BountyReserveEmpty(bountyReserve);
			_pause();
		} else {
			IERC20(rdnt).safeTransfer(address(mfd), _amount);
			IMFDPlus(mfd).mint(_to, _amount, true);
			return _amount;
		}
	}

Listagem 3.22: BountyManager.sol

Sugestão Transferir os tokens RDNT restantes mesmo que não sejam suficientes.

3.3.3 Problema Potencial 20: Nomenclatura Inconsistente em requiredUsdValue()

Item Descrição
Status Confirmado
Introduzido pela Versão 1

Descrição A função requiredUsdValue() é usada para verificar o valor bloqueado exigido do usuário que deseja se qualificar para ganhar recompensas ao manter RTokens. O cálculo é baseado no valor de colateral do usuário, que é retornado pela função getUserAccountData(). No entanto, o valor retornado é nomeado como totalCollateralETH, o que é inconsistente com o da função requiredUsdValue() (ou seja, totalCollateralUSD).

Sugestão Padronizar as convenções de nomenclatura das funções com o nome correto do token. Por exemplo, renomear requiredUsdValue() para requiredEthValue().

Feedback Preferimos manter os contratos AAVE o mais semelhante possível, por isso não atualizamos o nome.

3.4 Notas

3.4.1 Problema Potencial 21: MFDPlus Obsoleto

Item Descrição
Status Confirmado
Introduzido pela versão 10

Descrição O contrato MFDPlus não é mais utilizado. A lógica de composição foi movida para o contrato AutoCompounder e outra lógica foi movida para o contrato MiddleFeeDistribution.

4. Apêndice

4.1 Resultados dos Testes de Segurança Estáticos Automatizados

Tabela 4.1: Resultados dos Testes de Segurança Estáticos Automatizados. Encontrado indica o número de problemas relatados pelas ferramentas. FP significa o número de falsos positivos após nossa verificação manual.

ID Detector Descrição Impacto Encontrado FP Resultado
1 arbitrary-send-erc20 Chamada de transferFrom com from arbitrário Alto 1 1 Aprovado
2 array-by-reference Modificando array de armazenamento por valor Alto 0 0 Aprovado
3 incorrect-shift Ordem incorreta de parâmetros em uma instrução de deslocamento Alto 0 0 Aprovado
4 multiple-constructors Múltiplos esquemas de construtor Alto 0 0 Aprovado
5 name-reused Reutilização do nome do contrato Alto 0 0 Aprovado
6 protected-vars Modificando variáveis diretamente sem controle de acesso Alto 0 0 Aprovado
7 rtlo Usando caractere de controle Right-To-Left-Override Alto 0 0 Aprovado
8 shadowing-state Sombreamento de variáveis de estado Alto 1 1 Aprovado
9 suicidal Funções que permitem que qualquer pessoa destrua o contrato Alto 0 0 Aprovado
10 uninitialized-state Variáveis de estado não inicializadas Alto 3 3 Aprovado
11 uninitialized-storage Variáveis de armazenamento não inicializadas Alto 0 0 Aprovado
12 unprotected-upgrade Contrato atualizável desprotegido Alto 1 1 Aprovado
13 arbitrary-send-erc20-permit transferFrom usa from arbitrário com permit Alto 0 0 Aprovado
14 arbitrary-send-eth Funções que enviam Ether para destinos arbitrários Alto 0 0 Aprovado
15 controlled-array-length Atribuição de comprimento de array contaminado Alto 0 0 Aprovado
16 controlled-delegatecall Destino de delegatecall controlado Alto 0 0 Aprovado
17 delegatecall-loop Funções pagáveis usando delegatecall dentro de um loop Alto 0 0 Aprovado
18 msg-value-loop Usando msg.value dentro de um loop Alto 0 0 Aprovado
19 reentrancy-eth Vulnerabilidades de reentrância (roubo de ethers) Alto 5 5 Aprovado
20 storage-array Bug do compilador em array de inteiros de armazenamento assinados Alto 0 0 Aprovado
21 unchecked-transfer Transferência de tokens não verificada Alto 12 12 Aprovado
22 weak-prng PRNG fraco Alto 0 0 Aprovado
23 domain-separator-collision Detecta tokens ERC20 que possuem uma função cuja assinatura colide com DOMAIN_SEPARATOR() do EIP-2612 Médio 0 0 Aprovado
24 enum-conversion Detecta conversão perigosa de enum Médio 0 0 Aprovado
25 erc20-interface Interfaces ERC20 incorretas Médio 0 0 Aprovado
26 erc721-interface Interfaces ERC721 incorretas Médio 0 0 Aprovado
27 incorrect-equality Igualdades estritas perigosas Médio 23 23 Aprovado
28 locked-ether Contratos que bloqueiam ether Médio 1 1 Aprovado
29 mapping-deletion Exclusão em mapeamento contendo uma estrutura Médio 0 0 Aprovado
30 shadowing-abstract Sombreamento de variáveis de estado de contratos abstratos Médio 0 0 Aprovado
31 tautology Tautologia ou contradição Médio 0 0 Aprovado
32 write-after-write Escrita não utilizada Médio 3 3 Aprovado
33 boolean-cst Uso incorreto de constante booleana Médio 0 0 Aprovado
34 constant-function-asm Funções constantes usando código assembly Médio 0 0 Aprovado
35 constant-function-state Funções constantes alterando o estado Médio 0 0 Aprovado
36 divide-before-multiply Ordem imprecisa de operações aritméticas Médio 20 20 Aprovado
37 reentrancy-no-eth Vulnerabilidades de reentrância (sem roubo de ethers) Médio 12 12 Aprovado
38 reused-constructor Construtor base reutilizado Médio 0 0 Aprovado
39 tx-origin Uso perigoso de tx.origin Médio 1 1 Aprovado
40 unchecked-lowlevel Chamadas de baixo nível não verificadas Médio 0 0 Aprovado
41 unchecked-send Send não verificado Médio 0 0 Aprovado
42 uninitialized-local Variáveis locais não inicializadas Médio 33 33 Aprovado
43 unused-return Valores de retorno não utilizados Médio 19 19 Aprovado

4.2 Resultados dos Testes de Segurança Dinâmicos Automatizados

Tabela 4.2: Propriedades Testadas para Lógica Relacionada ao Empréstimo

ID Propriedade Resultado
1 Chamar deposit nunca leva a uma diminuição da quantidade de RToken de onBehalfOf Aprovado
2 Chamar withdraw nunca leva a um aumento da quantidade de RToken de msg.sender Aprovado
3 Chamar borrow com modo de taxa de juros estável nunca leva a uma diminuição do StableDebtToken de onBehalfOf. Aprovado
4 Chamar borrow com modo de taxa de juros variável nunca leva a uma diminuição do VariableDebtToken de onBehalfOf. Aprovado
5 Chamar borrow com onBehalfOf diferente de msg.sender nunca leva a um aumento da permissão de empréstimo de msg.sender. Aprovado
6 Chamar repay com modo de taxa de juros estável nunca leva a um aumento do StableDebtToken de onBehalfOf. Aprovado
7 Chamar repay com modo de taxa de juros variável nunca leva a um aumento do VariableDebtToken de onBehalfOf. Aprovado
8 liquidityIndex nunca diminuirá. Aprovado
9 liquidityIndex permanecerá constante dentro do mesmo bloco. Aprovado
10 variableBorrowIndex nunca diminuirá. Aprovado
11 variableBorrowIndex permanecerá constante dentro do mesmo bloco. Aprovado
12 Diminuir os valores de colateral nunca levará a um fator de saúde menor que 1. Aprovado
13 Aumentar os valores de empréstimo nunca levará a um fator de saúde menor que 1. Aprovado

Tabela 4.3: Propriedades Testadas para Lógica Relacionada ao Staking

ID Propriedade Resultado
1 O saldo total do usuário sempre é igual à soma do saldo bloqueado, saldo desbloqueado e saldo ganho. Aprovado
2 O saldo bloqueado do usuário sempre é igual à soma do valor de userLocks Aprovado
3 O saldo lockedWithMultiplier do usuário sempre é igual à soma do valor de userLocks vezes o multiplicador de userLocks Aprovado
4 lockedSupply sempre é igual à soma do saldo bloqueado dos usuários Aprovado
5 lockedSupplyWithMultiplier sempre é igual à soma do saldo lockedWithMultiplier dos usuários Aprovado
6 rewardPerTokenStored nunca diminui. Aprovado
7 rewardPerTokenStored permanecerá constante dentro do mesmo bloco. Aprovado
8 totalSupply sempre é igual à soma dos valores dos usuários Aprovado
9 accRewardPerShare nunca diminui. Aprovado
10 accRewardPerShare permanecerá constante dentro do mesmo bloco. Aprovado

Tabela 4.4: Propriedades Testadas para Outros Recursos

ID Propriedade Resultado
1 O saldo de WETH e RDNT do contrato LockedZap sempre será zero. Aprovado
2 O saldo de WETH e RDNT do contrato LiquidityZap sempre será zero. Aprovado
3 O saldo de WETH e RDNT do contrato BalancerPoolHelper sempre será zero. Aprovado
4 O saldo de WETH e RDNT do contrato UniswapPoolHelper sempre será zero. Aprovado
5 Chamar loop sempre levará o usuário a ser elegível para recompensas Aprovado
6 Chamar loopETH sempre levará o usuário a ser elegível para recompensas Aprovado
7 Chamar executeBounty com _execute igual a false nunca levará a mudança de armazenamento. Aprovado
8 Chamar transfer com remetente igual ao receptor nunca leva a mudança de saldo. Falhou na Versão 1. Aprovado na Versão 7

5 Avisos e Observações

5.1 Isenção de Responsabilidade

Este relatório não constitui conselho de investimento ou uma recomendação pessoal. Ele não considera, e não deve ser interpretado como considerando ou tendo qualquer relação com, a economia potencial de um token, venda de token ou qualquer outro produto, serviço ou outro ativo. Qualquer entidade não deve confiar neste relatório de nenhuma forma, inclusive para fins de tomar quaisquer decisões de compra ou venda de qualquer token, produto, serviço ou outro ativo.

Este relatório não é um endosso de nenhum projeto ou equipe específica, e o relatório não garante a segurança de nenhum projeto específico. Este teste de segurança não fornece nenhuma garantia de descoberta de todos os problemas de segurança dos contratos inteligentes, ou seja, o resultado da avaliação não garante a inexistência de quaisquer outras descobertas de problemas de segurança. Como os testes de segurança não podem ser considerados abrangentes, sempre recomendamos prosseguir com auditorias independentes e um programa público de recompensa por bugs para garantir a segurança dos contratos inteligentes.

O escopo deste teste de segurança é limitado ao código mencionado na Seção 1.2. A menos que especificado explicitamente, a segurança da linguagem em si (por exemplo, a linguagem Solidity), a cadeia de ferramentas de compilação subjacente e a infraestrutura de computação estão fora do escopo.

5.2 Procedimento de Auditoria

Realizamos a auditoria de acordo com o seguinte procedimento.

  • Detecção de Vulnerabilidades Primeiro escaneamos os contratos inteligentes com analisadores de código automáticos e, em seguida, verificamos manualmente (rejeitamos ou confirmamos) os problemas relatados por eles.

  • Análise Semântica Estudamos a lógica de negócios dos contratos inteligentes e conduzimos investigações adicionais sobre as possíveis vulnerabilidades usando uma ferramenta automática de fuzzing (desenvolvida por nossa equipe de pesquisa). Também analisamos manualmente possíveis cenários de ataque com auditores independentes para verificar cruzadamente os resultados.

  • Recomendação Fornecemos alguns conselhos úteis aos desenvolvedores sob a perspectiva de boas práticas de programação, incluindo otimização de gas, estilo de código, entre outros.

Apresentamos os principais pontos de verificação concretos a seguir.

5.2.1 Segurança de Software

  • Reentrância

  • DoS

  • Controle de acesso

  • Manipulação de dados e fluxo de dados

  • Tratamento de exceções

  • Chamadas externas não confiáveis e fluxo de controle

  • Consistência de inicialização

  • Operações de eventos

  • Aleatoriedade propensa a erros

  • Uso incorreto do sistema de proxy

5.2.2 Segurança DeFi

  • Consistência semântica

  • Consistência de funcionalidade

  • Gerenciamento de permissões

  • Lógica de negócios

  • Operação de tokens

  • Mecanismo de emergência

  • Segurança de oráculos

  • Lista de permissões e lista de bloqueios

  • Impacto econômico

  • Transferência em lote

5.2.3 Segurança de NFT

  • Item duplicado

  • Verificação do receptor do token

  • Segurança de metadados off-chain

5.2.4 Recomendações Adicionais

  • Otimização de gas

  • Qualidade e estilo de código

Nota: Os pontos de verificação anteriores são os principais. Podemos usar mais pontos de verificação durante o processo de auditoria de acordo com a funcionalidade do projeto.

Best Security Auditor for Web3

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

BlockSec Audit