WEB3DEV Español

Hector
Hector

Posted on

Diseñando Patrones para Contratos Inteligentes - Seguridad

Image description

Prólogo

Esta es la sección final de la serie de 5 partes de cómo resolver defectos de diseños recurrentes con patrones de diseños reusables y convencionales. Mediante esto, diseccionaremos en la Seguridad, un grupo de patrones que introducen medidas de seguridad para mitigar el daño y asegurar una ejecución de contrato confiable.

Interacción Revisión-Efecto

Problema

Cuando un contrato llama a otro, le da el control al mismo. El contrato invocado puede, entonces, reentrar en el contrato que fue llamado e intentar manipular su estado o secuestrar el flujo de control a través de un código maligno.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

// ESTE CONTRATO CONTIENE UN BUG - NO USAR
contract Fund {
/// @dev Mapeo del ether que comparte con el contrato.
mapping(address => uint256) shares;

/// Retira tu parte.
function withdraw() public {
// El código del invocador se ejecuta y puede reentrar para la retirada
(bool success,) = msg.sender.call{value: shares[msg.sender]}("");

// POCO SEGURO - las partes del usuario deben ser reseteadas antes de la llamada externa
if (success)
shares[msg.sender] = 0;
}
}
Enter fullscreen mode Exit fullscreen mode

Cada transferencia puede incluir siempre un código de ejecución, así que el receptor puede ser un contrato que llama de nuevo a la retirada. Esto permitiría que tenga múltiples retiradas y, básicamente, retirar todo el Ether del contrato.

Solución

El patrón de Interacción Revisión-Efecto asegura que todas las rutas del código, a través de un contrato, completen todos los controles necesarios de los parámetros suministrados antes de modificar el estado del contrato (Revisión, checks) y sólo luego de eso, realiza cualquier cambio al estado (Effectos, effects), puede que haga llamados a la función en otros contratos luego que todos los estados de cambio planeados hayan sido escritos en el almacenamiento (Interactions).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

contract Fund {
/// @dev Mapeo del ether que comparte con el contrato.
mapping(address => uint256) shares;

/// Retira tu parte.
function withdraw() public {
uint256 share = shares[msg.sender];
// 1. Revisiones
require(share > 0);

// 2. Efectos
shares[msg.sender] = 0;

// 3. Interacción
payable(msg.sender).transfer(share);
}
}
Enter fullscreen mode Exit fullscreen mode

El ataque de reentrada es especialmente dañino cuando se usa un address.call de bajo nivel, el cual envía todo el gas restante por defecto, dándole al contrato llamado más oportunidad para acciones potencialmente malignas. Por lo tanto, el uso de address.call de bajo nivel debe ser evitado en la medida de lo posible.

Para enviar fondos, address.send() y address.transfer() son las formas preferidas, estas funciones minimizan el riesgo de reentrada a través de un envío limitado de gas (el contrato invocado sólo le dan un estipendio de 2.300 gas, lo cual es lo mínimo suficiente para el registro de un evento)

Parada de Emergencia (Interruptor del circuito)

Problema

Desde que un contrato desplegado se ejecuta autónomamente en la red de Ethereum, no hay opción para detener su ejecución en caso de un bug mayor o problemas con la seguridad.

Solución

Una contramedida y respuesta rápida para atacantes desconocidos son las paradas de emergencia o la interrupción de circuitos. Para la ejecución del contrato o sus partes, cuando ciertas condiciones se cumplen.

Un escenario recomendado sería que, luego que un bug sea detectado, todas las funciones críticas se pararían, dejando sólo la posibilidad de retirar los fondos.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract Pausable {
bool private paused;

event Paused(address account);
event Unpaused(address account);

error EnforcedPause();
error ExpectedPause();

modifier whenNotPaused() {
if (paused) {
revert EnforcedPause();
}
_;
}

modifier whenPaused() {
if (!paused) {
revert ExpectedPause();
}
_;
}

constructor() {
paused = false;
}

function _pause() internal virtual whenNotPaused {
paused = true;
emit Paused(msg.sender);
}

function _unpause() internal virtual whenPaused {
paused = false;
emit Unpaused(msg.sender);
}
}
Enter fullscreen mode Exit fullscreen mode

Implementar una parada de emergencia en un contrato Staking:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/access/Ownable.sol";
import "./Pausable.sol";

contract Staking is Pausable, Ownable {
/** declara las variables de estado */

function pause() external onlyOwner {
_pause();
}

function unpause() external onlyOwner {
_unpause();
}

function deposit() external payable whenNotPaused {
// algún código
}

function withdraw() external whenNotPaused {
// algún código
}

function emergencyWithdraw() external whenPaused {
// algún código
}
}
Enter fullscreen mode Exit fullscreen mode

Bache de Velocidad

Problema

La ejecución simultánea de tareas sensibles por un gran número de grupos, pueden provocar la caída de un contrato.

Solución

Las tareas sensibles son ralentizadas a propósito, para que cuando una acción maligna ocurra, el daño sea restringido y haya más tiempo disponible para contrarrestar.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract SpeedBump {
struct Withdrawal {
uint256 amount;
uint256 requestedAt;
}

uint256 constant WAIT_PERIOD = 7 days;

mapping (address => uint256) private balances;
mapping (address => Withdrawal) private withdrawals;

// La dirección de cada usuario puede sólo depositar una vez hasta que haya hecho una retirada total
function deposit() public payable {
bool hasDeposited = withdrawals[msg.sender].amount > 0;
if(!hasDeposited)
balances[msg.sender] += msg.value;
}

function requestWithdrawal() public {
if (balances[msg.sender] > 0) {
uint256 amountToWithdraw = balances[msg.sender];
balances[msg.sender] = 0;

withdrawals[msg.sender] = Withdrawal({
amount: amountToWithdraw,
requestedAt: block.timestamp
});
}
}

// Sòlo se puede hacer una retirada total cuando el WAIT_PERIOD expira.
function withdraw() public {
if(withdrawals[msg.sender].amount > 0 &&
block.timestamp > withdrawals[msg.sender].requestedAt + WAIT_PERIOD)
{
uint256 amount = withdrawals[msg.sender].amount;
withdrawals[msg.sender].amount = 0;
payable(msg.sender).transfer(amount);
}
}
}
Enter fullscreen mode Exit fullscreen mode

Puedes referenciarte del contrato implementado por TimelockController a través de OpenZeppeling para la versión del contrato basado en la producción. Un timelock es un contrato inteligente que retrasa la función de llamada de otro contrato inteligente después que una cantidad de tiempo predeterminada haya pasado. Los Timelocks son mayormente usados en el contexto de la gobernanza para añadir un retraso a las acciones administrativas y son generalmente considerados como un fuerte indicador que un proyecto es legítimo y demuestra compromiso al proyecto por los dueños del proyecto.

Límite de Tasa

Problema

Un pedido urgente en ciertas tareas no es deseable y puede dificultar el rendimiento operacional correcto de un contrato.

Solución

Un límite de tasa regula la frecuencia en la que una función puede ser llamada consecutivamente, dentro de un intervalo de tiempo especificado.

Un escenario de uso para contratos inteligentes puede ser encontrado en consideraciones operativas, para poder controlar el impacto del comportamiento del (colectivo) usuario.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract RateLimit {
uint enabledAt = block.timestamp;

modifier enabledEvery(uint256 t) {
if (block.timestamp >= enabledAt) {
enabledAt = block.timestamp + t;
_;
}
}

function withdraw() public enabledEvery(1 minutes) {
// algún código
}
}
Enter fullscreen mode Exit fullscreen mode

El ejemplo de arriba demuestra la limitación de la ejecución de retirada de un contrato para prevenir el desangre rápido de los fondos.

Mutex

Problema

Los ataques de reentrada pueden manipular el estado de un contrato y secuestrar el flujo de control.

Solución

Un mutex (de mutual exclusion, exclusión mutua) se conoce como un mecanismo de sincronización en las ciencias de la computación para restringir el acceso concurrente a un recurso. Luego que hayan emergido escenarios de ataques de reentrada, este patrón encuentra su aplicación en contratos inteligentes para protegerlos contra las funciones recursivas de llamadas de contratos externos.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

abstract contract ReentrancyGuard {
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;

uint256 private _status;

error ReentrancyGuardReentrantCall();

constructor() {
_status = _NOT_ENTERED;
}

modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}

function _nonReentrantBefore() private {
// En la primera llamada a nonReentrant, _status será _NOT_ENTERED
if (_status == _ENTERED) {
revert ReentrancyGuardReentrantCall();
}
// Cualquier llamada a nonReentrant luego de este punto, fallará
_status = _ENTERED;
}

function _nonReentrantAfter() private {
// Almacenando el valor original nuevamente, un reembolso es desencadenado (mira
// https://eips.ethereum.org/EIPS/eip-2200)
_status = _NOT_ENTERED;
}
}

contract Mutex is ReentrancyGuard {
/** declara las variables de estado */

// f está protegido por un mutex, por lo tanto, las llamadas de reentrada
// desde msg.sender.call no pueden invocar de nuevo a f
function f() external nonReentrant {
// algún código
}
}
Enter fullscreen mode Exit fullscreen mode

Límite del Balance

Problema

Siempre estará el riesgo que un contrato sea comprometido por bugs en el código o por algún problema de seguridad desconocido dentro de la plataforma del contrato.

Solución

Generalmente, es una buena idea administrar la cantidad de dinero en riesgo cuando codeas contratos inteligentes. Esto se puede lograr limitando el balance total contenido dentro del contrato.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract LimitBalance {
uint256 public limit;

modifier limitedPayable() {
require(address(this).balance < limit);
_;
}

constructor(uint256 value) {
limit = value;
}

function deposit() external payable limitedPayable {
// algún código
}
}
Enter fullscreen mode Exit fullscreen mode

El patrón monitorea el balance del contrato y rechaza los pagos enviados junto a la función de invocación luego que se exceda del límite de la cuota predefinida.

Deberías tomar en cuenta que esta forma no puede prevenir la admisión de Ether enviado forzosamente, por ejemplo: un beneficiario de una llamada selfdestruct(address) o como el receptor de recompensas de deberes del validador.

Conclusión

Describí los patrones del grupo de Seguridad en detalle y he proveído códigos de ejemplo para ilustrarlo mejor. Te recomiendo que uses al menos, uno de estos patrones en tu próximo proyecto de Solidity para revisar tu entendimiento sobre este tópico.

Ten en cuenta que incluso si tu contrato inteligente está libre de bugs, incluso si cumples estrictamente con esos patrones que te mencioné, el compilador o la plataforma en sí pueden que tengan bugs. Una lista de algunos bugs relacionados a la seguridad del compilador de conocimiento público, puede ser encontrado aquí y las consideraciones espléndidas de la seguridad pueden ser encontradas aquí. Te sugiero que eches un vistazo a esos documentos, encuentres más artículos y blogs para las mejoras de seguridad y siempre es una buena práctica, pedirle a personas que revisen tu código.

Sígueme en LinkedIn para estar conectados

https://www.linkedin.com/in/ninh-kim-927571149/

Este artículo es una traducción de BrianKim, hecha por Héctor Botero. Puedes encontrar el artículo original aquí.
Sería genial escucharte en nuestro Discord, puedes contarnos tus ideas, comentarios, sugerencias y dejarnos saber lo que necesitas.
Si prefieres puedes escribirnos a @web3dev_es en Twitter.

Discussion (0)