La Magia detrás del ERC-165


La interoperabilidad entre contratos es un aspecto fundamental para el desarrollo y adopción de soluciones descentralizadas. Por ese motivo el estándar ERC-165 juega un papel importante al proporcionar una forma estandarizada de detectar si un contrato inteligente implementa una o varias interfaces específicas.

¿Qué es el ERC-165?

El ERC-165 es un estándar de Ethereum propuesto en 2018. La propuesta fue aprobada y ampliamente aceptada por la comunidad de desarrolladores debido a su utilidad. Este estándar permite a los contratos inteligentes declarar qué interfaces o características implementan, facilitando la comunicación y la interoperabilidad entre contratos sin requerir un conocimiento previo de las interfaces que estos implementan.

Esto resulta de gran utilidad para poder verificar qué interfaces específicas tiene implementado un contrato, y saber qué versión del contrato inteligente está siendo utilizada.

¿Cómo funciona?

Cuando se habla de interfaces nos referimos a un conjunto de funciones que un contrato puede tener. Cada función en una interfaz tiene un selector, un código que identifica a esa función.

interface MyInterface {
  function obtainSelector() external pure returns (bytes4);
}

contract MyContract is MyInterface {

 function obtainSelector() external pure override returns (bytes4) {
  return MyInterface.obtainSelector.selector;
 }
}

En este caso particular, tenemos una interfaz llamada MyInterface con una función llamada obtainSelector.

Cada función tiene su propio selector, es un código único que identifica cada función dentro del contrato.

Para generar el selector, primero tomamos la firma de la función, que es una representación del nombre de la función y los tipos de parámetros que acepta. Luego, aplicamos el algoritmo keccak-256 a esta firma, obteniendo un valor hexadecimal de 256 bits. Finalmente, tomamos los primeros cuatro bytes de este resultado y ese será el selector de la función. Este selector actúa como una etiqueta única para identificar la función en un contrato inteligente.

⚠️

Todo esto no es necesario memorizarlo, pero sí es interesante saber como funciona por debajo para entender lo que haremos más adelante.

Ventajas del ERC-165

  • Interoperabilidad: Al adoptar el estándar ERC-165, pueden declarar explícitamente las interfaces que implementan, lo que facilita a otros contratos y dApps interactuar con ellos de manera segura y eficiente.

  • Eficiencia en el uso de recursos: Los contratos pueden verificar de manera proactiva si un contrato objetivo implementa una interfaz específica antes de realizar una llamada. De esta manera, se evitan llamadas innecesarias a contratos que no pueden manejar ciertas operaciones, lo que reduce el consumo de gas y, en última instancia, disminuye el costo de la ejecución de las transacciones. Esto contribuye a una mejor utilización de los recursos en la red y mejora la experiencia del usuario final.

  • Mejora de la seguridad: El estándar ERC-165 ofrece un mecanismo de verificación que ayuda a prevenir interacciones no deseadas entre contratos. Al permitir que un contrato inteligente declare explícitamente las interfaces que implementa, se reducen los riesgos de comportamientos inesperados o ataques maliciosos. Los contratos que utilizan el estándar pueden validar que los contratos con los que desean interactuar realmente cumplen con las interfaces requeridas antes de realizar cualquier acción. De esta manera, se evitan posibles vulnerabilidades y se fortalece la seguridad de todo el sistema.

  • Fomento de la estandarización: El estándar ERC-165 promueve la adopción de prácticas de desarrollo más estandarizadas. Al facilitar la identificación y la compatibilidad entre contratos, se crea un entorno más predecible y coherente para los desarrolladores. Además, esto facilita la creación de bibliotecas y herramientas comunes que pueden ser utilizadas por diferentes proyectos, lo que acelera el proceso de desarrollo y mejora la calidad del código.

Funcionamiento y métodos clave del ERC-165

El ERC-165 define dos elementos importantes: el identificador de interfaz interfaceId y el método supportsInterface.

  • Identificador de interfaz interfaceId:
    Al igual que el selector que tienen los métodos, las interfaces también tienen sus identificadores. El interfaceId es una función hash única. Se obtiene al aplicar la operación XOR a todos los selectores que tenga la interfaz.

    En términos técnicos, se calcula mediante la función keccak256 de la concatenación de las firmas de las funciones de la interfaz.

    La manera más sencilla de obtener este identificador es usar el type:

interface MyInterface {
 function obtainSelector() external pure returns (bytes4);
}

contract MyToken is MyInterface {
 function supportsInterface(
  bytes4 interfaceId
 ) public view virtual override returns (bool) {
  return
   interfaceId == type(MyInterface).interfaceId ||
   super.supportsInterface(interfaceId);
 }
 
 function obtainSelector() external pure override returns (bytes4) {
  return MyInterface.obtainSelector.selector;
 }
}
  • Método supportsInterface:
    El contrato que implementa el estándar ERC-165 debe proporcionar el método supportsInterface, que toma como entrada el interfaceId y devuelve un valor booleano indicando si el contrato admite la interfaz correspondiente.

    El proceso de implementación del método supportsInterface en un contrato implica iterar sobre la lista de interfaceId que el contrato soporta y compararlos con el interfaceId proporcionado como argumento. Si se encuentra una coincidencia, el contrato devuelve true, indicando que implementa la interfaz dada; de lo contrario, devuelve false.

¿Como obtenemos el interfaceId?

En el contexto de JavaScript, no contamos con una forma nativa de obtener el interfaceId de un contrato. Sobre todo si queremos realizar pruebas o validar si el contrato admite cierta interfaz.

Sin embargo, gracias a lo explicado previamente sobre el funcionamiento interno del estándar ERC-165, podemos desarrollar una función que genere automáticamente el interfaceId de un contrato.

Al compilar las interfaces, la herencia no se considera de forma automática, lo que significa que si nuestra interfaz hereda de otra, el interfaceId generado solo incluirá las funciones definidas directamente en la interfaz, omitiendo las heredadas.

Para solucionar esto, añadimos a la función que acepte un array de interfaces. De esta forma, se pueden calcular correctamente todas las funciones, incluidas las heredadas, asegurando que el interfaceId generado sea preciso y completo.

import { type Interface, type FunctionFragment } from "ethers";
import { MyInterface__factory } from "../typechain-types/index";

export function getInterfaceID<T extends Interface>(contractInterface: T[]) {

 let interfaceID: bigint = 0n;
 contractInterface.forEach((interfaceContract) => {
  const functions = interfaceContract.fragments
   .filter(fragment => fragment.type === 'function') as FunctionFragment[];
  for (let i = 0; i < functions.length; i++) {
   const sighash = functions[i].selector
   interfaceID = interfaceID ^ BigInt(sighash)
  }

 })

 return '0x' + interfaceID.toString(16).padStart(8, '0')
}
console.log(getInterfaceID([MyInterface__factory.createInterface()]))
// 0x3b7255
📰

He creado este repositorio en Github donde poder ver como se implementa y como funciona. Para el ejemplo se ha utilizado Hardhat.

GitHub

Conclusión

El estándar ERC-165 es una herramienta fundamental en el mundo de los contratos inteligentes, ya que proporciona una funcionalidad similar al instanceof en JavaScript. Al permitir que los contratos declaren las interfaces que implementan, se crea una forma estandarizada de verificar si un contrato es compatible con una interfaz específica. Esto facilita la interoperabilidad y la comunicación entre contratos, al igual que instanceof en JavaScript permite comprobar si un objeto es una instancia de una clase en particular. Gracias al ERC-165, se fortalece la seguridad, se mejora la eficiencia y se promueve una mayor estandarización.