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.



