Back to Blog

Informe de Pruebas de Seguridad para Radiant V2

Code Auditing
March 23, 2023
37 min read

Manifiesto del Informe

Elemento Descripción
Cliente Radiant Capital
Objetivo Radiant V2

Historial de Versiones

Versión Fecha Descripción
1.0 15 de marzo de 2023 Primera Versión
2.0 21 de marzo de 2023 Segunda Versión

1. Introducción

1.1 Acerca de las Pruebas de Seguridad

Fuimos invitados por Radiant Capital para realizar pruebas de seguridad (como el equipo rojo) de los contratos inteligentes de Radiant V2 con el fin de identificar riesgos potenciales. Como equipo responsable, Radiant Capital toma la seguridad en serio. Por ello, el equipo decidió invertir más esfuerzos en asegurar esos contratos inteligentes, aunque ya fueron auditados por múltiples empresas de seguridad ^1.

Cabe señalar que las pruebas de seguridad difieren de la auditoría de seguridad tanto en objetivos como en requisitos. Específicamente, las pruebas de seguridad tienen como objetivo descubrir puntos vulnerables adicionales/inusuales mediante la imitación de atacantes para comprometer el programa/protocolo, mientras que la auditoría de seguridad busca ofrecer una verificación de seguridad relativamente exhaustiva enumerando las posibles superficies de ataque. Por tanto, las pruebas de seguridad podrían no cubrir algunos errores de lógica compleja que podrían identificarse en una auditoría de seguridad debido al tiempo y los recursos limitados.

1.2 Acerca de los Contratos Objetivo

Información Descripción
Tipo Contrato Inteligente
Lenguaje Solidity
Enfoque Análisis estático, análisis dinámico, verificación semiautomática y manual

El repositorio objetivo es Radiant_v2.1.1. Los valores SHA de los commits durante las pruebas de seguridad se muestran a continuación. Nuestro informe es responsable únicamente de la versión inicial (es decir, la Versión 1), así como de los nuevos códigos para corregir los problemas indicados en el informe.

Tenga en cuenta que este informe solo cubre los contratos inteligentes bajo la carpeta radiant_v2.1.1/contracts de este repositorio, incluyendo:

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

Después de la actualización en la Versión 8, los archivos cubiertos en estas pruebas de seguridad incluyen:

  • 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 Seguridad

Para evaluar el riesgo, seguimos los estándares o sugerencias ampliamente adoptados tanto por la industria como por la academia, incluyendo la Metodología de Calificación de Riesgos de OWASP ^2 y la Enumeración de Debilidades Comunes ^3. La severidad general del riesgo está determinada por la probabilidad y el impacto. Específicamente, la probabilidad se utiliza para estimar cuán probable es que una vulnerabilidad particular pueda ser descubierta y explotada por un atacante, mientras que el impacto se utiliza para medir las consecuencias de una explotación exitosa.

En este informe, tanto la probabilidad como el impacto se clasifican en dos niveles, es decir, alto y bajo respectivamente, y sus combinaciones se muestran en la Tabla 1.1.

En consecuencia, la severidad medida en este informe se clasifica en tres categorías: Alta, Media, Baja. En aras de la exhaustividad, Indeterminado también se utiliza para cubrir circunstancias en las que el riesgo no puede determinarse con precisión.

Además, el estado de un elemento descubierto se ubicará en una de las siguientes cuatro categorías:

  • Indeterminado Aún sin respuesta.

  • Reconocido El elemento ha sido recibido por el cliente, pero aún no confirmado.

  • Confirmado El elemento ha sido reconocido por el cliente, pero aún no corregido.

  • Corregido El elemento ha sido confirmado y corregido por el cliente.

2. Pruebas de Seguridad Automatizadas

2.1 Pruebas de Seguridad Estáticas Automatizadas

Utilizamos nuestra herramienta de análisis estático interna basada en Slither para verificar la existencia de vulnerabilidades. Tras revisar los resultados manualmente, no se encontraron problemas. Los resultados detallados de las pruebas se pueden encontrar en la Tabla 4.1 en el Apéndice.

2.2 Pruebas de Seguridad Dinámicas Automatizadas

Utilizamos técnicas de fuzzing para probar la robustez, confiabilidad y precisión de los contratos objetivo. Específicamente, la semilla inicial en el proceso de fuzzing se determina en función de la semántica de las funciones y los scripts de prueba del contrato. Para simular el entorno en cadena, también mantenemos un conjunto de direcciones que han interactuado con los contratos LendingPool y MultiFeeDistribution.

Nuestro fuzzer también considera la semántica de las funciones durante la generación de secuencias de transacciones. Por ejemplo, es probable que la función stake del contrato MultiFeeDistribution y la función deposit del contrato LendingPool sean invocadas primero en la secuencia. La mutación de los parámetros de función y la secuencia está guiada por la cobertura del código del contrato. Si un determinado parámetro o secuencia alcanza una mayor cobertura de código, tendrá mayor prioridad para ser mutado en la siguiente ronda de fuzzing. Para explorar algunos caminos restringidos por números mágicos, recopilamos los valores leídos del almacenamiento (es decir, la instrucción SLOAD) en tiempo de ejecución y los usamos para generar parámetros de función durante el proceso de mutación.

En total, generamos 100,000 casos de prueba y utilizamos 31 oráculos, que se usan para detectar si ha ocurrido un fallo. Para cada caso de prueba, contiene 30 transacciones con órdenes especificados. Finalmente, descubrimos un problema crítico (es decir, la Sección 3.2.6), que también se descubrió en nuestro proceso de pruebas de seguridad manual. Los resultados detallados de las pruebas se pueden encontrar en las Tablas 4.2, 4.3 y 4.4 en el Apéndice.

3. Pruebas de Seguridad Manuales

Involucramos esfuerzos manuales para comprender el diseño general y las interacciones entre los diferentes módulos, y luego realizamos las pruebas de seguridad basándonos en nuestro conocimiento de las posibles superficies de ataque derivadas de nuestra investigación y experiencia previas.

En total, encontramos diecisiete problemas potenciales. Además, tenemos tres recomendaciones y una nota como se indica a continuación:

  • Riesgo Alto: 2

  • Riesgo Medio: 8

  • Riesgo Bajo: 7

  • Recomendaciones: 3

  • Notas: 1

ID Severidad Descripción Categoría Estado
1 Media Sin Interfaz Reservada para Restablecer Punteros de Función Seguridad de Software Corregido
2 Media Cálculo Incorrecto del Oráculo Seguridad DeFi Corregido
3 Alta Posible Drenaje de Fondos a través de BaseBounty Seguridad DeFi Corregido
4 Baja Posibles Calendarios de Emisión Inválidos Seguridad DeFi Corregido
5 Baja Calendarios de Emisión Omitibles Seguridad DeFi Confirmado
6 Media Tasa de Cambio Modificable durante la Migración Seguridad DeFi Corregido
7 Alta Implementación Incorrecta de _transfer() (I) Seguridad DeFi Corregido
8 Baja Falta de Verificación del Período en UniV2TwapOracle Seguridad DeFi Corregido
9 Media Tokens de Polvo No Reembolsables Seguridad DeFi Corregido
10 Media Implementación Incorrecta de _transfer() (II) Seguridad DeFi Corregido
11 Media Recompensas de Compuesto Manipulables Seguridad DeFi Corregido
12 Media Falta de Control de Acceso en setLeverager() Seguridad DeFi Corregido
13 Media Sin Verificación de Deslizamiento en addLiquidityWETHOnly() Seguridad DeFi Confirmado
14 Baja Falta de Verificación de borrowRatio en loopETH() Seguridad DeFi Corregido
15 Baja Falta de Verificación de Longitud entre assets y poolIDs en setPoolIDs() Seguridad DeFi Corregido
16 Baja Falta de Revocación del Privilegio de mint en addBountyContract() Seguridad DeFi Confirmado
17 Baja Los Minters Solo Pueden Asignarse Una Vez Seguridad DeFi Confirmado
18 - Optimización de Gas (zapVestingToLp() en Mfd) Recomendación Corregido
19 - Reserva de Bounty No Vacía en BountyManager Recomendación Corregido
20 - Nomenclatura Inconsistente en requiredUsdValue() Recomendación Confirmado
21 - Nota sobre MFDPlus Obsoleto Nota Confirmado

Los detalles se proporcionan en las siguientes secciones.

3.1 Seguridad de Software

3.1.1 Problema Potencial 1: Sin Interfaz Reservada para Restablecer Punteros de Función

Elemento Descripción
Severidad Media
Estado Corregido en la Versión 7
Introducido por Versión 1

Descripción Tres funciones, getLpMfdBounty(), getChefBounty() y getAutoCompoundBounty(), son invocadas a través de punteros de función en el contrato BountyManager. Mientras tanto, la herencia de OwnableUpgradable muestra que este contrato sería la implementación de un proxy. Esto indica que el contrato de implementación puede actualizarse en el futuro, lo que genera un problema relacionado con los punteros de función.

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();
    } 

Listado 3.1: BountyManager.sol

Impacto Cuando los desplazamientos de las tres funciones mencionadas anteriormente cambian, los punteros de función no pueden funcionar como se espera y toda la lógica del contrato puede verse alterada.

Sugerencia El contrato debería proporcionar interfaces para restablecer los punteros de función.

3.2 Seguridad DeFi

3.2.1 Problema Potencial 2: Cálculo Incorrecto del Oráculo

Elemento Descripción
Severidad Media
Estado Corregido en la Versión 11
Introducido por Versión 1 y Versión 4

Descripción La función consult() en el contrato ComboOracle se utiliza para calcular el precio promedio de varias fuentes. En la implementación de la versión 1, utiliza la media aritmética para calcular el precio final, que puede ser manipulado influyendo en uno de los oráculos fuente.

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);
    }

Listado 3.2: ComboOracle.sol

En la implementación de la versión 4, cuando el precio promedio es mayor que el precio más bajo×1.025, se devolverá el precio más bajo. Sin embargo, el valor de retorno aún puede manipularse si el resultado devuelto por uno de los oráculos fuente es anormalmente bajo.

/**
    * @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;
   }

Listado 3.3: ComboOracle.sol

Impacto El precio devuelto por ComboOracle puede ser manipulado, lo que permite al atacante obtener beneficios de ello.

Sugerencia Sugerimos usar el valor medio en lugar del valor promedio. Si solo hay dos oráculos fuente y se produce una diferencia bastante grande, es más razonable revertir la transacción cuando el precio promedio es considerablemente mayor que el precio más bajo.

Comentario Solo habrá dos oráculos fuente. Si se produce una diferencia bastante grande, usaremos un OZ Defender Sentinel para pausar los contratos asociados.

Nota El contrato ComboOracle ha sido eliminado y ya no se utiliza.

3.2.2 Problema Potencial 3: Posible Drenaje de Fondos a través de BaseBounty

Elemento Descripción
Severidad Alta
Estado Corregido en la Versión 4
Introducido por Versión 1

Descripción Un usuario puede bloquear tokens (es decir, RDNT) por una duración fija para ganar recompensas. Cuando el bloqueo expira, otros usuarios pueden invocar la función executeBounty() para re-bloquear los tokens de este usuario y ganar el BaseBounty si este usuario tiene habilitado el AutoRelock. Durante el proceso de re-bloqueo, los bloqueos expirados se limpiarán y se vuelven a apostar en el pool mediante la función interna _cleanWithdrawableLocks(). Sin embargo, existe una variable maxLockWithdrawPerTxn que limita el número máximo de bloqueos que pueden limpiarse. En este caso, los bloqueos expirados no limpiados aún pueden existir incluso después de que la función executeBounty() haya sido ejecutada. Esto puede eludir la verificación en la línea 106 de la función claimBounty() en el contrato MFDPlus. El issueBaseBounty se establecerá como verdadero y se devolverá.

**
    * @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);
           }
       }
   }

Listado 3.4: MultiFeeDistribution.sol

Específicamente, el atacante puede apostar 1 wei token con el mismo tiempo de expiración múltiples veces, lo cual es considerablemente mayor que maxLockWithdrawPerTxn. Después de eso, el atacante puede establecer la acción como getLpMfdBounty e invocar executeBounty() repetidamente. Como la cantidad de bloqueos limpiados está limitada por el maxLockWithdrawPerTxn, el BaseBounty en el contrato BountyManager puede ser drenado por el atacante.

Impacto El atacante puede drenar todos los fondos del contrato BountyManager en una sola transacción, lo que lleva a la interrupción de los mecanismos de bounty diseñados.

Sugerencia Asegurarse de que la función _cleanWithdrawableLocks() pueda limpiar todos los bloqueos expirados y establecer un monto mínimo de apuesta en la función _stake().

3.2.3 Problema Potencial 4: Posibles Calendarios de Emisión Inválidos

Elemento Descripción
Severidad Baja
Estado Corregido en la Versión 10
Introducido por Versión 1

Descripción En el contrato ChefIncentivesController, la función setEmissionSchedule() es invocada por el propietario para establecer calendarios para diferentes tasas de recompensa. En este caso, el tiempo de inicio para cada calendario (_startTimeOffsets[i] + startTime) debería validarse para ser mayor que el timestamp actual. Sin embargo, solo verifica el primer elemento en _startTimeOffsets, lo que no es suficiente. Además, el _startTimeOffsets[i] se convierte de uint256 a uint128 cuando se agrega a emissionSchedule, lo que puede truncarse si la entrada original es demasiado 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);
    } 

Listado 3.5: ChefIncentivesController.sol

Impacto Si _startTimeOffsets no está en orden ascendente, algunas recompensas prometidas no se distribuirán a los usuarios. Si _startTimeOffsets[i] está fuera del rango de uint128, se añadirá un calendario de emisión inválido.

Sugerencia Asegurarse de que _startTimeOffsets esté en orden ascendente y que todos los elementos estén dentro del rango de uint128.

3.2.4 Problema Potencial 5: Calendarios de Emisión Omitibles

Elemento Descripción
Severidad Baja
Estado Confirmado
Introducido por Versión 1

Descripción En el contrato ChefIncentivesController, la función setScheduleRewardsPerSecond() iterará emissionSchedule para localizar el calendario objetivo con el índice más grande que ya ha comenzado, y actualizará la tasa de recompensa en consecuencia. Sin embargo, en este caso, algunos calendarios de emisión pueden omitirse.

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);
			}
		}
	}

Listado 3.6: ChefIncentivesController.sol

Impacto Si la función setScheduledRewardsPerSecond() no se invoca durante mucho tiempo, algunas recompensas prometidas podrían no distribuirse a los usuarios.

Sugerencia La función setScheduledRewardsPerSecond() se invoca dentro de la función claim() y _handleActionAfterForToken(), por lo que la única manera en que el calendario de emisiones sería omitido sería si ninguna persona interactúa con el protocolo durante una época de emisiones.

3.2.5 Problema Potencial 6: Tasa de Cambio Modificable durante la Migración

Elemento Descripción
Severidad Media
Estado Corregido en la Versión 5
Introducido por Versión 1

Descripción El contrato Migration está implementado para que los usuarios intercambien desde tokenV1 a tokenV2 con un exchangeRate especificado. Sin embargo, durante el proceso de migración, este exchangeRate aún puede ser ajustado por el propietario a través de la función 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);
   }

Listado 3.7: Migration.sol

Impacto Será injusto para los demás usuarios si el exchangeRate cambia durante el proceso de migración.

Sugerencia Una vez que comience la migración, el exchangeRate debe fijarse.

3.2.6 Problema Potencial 7: Implementación Incorrecta de _transfer() (I)

Elemento Descripción
Severidad Alta
Estado Corregido en la Versión 7
Introducido por Versión 1

Descripción En el contrato IncentivizedERC20, la función _transfer() no considera la situación en que el emisor y el destinatario pueden ser la misma cuenta (denominada auto-transferencia). Específicamente, si el emisor es igual al destinatario, el saldo del emisor será sobreescrito al actualizar el saldo del destinatario. En este caso, el atacante puede aumentar su propio saldo infinitamente transfiriéndose a su propia cuenta 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);
          }
        }
      }

Listado 3.8: IncentivizedERC20.sol

Impacto Los tokens pueden ser acuñados infinitamente.

Sugerencia Implementar correctamente la función _transfer(). Por ejemplo, la implementación estándar de _transfer() de ERC20 en OpenZeppelin.

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

Listado 3.9: ERC20.sol en OpenZeppelin

3.2.7 Problema Potencial 8: Falta de Verificación del Período en UniV2TwapOracle

Elemento Descripción
Severidad Baja
Estado Corregido en la Versión 9
Introducido por Versión 1

Descripción En el contrato UniV2TwapOracle, el atributo _period no se valida en la función initialize() ni en 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;
    }

Listado 3.10: UniV2TwapOracle.sol

Impacto En este caso, el oráculo puede devolver un valor inesperado si el _period es demasiado pequeño.

Sugerencia Establecer un límite mínimo en el _period en las funciones initialize y setPeriod.

3.2.8 Problema Potencial 9: Tokens de Polvo No Reembolsables

Elemento Descripción
Severidad Media
Estado Corregido en la Versión 5
Introducido por Versión 1

Descripción En el contrato UniswapPoolHelper, la función zapWETH() está diseñada para ayudar al usuario a convertir tokens WETH en tokens LP. Invocará la función addLiquidityWETHOnly() para añadir liquidez en el pool para los tokens LP. En este proceso, pueden existir tokens de polvo que deberían devolverse a los usuarios. Sin embargo, el UniswapPoolHelper no implementa dicha funcionalidad para manejar estos tokens de polvo.

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);
}

Listado 3.11: UniswapPoolHelper.sol

Impacto Los tokens de polvo permanecerán en el contrato, lo que puede ser aprovechado por otros a través de la función zapTokens(0,0).

Sugerencia Implementar la función para devolver los tokens de polvo después de añadir liquidez.

3.2.9 Problema Potencial 10: Implementación Incorrecta de _transfer() (II)

Elemento Descripción
Severidad Media
Estado Corregido en la Versión 9
Introducido por Versión 7

Descripción En el contrato IncentivizedERC20, la función _transfer() invocará la función handle_ActionAfter() para actualizar el estado del usuario en el contrato ChefIncentivesController en consecuencia. Sin embargo, el parámetro senderBalance no se actualizará si el emisor es igual al destinatario, lo cual es incorrecto.

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);
          }
        }
      }

Listado 3.12: IncentivizedERC20.sol

Impacto Cuando los usuarios se transfieren a sí mismos, su estado en el contrato ChefIncentivesController no se actualizará correctamente, lo que traerá problemas adicionales para las recompensas.

Sugerencia Corregir el senderBalance en la función handleActionAfter().

3.2.10 Problema Potencial 11: Recompensas de Compuesto Manipulables

Elemento Descripción
Severidad Media
Estado Corregido en la Versión 10
Introducido por Versión 5

Descripción En el contrato MFDPlus, la función _convertPendingRewardsToWeth() intercambia las recompensas del usuario por WETH a través del router de Uniswap para re-bloquearlas. Sin embargo, no hay verificación de deslizamiento después del intercambio.

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
     );

Listado 3.13: MFDPlus.sol

Impacto El atacante puede adelantarse a la transacción para manipular el precio y obtener beneficios.

Sugerencia Agregar la verificación de deslizamiento en la función claimCompound().

3.2.11 Problema Potencial 12: Falta de Control de Acceso en setLeverager()

Elemento Descripción
Severidad Media
Estado Corregido en la Versión 9
Introducido por Versión 1

Descripción La función setLeverager() en el contrato LendingPool no tiene control de acceso.

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

Listado 3.14: LendingPool.sol

Impacto Si el leverager no se establece al principio, un atacante podría establecerlo en cualquier dirección, obteniendo así control sobre la lógica de la función depositWithAutoDLP().

Sugerencia Establecer el leverager en la función initialize() o añadir control de acceso para la función setLeverager().

3.2.12 Problema Potencial 13: Sin Verificación de Deslizamiento en addLiquidityWETHOnly()

Elemento Descripción
Severidad Media
Estado Confirmado
Introducido por Versión 1

Descripción El usuario puede usar tokens WETH prestados (o sus propios tokens ETH) o tokens RDNT en vesting en los contratos MFD para obtener tokens LP (es decir, WETH-RDNT).

Sin embargo, al añadir liquidez al pool, el cálculo de los tokens requeridos se basa en la cantidad de reservas en el pool, que puede ser manipulada. En este caso, si el usuario solo tiene tokens WETH, se invocará la función addLiquidityWETHOnly() para intercambiar la mitad de los tokens WETH por tokens RDNT en el pool desequilibrado sin verificar el deslizamiento.

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);
}

Listado 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;
   }  

Listado 3.16: UniswapV2Library.sol

Impacto El atacante puede adelantarse a la transacción para manipular el precio y obtener beneficios.

Sugerencia Verificar el deslizamiento en la función addLiquidityWETHOnly() o asegurarse de que solo pueda ser invocada por UniswapPoolHelper.

3.2.13 Problema Potencial 14: Falta de Verificación de borrowRatio en loopETH()

Elemento Descripción
Severidad Baja
Estado Corregido en la Versión 10
Introducido por Versión 1

Descripción La función loopETH() se utiliza para el apalancamiento de préstamos y recibe un parámetro borrowRatio para especificar la relación de préstamo. Sin embargo, el borrowRatio no se verifica antes de que comience el bucle.

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);
    }

Listado 3.17: Leverager.sol

Impacto El borrowRatio puede ser mayor que RATIO_DIVISOR, lo cual es inconsistente con el diseño original.

Sugerencia Asegurarse de que borrowRatio sea menor o igual a RATIO_DIVISOR.

3.2.14 Problema Potencial 15: Falta de Verificación de Longitud entre assets y poolIDs en setPoolIDs()

Elemento Descripción
Severidad Baja
Estado Corregido en la Versión 10
Introducido por Versión 1

Descripción La función setPoolIDs() permite al propietario establecer diferentes poolIDs para diferentes activos. Sin embargo, no se verifica que las longitudes de estos dos arrays sean iguales.

// 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);
    } 

Listado 3.18: StarBorrow.sol

Impacto Los activos no serán asignados a los poolIDs correctos.

Sugerencia Asegurarse de que las longitudes de assets y poolIDs sean iguales.

3.2.15 Problema Potencial 16: Falta de Revocación del Privilegio de mint en addBountyContract()

Elemento Descripción
Severidad Baja
Estado Confirmado
Introducido por Versión 1

Descripción La función addBountyContract() se utiliza para establecer el nuevo BountyManager. Sin embargo, el contrato de bounty original aún conserva el privilegio de mint, lo cual va en contra del diseño original.

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

Listado 3.19: Leverager.sol

Impacto El BountyManager obsoleto aún tiene privilegios de mint.

Sugerencia Revocar el privilegio de mint del contrato BountyManager original.

Comentario La función addBountyContract solo se llamará una vez para inicializar el BountyManager.

3.2.16 Problema Potencial 17: Los Minters Solo Pueden Asignarse Una Vez

Elemento Descripción
Severidad Baja
Estado Confirmado
Introducido por Versión 1

Descripción El minters se utiliza para registrar a quienes tienen permiso para acceder a la función mint() y addReward(). Sin embargo, cuando uno de los minters (por ejemplo, el contrato ChefIncentivesController) se actualiza, los minters desactualizados no pueden eliminarse.

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

Listado 3.20: MultiFeeDistribution.sol

Impacto Los minters desactualizados no pueden eliminarse cuando se actualizan.

Sugerencia Implementar una función con privilegios para modificar los minters.

Comentario Dado que BountyManager, ChefIncentivesController y MultiFeeDistribution serán actualizables, los minters siempre mantienen la misma dirección proxy.

3.3 Recomendaciones Adicionales

3.3.1 Problema Potencial 18: Optimización de Gas (zapVestingToLp() en Mfd)

Elemento Descripción
Estado Corregido en la Versión 10
Introducido por Versión 1

Descripción La función zapVestingToLp() solo puede ser invocada por el contrato LockZap para transferir las ganancias bloqueadas del usuario. Itera el array de ganancias del usuario comenzando desde el índice 0, y verifica si el unlockTime es mayor que el timestamp actual. Si es así, esta ganancia se eliminará del array y se transferirá. Sin embargo, dado que el unlockTime en el array aumenta con el índice, sería más eficiente comenzar la iteración desde el final del array hacia el principio. Si el unlockTime es menor que el timestamp actual, el bucle puede interrumpirse.

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;
    }

Listado 3.21: MultiFeeDistribution.sol

Sugerencia Comenzar la iteración desde el final de earnings hacia el principio. Si el unlockTime es menor que el timestamp actual, el bucle puede interrumpirse.

3.3.2 Problema Potencial 19: Reserva de Bounty No Vacía en BountyManager

Elemento Descripción
Estado Corregido en la Versión 10
Introducido por Versión 1

Descripción En la función _sendBounty(), si no hay suficientes tokens RDNT para la transferencia en el contrato BountyManager, se emitirá el evento BountyReseveEmpty(), y el contrato se pausará. Sin embargo, es posible que aún queden algunos tokens RDNT, lo cual es inconsistente con el 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;
		}
	}

Listado 3.22: BountyManager.sol

Sugerencia Transferir los tokens RDNT restantes aunque no sean suficientes.

3.3.3 Problema Potencial 20: Nomenclatura Inconsistente en requiredUsdValue()

Elemento Descripción
Estado Confirmado
Introducido por Versión 1

Descripción La función requiredUsdValue() se utiliza para verificar el valor bloqueado requerido del usuario que desea calificarse para ganar recompensas al mantener RTokens. El cálculo se basa en el valor de colateral del usuario, que se devuelve desde la función getUserAccountData(). Sin embargo, el valor devuelto se denomina totalCollateralETH, lo cual es inconsistente con el de la función requiredUsdValue() (es decir, totalCollateralUSD).

Sugerencia Estandarizar las convenciones de nomenclatura de las funciones con el nombre de token correcto. Por ejemplo, renombrar requiredUsdValue() a requiredEthValue().

Comentario Preferimos mantener los contratos de AAVE lo más similares posible, por lo que no actualizamos el nombre.

3.4 Notas

3.4.1 Problema Potencial 21: MFDPlus Obsoleto

Elemento Descripción
Estado Confirmado
Introducido por versión 10

Descripción El contrato MFDPlus ya no se utiliza. La lógica de compuesto se ha trasladado al contrato AutoCompounder y otra lógica se ha trasladado al contrato MiddleFeeDistribution.

4. Apéndice

4.1 Resultados de las Pruebas de Seguridad Estáticas Automatizadas

Tabla 4.1: Resultados de las Pruebas de Seguridad Estáticas Automatizadas. Encontrado indica el número de problemas reportados por las herramientas. FP significa el número de falsos positivos tras nuestra verificación manual.

ID Detector Descripción Impacto Encontrado FP Resultado
1 arbitrary-send-erc20 Llamada a transferFrom con from arbitrario Alto 1 1 Superado
2 array-by-reference Modificación de array de almacenamiento por valor Alto 0 0 Superado
3 incorrect-shift Orden incorrecto de parámetros en una instrucción de desplazamiento Alto 0 0 Superado
4 multiple-constructors Múltiples esquemas de constructor Alto 0 0 Superado
5 name-reused Reutilización del nombre del contrato Alto 0 0 Superado
6 protected-vars Modificación de variables directamente sin control de acceso Alto 0 0 Superado
7 rtlo Uso del carácter de control de derecha a izquierda Alto 0 0 Superado
8 shadowing-state Sombreado de variables de estado Alto 1 1 Superado
9 suicidal Funciones que permiten a cualquiera destruir el contrato Alto 0 0 Superado
10 uninitialized-state Variables de estado no inicializadas Alto 3 3 Superado
11 uninitialized-storage Variables de almacenamiento no inicializadas Alto 0 0 Superado
12 unprotected-upgrade Contrato actualizable no protegido Alto 1 1 Superado
13 arbitrary-send-erc20-permit transferFrom usa from arbitrario con permit Alto 0 0 Superado
14 arbitrary-send-eth Funciones que envían Ether a destinos arbitrarios Alto 0 0 Superado
15 controlled-array-length Asignación de longitud de array contaminada Alto 0 0 Superado
16 controlled-delegatecall Destino de delegatecall controlado Alto 0 0 Superado
17 delegatecall-loop Funciones pagables que usan delegatecall dentro de un bucle Alto 0 0 Superado
18 msg-value-loop Uso de msg.value dentro de un bucle Alto 0 0 Superado
19 reentrancy-eth Vulnerabilidades de reentrada (robo de ethers) Alto 5 5 Superado
20 storage-array Error del compilador en array de enteros con signo en almacenamiento Alto 0 0 Superado
21 unchecked-transfer Transferencia de tokens sin verificar Alto 12 12 Superado
22 weak-prng PRNG débil Alto 0 0 Superado
23 domain-separator-collision Detecta tokens ERC20 que tienen una función cuya firma colisiona con DOMAIN_SEPARATOR() de EIP-2612 Medio 0 0 Superado
24 enum-conversion Detecta conversión peligrosa de enum Medio 0 0 Superado
25 erc20-interface Interfaces ERC20 incorrectas Medio 0 0 Superado
26 erc721-interface Interfaces ERC721 incorrectas Medio 0 0 Superado
27 incorrect-equality Igualdades estrictas peligrosas Medio 23 23 Superado
28 locked-ether Contratos que bloquean ether Medio 1 1 Superado
29 mapping-deletion Eliminación en un mapping que contiene una estructura Medio 0 0 Superado
30 shadowing-abstract Sombreado de variables de estado desde contratos abstractos Medio 0 0 Superado
31 tautology Tautología o contradicción Medio 0 0 Superado
32 write-after-write Escritura no utilizada Medio 3 3 Superado
33 boolean-cst Uso incorrecto de constante booleana Medio 0 0 Superado
34 constant-function-asm Funciones constantes que usan código ensamblador Medio 0 0 Superado
35 constant-function-state Funciones constantes que cambian el estado Medio 0 0 Superado
36 divide-before-multiply Orden impreciso de operaciones aritméticas Medio 20 20 Superado
37 reentrancy-no-eth Vulnerabilidades de reentrada (sin robo de ethers) Medio 12 12 Superado
38 reused-constructor Constructor base reutilizado Medio 0 0 Superado
39 tx-origin Uso peligroso de tx.origin Medio 1 1 Superado
40 unchecked-lowlevel Llamadas de bajo nivel sin verificar Medio 0 0 Superado
41 unchecked-send Envío sin verificar Medio 0 0 Superado
42 uninitialized-local Variables locales no inicializadas Medio 33 33 Superado
43 unused-return Valores de retorno no utilizados Medio 19 19 Superado

4.2 Resultados de las Pruebas de Seguridad Dinámicas Automatizadas

Tabla 4.2: Propiedades Probadas para la Lógica Relacionada con Préstamos

ID Propiedad Resultado
1 Llamar a deposit nunca conduce a una disminución de la cantidad de RToken de onBehalfOf Superado
2 Llamar a withdraw nunca conduce a un aumento de la cantidad de RToken de msg.sender Superado
3 Llamar a borrow con modo de tasa de interés estable nunca conduce a una disminución del StableDebtToken de onBehalfOf. Superado
4 Llamar a borrow con modo de tasa de interés variable nunca conduce a una disminución del VariableDebtToken de onBehalfOf. Superado
5 Llamar a borrow con onBehalfOf diferente de msg.sender nunca conduce a un aumento de la asignación de préstamo de msg.sender. Superado
6 Llamar a repay con modo de tasa de interés estable nunca conduce a un aumento del StableDebtToken de onBehalfOf. Superado
7 Llamar a repay con modo de tasa de interés variable nunca conduce a un aumento del VariableDebtToken de onBehalfOf. Superado
8 liquidityIndex nunca disminuirá. Superado
9 liquidityIndex permanecerá constante dentro del mismo bloque. Superado
10 variableBorrowIndex nunca disminuirá. Superado
11 variableBorrowIndex permanecerá constante dentro del mismo bloque. Superado
12 La disminución de los montos de colateral nunca conducirá a un factor de salud menor que 1. Superado
13 El aumento de los montos de préstamo nunca conducirá a un factor de salud menor que 1. Superado

Tabla 4.3: Propiedades Probadas para la Lógica Relacionada con Staking

ID Propiedad Resultado
1 El saldo total del usuario siempre es igual a la suma del saldo bloqueado, saldo desbloqueado y saldo ganado. Superado
2 El saldo bloqueado del usuario siempre es igual a la suma de los montos de userLocks Superado
3 El saldo lockedWithMultiplier del usuario siempre es igual a la suma de los montos de userLocks multiplicados por el multiplicador de userLocks Superado
4 lockedSupply siempre es igual a la suma de los saldos bloqueados de los usuarios Superado
5 lockedSupplyWithMultiplier siempre es igual a la suma de los saldos lockedWithMultiplier de los usuarios Superado
6 rewardPerTokenStored nunca disminuye. Superado
7 rewardPerTokenStored permanecerá constante dentro del mismo bloque. Superado
8 totalSupply siempre es igual a la suma de los montos de los usuarios Superado
9 accRewardPerShare nunca disminuye. Superado
10 accRewardPerShare permanecerá constante dentro del mismo bloque. Superado

Tabla 4.4: Propiedades Probadas para Otras Características

ID Propiedad Resultado
1 El saldo de WETH y RDNT del contrato LockedZap siempre será cero. Superado
2 El saldo de WETH y RDNT del contrato LiquidityZap siempre será cero. Superado
3 El saldo de WETH y RDNT del contrato BalancerPoolHelper siempre será cero. Superado
4 El saldo de WETH y RDNT del contrato UniswapPoolHelper siempre será cero. Superado
5 Llamar a loop siempre conducirá a que el usuario sea elegible para recompensas Superado
6 Llamar a loopETH siempre conducirá a que el usuario sea elegible para recompensas Superado
7 Llamar a executeBounty con _execute igual a false nunca conducirá a cambios en el almacenamiento. Superado
8 Llamar a transfer con el emisor igual al receptor nunca conduce a cambios de saldo. Fallido en la Versión 1. Superado en la Versión 7

5 Avisos y Observaciones

5.1 Descargo de Responsabilidad

Este informe no constituye asesoramiento de inversión ni una recomendación personal. No considera, y no debe interpretarse como que considera o tiene alguna relación con, la economía potencial de un token, venta de tokens o cualquier otro producto, servicio u otro activo. Ninguna entidad debe basarse en este informe de ninguna manera, incluyendo para el propósito de tomar decisiones de compra o venta de cualquier token, producto, servicio u otro activo.

Este informe no es un respaldo de ningún proyecto o equipo en particular, y el informe no garantiza la seguridad de ningún proyecto en particular. Estas pruebas de seguridad no otorgan ninguna garantía sobre el descubrimiento de todos los problemas de seguridad de los contratos inteligentes, es decir, el resultado de la evaluación no garantiza la inexistencia de hallazgos adicionales de problemas de seguridad. Como las pruebas de seguridad no pueden considerarse exhaustivas, siempre recomendamos proceder con auditorías independientes y un programa público de recompensas por errores para garantizar la seguridad de los contratos inteligentes.

El alcance de estas pruebas de seguridad está limitado al código mencionado en la Sección 1.2. A menos que se especifique explícitamente, la seguridad del lenguaje en sí (por ejemplo, el lenguaje Solidity), la cadena de herramientas de compilación subyacente y la infraestructura informática están fuera del alcance.

5.2 Procedimiento de Auditoría

Realizamos la auditoría de acuerdo con el siguiente procedimiento.

  • Detección de Vulnerabilidades Primero escaneamos los contratos inteligentes con analizadores de código automáticos, y luego verificamos manualmente (rechazamos o confirmamos) los problemas reportados por ellos.

  • Análisis Semántico Estudiamos la lógica de negocio de los contratos inteligentes y realizamos una investigación más profunda sobre las posibles vulnerabilidades utilizando una herramienta de fuzzing automática (desarrollada por nuestro equipo de investigación). También analizamos manualmente posibles escenarios de ataque con auditores independientes para verificar los resultados.

  • Recomendación Proporcionamos algunos consejos útiles a los desarrolladores desde la perspectiva de las buenas prácticas de programación, incluyendo optimización de gas, estilo de código, etc.

A continuación, mostramos los principales puntos de verificación concretos.

5.2.1 Seguridad de Software

  • Reentrada

  • DoS

  • Control de acceso

  • Manejo de datos y flujo de datos

  • Manejo de excepciones

  • Llamada externa no confiable y flujo de control

  • Consistencia de inicialización

  • Operaciones de eventos

  • Aleatoriedad propensa a errores

  • Uso incorrecto del sistema proxy

5.2.2 Seguridad DeFi

  • Consistencia semántica

  • Consistencia de funcionalidad

  • Gestión de permisos

  • Lógica de negocio

  • Operación de tokens

  • Mecanismo de emergencia

  • Seguridad del oráculo

  • Lista blanca y lista negra

  • Impacto económico

  • Transferencia por lotes

5.2.3 Seguridad NFT

  • Elemento duplicado

  • Verificación del receptor del token

  • Seguridad de metadatos fuera de cadena

5.2.4 Recomendaciones Adicionales

  • Optimización de gas

  • Calidad y estilo del código

Nota: Los puntos de verificación anteriores son los principales. Podemos utilizar más puntos de verificación durante el proceso de auditoría según la funcionalidad del proyecto.

Best Security Auditor for Web3

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

BlockSec Audit