El auge de NFT ha ocurrido y sigue ocurriendo (al momento de escribir este artículo en julio de 2022). Etherscan tiene una práctica utilidad de búsqueda que, junto con sus útiles funciones de verificación y descompilación, le permite echar un vistazo al código de muchos ERC721 para comparar. Junto con muchos contratos bien diseñados, también podemos ver que muchos cometen los mismos errores una y otra vez. En este artículo, daré mi opinión sobre lo que creo que son 4 de los "fallos de diseño" más comunes para NFT, que comúnmente noto cuando veo contratos de NFT en etherscan.
Tenga en cuenta que este artículo se escribió principalmente teniendo en cuenta las cadenas de bloques compatibles con EVM, pero muchos de los puntos son aplicables o tienen alguna analogía o equivalente en otras redes también.
Por favor, no se sienta insultado si ha cometido algo de lo que yo llamo “fallas de diseño”. Estas son mis opiniones y, además, como desarrollador, entiendo perfectamente la necesidad de ahorrar en costos de gas en estos tiempos de congestión de la red, que son muy costosos. Considere sin embargo mi punto de vista como consultor independiente; un cliente que puede gastar miles de dólares en ayuda para el desarrollo seguramente puede destinar cien más para la implementación, en la búsqueda de la excelencia.
Por supuesto, esto está hablando de la cadena Ethereum, que es la más cara a la fecha de este escrito; Polygon lo es menos, y otras cadenas como Solana (que no es EVM) aún menos. Mi punto es que si hay fondos disponibles, los beneficios de una implementación de mayor calidad pueden valer el costo adicional.
Esto es extremadamente común, pero cuando lo veo, marca el contrato, a mis ojos, como hecho por un aficionado. Para ser justos, existen motivaciones válidas y comprensibles. Por un lado, implementar y administrar contratos en muchas redes se ha vuelto muy costoso y se han hecho esfuerzos para ahorrar en esos costos. Y, en aras de la simplicidad, uno podría pensar, ¿por qué no incluir la lógica de acuñación y venta en el contrato mismo?
Pero esto no es realmente una buena idea. El contrato en sí debe ser el centro inmutable de una red de lógica, pero nunca debe ensuciarse las manos manejando dinero directamente. Esto incluye ventas, tiempos de venta, listas blancas, etc., directamente en el mismo código de contrato que la implementación de ERC721. La lógica de venta y la lógica central están estrechamente unidas.
Si bien ahorrar en costos de gasolina podría ser la razón mejor y más comprensible para incluir toda la lógica en un contrato, creo que, considerando todo, hay razones mucho mejores para no implementar este atajo de diseño. La lógica de su contrato central debe ser lo único grabado en piedra y, en la mayoría de los casos, implementará el estándar de una manera muy, bueno... estándar. Muchos clones son (o podrían ser) casi clones entre sí. Su estrategia de acuñación, precios (si está vendiendo mentas): este tipo de cosas deben desvincularse. Esto permite que su contrato sea flexible de una manera que no perjudique la confianza del usuario. Diseño disociado y principio de responsabilidad única. Nota al margen: creo que tiene sentido restringir el suministro (es decir, maxSupply) en el propio contrato ERC721, siempre que alguien con un rol de administrador pueda modificarlo.
Un contrato de token necesita algún tipo de control de acceso, porque hay funciones (como acuñar o hacer algo con los parámetros de suministro) que deberían estar disponibles solo para las direcciones autorizadas. La forma más sencilla de lograr esto es usar un modelo Ownable (generalmente usando el contrato Ownable de OpenZeppelin porque ¿por qué reinventar la rueda para una necesidad tan básica?). Pero recomendaría enfáticamente usar un control de acceso basado en roles en su lugar, por las siguientes razones. La motivación detrás del uso de Ownable (o algo similar) es probablemente la simplicidad (y el ahorro en costos de gasolina), lo cual está bien en la superficie. También puede "saber" que usted (o su cliente) "siempre" será el único que administrará el contrato. Es preferible la preparación para el futuro, cuando el costo es bajo; y la complejidad de la seguridad basada en roles (por ejemplo, IAccessControl de OpenZeppelin) es honestamente un poco más compleja (y costosa) en comparación con el modelo Ownable. Si los costos de la gasolina siguen siendo un problema, siempre puede reducir el código de seguridad basado en roles (ya sea OpenZeppelin o el suyo propio) solo para lo que necesita. Pero la razón más importante para usar la función basada en roles es que le permite desvincular la funcionalidad (como en el punto anterior, la información de precios y ventas) del propio contrato ERC721. Le permite designar un contrato separado como el minter asignándole el rol de "minter", sin permitirle permisos de administrador completos. Mientras que el administrador (o administradores, que probablemente sean humanos y no contratos) todavía tienen permisos de nivel superior (como eliminar y agregar permisos). Cuando el minter (por ejemplo) ya no satisface sus necesidades, uno simplemente lo retira revocando sus derechos de acuñación y asignando derechos de acuñación a un nuevo contrato que implementa una nueva estrategia de acuñación; es modular, conveniente y seguro. Otras actividades además de la acuñación se pueden manejar de la misma manera, según los casos de uso particulares de los proyectos.
Muchos tokens (o contratos en general) no implementan ERC-165 o no lo implementan de manera óptima. ERC-165, en mi opinión, se trata de interoperabilidad. Hace que su contrato sea compatible con el futuro, y los intercambios pueden llamarlo para averiguar (por ejemplo) sobre la estructura de regalías de su NFT. Veo que esto a menudo no se implementa en absoluto, o se implementa de manera subóptima.
Aquí hay una regla general para implementarlo correctamente:
|| type(ISomeInterface).interfaceId == _interfaceId
ejemplo:
function supportsInterface(bytes4 _interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) { return super.supportsInterface(_interfaceId) || _interfaceId == type(IERC2981).interfaceId; }
Si su código no tiene clases principales que implementen ERC-165, solo se debe representar el segundo tipo, como
function supportsInterface(bytes4 _interfaceId) public view override returns (bool) { return _interfaceId == type(IERC721).interfaceId || _interfaceId == type(IERC2981).interfaceId || _interfaceId == type(IAccessControl).interfaceId; }
Si su código no implementa otras interfaces además de las que manejan las implementaciones de ERC-165 de las clases principales, entonces el segundo tipo no es necesario. Como:
function supportsInterface(bytes4 _interfaceId) public view override(ERC721, ERC721Enumerable) //just make sure this list is complete returns (bool) { return super.supportsInterface(_interfaceId); }
La implementación correcta de ERC-165 es opcional, pero importante. Desea que sus tokens sean compatibles con tantos otros sistemas (como intercambios) como sea posible, incluidos los futuros que aún no se han implementado. El estándar ERC-165 probablemente se volverá más utilizado e importante a medida que pase el tiempo y el espacio madure.
Su token ERC721 puede ser muy estándar y puede usar todas las clases y bibliotecas principales de terceros con muy poca personalización, y puede saber que ese código de terceros está bien probado y es seguro. Pero aún necesita probar a fondo su código, porque solo tiene una oportunidad de hacerlo bien antes de que se implemente en la red principal, lo que le dará gloria o vergüenza para siempre.
En primer lugar, por supuesto, las pruebas unitarias . En mi opinión, el marco de prueba que utilice no es importante; Yo uso casco con éteres y moka. Para mí, la única parte importante es que la cobertura de prueba y la cobertura de casos felices, casos excepcionales y casos extremos es amplia y profunda. A pesar de que puede estar probando código (por ejemplo, OpenZeppelin) que ya se ha probado bien, (a) su código personalizado puede haber roto algunos de esos casos, por lo que deben volver a probarse, y (b) OpenZeppelin ha tenido errores antes, y pueden volver a hacerlo en el futuro. Para ahorrarle algo de tiempo, puede tener un conjunto estándar de pruebas para todos los tokens ERC721, todos los tokens ERC20, todos los tokens ERC1155, etc. que puede reutilizar de un proyecto a otro. Esto es bueno. Luego puede agregar casos para cada proyecto para cubrir cualquier personalización del estándar; esto ahorrará tiempo. Las pruebas unitarias deben cubrir el control de acceso, la funcionalidad básica (como la acuñación y la transferencia), la pausabilidad (si su contrato es pausable), la implementación del estándar ERC165 y más. Puede probar su cobertura utilizando solidity-coverage (un paquete nodejs).
Finalmente, las herramientas automatizadas pueden brindarle una gran ayuda en las pruebas. Slither , Manticore y Mythril son estándares de la industria, generalmente utilizados por los principales nombres en auditoría de seguridad como Consensys y Certik. Solidity-coverage (un paquete de nodejs) le indicará los porcentajes estimados de cobertura que proporcionan sus pruebas unitarias (muy útil como regla general). Solgraph es una herramienta que puede ayudarlo a ver las relaciones y conexiones en el código del contrato; útil en la planificación de pruebas. Echidna también es útil; es una herramienta de prueba de fuzzing. Personalmente, uso una metodología de prueba primero cuando corresponde. Esto asegura una buena cobertura de prueba y el conjunto de pruebas se vuelve similar a una especificación de proyecto. Me encanta una buena cobertura de prueba.
> pip3 install slither-analyzer > pip3 install mythril > npm install solidity-coverage
Entonces, para resumir:
La verificación del código de contrato en etherscan es una gran característica. Ver la marca de verificación verde en la pestaña Contrato y poder ver el código en sí, con la etiqueta "coincidencia exacta", emana confianza y responsabilidad. Cuando alguien está viendo su contrato por primera vez, tratando de evaluar los riesgos relativos de uso o inversión, esto solo puede ayudar. Y esto es en general, para todos los contratos de todo tipo, no solo NFT.