ERC721 (o ERC-721) es el estándar de Ethereum más utilizado para tokens no fungibles. Asocia un número único con una dirección de Ethereum, denotando así que esa dirección es propietaria del número único: el NFT.
En efecto, no faltan tutoriales que cubran el diseño de este famoso token; sin embargo, hemos descubierto que muchos desarrolladores, incluso los más experimentados, no tienen un conocimiento completo de las especificaciones — y, a veces, de los problemas de seguridad. Por lo tanto, documentamos el estándar aquí con un énfasis en las áreas que los desarrolladores más experimentados pasan por alto.
Al final se proporcionan problemas de práctica para evaluar los casos extremos menos conocidos.
Tabla de Contenidos
- ¿Qué hace únicos a los NFTs?
- La propiedad y la función
ownerOf - Proceso de acuñación
- Transfiriendo NFTs con
transferFrom - Entendiendo la función
balanceOf - Aprobaciones ilimitadas: funciones
setApprovalForAlleisApprovedForAll - Aprobaciones específicas con las funciones
approveygetApproved - Identificando los NFTs en propiedad sin la extensión enumerable
- Transferencias Seguras:
safeTransferFrom,_safeMint, y la funciónonERC721Received safeTransferFromcon data y por qué existe - Casos de Uso Prácticos y Eficiencia- Consideraciones de gas para
_safeMintysafeTransferFromvs_mintytransferFrom - La función
burny la Destrucción de NFTs - Implementaciones de ERC721
- Pon a prueba tus conocimientos
¿Qué hace únicos a los NFTs?
Los NFTs se identifican de manera única mediante tres valores (chain id, contract address, id).
Poseer un NFT significa poseer un uint256 almacenado en un contrato ERC721 en una cadena EVM en particular.
Profundizaremos en las funciones que componen la especificación ERC721 y facilitan su comportamiento, incluyendo funciones principales y auxiliares. Estas son:
ownerOf: Mapeo de Propiedad- mint: Creación del Token
transferFrom: Transferencia de PropiedadbalanceOf: Recuento de PropiedadsetApprovalForAll&isApprovedForAll: Delegación de Derechos de Transferenciaapprove&getApproved: Mecanismo de Aprobación de un Solo NFTsafeTransferFrom&_safeMint: Funciones de Transferencia Seguraburn: Destrucción de NFT
La propiedad y la función ERC721 ownerOf
La propiedad es solo un mapeo: ownerOf(uint256 id)
En su núcleo, un ERC721 es solo un mapeo de un uint256 (el id del NFT) a la dirección del propietario. A pesar de todo el revuelo sobre los NFTs, no son más que tablas hash glorificadas. “Poseer” un NFT significa que existe un mapeo que tiene un cierto id como clave y tu dirección como valor. Eso es todo.
La especificación requiere una función pública que, dado un id, devuelva la dirección de un propietario.
En aras de la simplicidad, usaremos una variable pública en lugar de una función pública. Desde el exterior, la interacción es idéntica.
contract ERC721 {
mapping(uint256 => address) public ownerOf;
}
La función (o mapeo público) ownerOf toma el id del NFT y devuelve la dirección que lo posee.
Proceso de acuñación con la función mint
Dado que el valor predeterminado de un mapeo es 0, por defecto, la dirección cero “posee” todos los NFTs, pero no es así como generalmente interpretamos esto. Si ownerOf devuelve la dirección cero, decimos que el NFT no existe. La acuñación es la forma en que los tokens cobran vida.
Mint no forma parte de la especificación ERC721, se deja en manos del usuario definir cómo se acuñan los NFTs. No hay ningún requisito de que los NFTs se acuñen en la secuencia 0,1,2,3, etc. Podríamos acuñarle a alguien un NFT basado en el hash del número de bloque con su dirección o algo similar. En la siguiente implementación, cualquiera puede acuñar cualquier id siempre y cuando no haya sido acuñado antes.
contract ERC721 {
mapping(uint256 id => address owner) public ownerOf;
event Transfer(address indexed from, address indexed to, uint256 indexed id);
function mint(address recipient, uint256 id) public {
require(ownerOf[id] == address(0), "already minted");
ownerOf[id] = recipient;
emit Transfer(address(0), recipient, id);
}
}
Puede parecer curioso tener un evento Transfer que va desde address(0) hasta el receptor, pero así es la especificación.
Transfiriendo NFTs con la función ERC721 transferFrom
Naturalmente, queremos una forma de mover nuestro NFT a otra dirección. La función transferFrom logra esto.
contract ERC721 {
mapping(uint256 id => address owner) public ownerOf;
event Transfer(address indexed from, address indexed to, uint256 indexed id);
// mint hidden for readability
function transferFrom(address from, address to, uint256 id) external payable {
require(ownerOf[id] == msg.sender, "not allowed to transfer");
ownerOf[id] = to;
emit Transfer(from, to, id);
}
}
Puede parecer extraño que transferFrom sea payable, pero eso es lo que dice la especificación del EIP 721. Presumiblemente, esto es para permitir aplicaciones que requieran el pago de Ether para adquirir un NFT que ya ha sido acuñado. Muchas implementaciones no siguen esta parte de la especificación, y esta característica se usa muy raramente.
Además, ¿por qué tenemos un campo from si solo permitimos que msg.sender sea el from? Llegaremos a esto cuando hablemos de las aprobaciones. Por ahora, debería ser obvio que el propietario debe poder transferir un id que le pertenece.
Entendiendo la función ERC721 balanceOf
La especificación ERC721 requiere que llevemos un registro de cuántos NFTs posee una dirección por contrato.
ERC721 contiene un mapeo mapping(address owner => uint256 balances) balanceOf.
Nuestro NFT mínimo ahora tiene la siguiente funcionalidad que se muestra en el código a continuación.
Cabe destacar que balanceOf solo indica cuántos NFTs posee una dirección, no dice cuáles. Necesitamos actualizar las funciones donde los balances pueden cambiar, que por supuesto son mint y transfer. Los lugares donde actualizamos esas funciones han sido resaltados

Aquí hay otra advertencia: un propietario puede transferir NFTs a voluntad, por lo que debes tener mucho cuidado al depender de balanceOf al tomar decisiones en un contrato inteligente. No trates a balanceOf() como un valor estático, ya que puede cambiar durante el transcurso de la transacción; si el propietario transfiere un NFT a sí mismo desde otra dirección, o transfiere el NFT a otra dirección que posee, puede manipular la función balanceOf().
Aprobaciones ilimitadas: funciones ERC721 setApprovalForAll e isApprovedForAll
La especificación ERC721 permite a un propietario de un NFT ceder el control del NFT a otra dirección sin transferirle el NFT. El primer mecanismo para hacer esto es con la función setApprovalForAll(). Como su nombre indica, permite a otra dirección transferir NFTs en nombre del propietario. Esto aplica a cualquier NFT que la dirección posea. La contraparte isApprovedForAll() comprueba si a una cierta dirección, llamada el operador, se le ha delegado autoridad por parte de un propietario.
Un owner puede tener múltiples operadores. Este es un mecanismo mediante el cual el mismo NFT puede estar a la venta en múltiples mercados de NFTs. Si los mercados están aprobados para la dirección del propietario, pueden transferirlo a un comprador si este paga la cantidad correcta de Ether.

TransferFrom ahora permite tanto al propietario como a una dirección que tiene _approvedForAll transferir el token.
Aprobaciones específicas de token id con las funciones ERC721 approve y getApproved
En lugar de aprobar otra dirección para poder transferir cada NFT que posees, puedes aprobarlos para un solo id, lo cual es generalmente más seguro. Esto se coloca en el mapeo público getApproved().
A diferencia de isApprovedForAll, estar aprobado para un NFT no tiene nada que ver con la dirección del propietario, solo está asociado con el id.
Después de una transferencia, el nuevo propietario probablemente no querrá que alguien más tenga aprobación sobre ese id. Por lo tanto, la función transferFrom necesita ser actualizada para borrar esa aprobación.
Una limitación de approve es que solo se puede aprobar una dirección por id. Si queremos aprobar múltiples direcciones, sería muy costoso eliminarlas todas durante una transferencia.
Ten en cuenta que si una dirección es approvedForAll, entonces puede ejecutar approve hacia otra dirección para los ids propiedad de la dirección de la cual es operador. Nada ha cambiado en la función setApprovalForAll().

Después de la transferencia, las aprobaciones se borran porque el nuevo propietario, por lo general, no querrá que la dirección anterior tenga aprobación sobre el id.
Casi hemos terminado de implementar cada función que requiere la especificación ERC721. Las restantes requieren mucha más documentación.
Identificando los NFTs en propiedad sin la extensión enumerable
Determinando una lista de ids en propiedad
Usando los métodos anteriores, ¿existe una forma eficiente de determinar qué NFTs posee una dirección?
No la hay.
La función balanceOf solo nos dice cuántos NFTs posee una dirección, y ownerOf solo nos dice quién posee un id en particular. En teoría, podríamos iterar sobre todos los ids para averiguar cuáles posee una dirección en particular, pero esto no es eficiente.
Sin la extensión enumerable, no existe una forma eficiente de determinar puramente on-chain qué NFTs posee una dirección.
Abordaremos la extensión enumerable más adelante, pero ¿cómo procedemos sin ella?
Si un contrato necesita saber que 0xc0ffee… posee los ids 5, 7 y 21, la solución es decirle al contrato que 0xc0ffee… posee esos ids, y luego el contrato verifica si de hecho es cierto.
function checkOwnership(uint256[] calldata ids, address claimedOwner) public {
for (uint256 i = 0; i < ids.length; i++) {
require(nft.ownerOf(ids[i]) == claimedOwner, "not the claimed owner");
}
// rest of the logic
}
Pero ¿cómo determinamos eficientemente off-chain que 0xc0ffee… posee el 5, 7 y 21? Podríamos iterar a través de todos los ids y llamar a ownerOf(), pero eso haría rico a nuestro proveedor de RPC.
Analizando los Eventos ERC721
Aquí hay un código de ejemplo usando web3 js para rastrear qué NFTs son propiedad de una dirección. Ten en cuenta que el código escanea eventos desde el bloque 0, lo cual no es eficiente. Deberías elegir un valor reciente más sensato.
gist.github.com/RareSkills/5d60ad42cdd81b6e136605a832ba59ee
Transferencias Seguras: safeTransferFrom, _safeMint, y la función onERC721Received
La intención de safeTransferFrom y _safeMint es manejar los NFTs que se quedan atascados en un contrato. Si un NFT se transfiere a un contrato que no tiene la capacidad de llamar a transferFrom por sí mismo, entonces el NFT quedará “bloqueado” en el contrato, destruyéndolo efectivamente.
Para evitar que esto suceda, ERC-721 solo quiere transferir a contratos que tengan un mecanismo para poder transferir el NFT más adelante. Un contrato se marca como capaz de “manejar” NFTs si tiene una función onERC721Received() que devuelve el valor mágico bytes4 0x150b7a02. Este es el selector de función de onERC721Received() que se muestra a continuación. (Un selector de función es el identificador interno de Solidity para las funciones).
interface IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}
Aquí hay un ejemplo mínimo de un contrato usando esa interfaz:
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
contract MinimaExample is IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4) {
return IERC721Receiver.onERC721Received.selector; // returns 0x150b7a02
}
}
safeTransferFrom se comporta exactamente como transferFrom. Bajo el capó, llama a transferFrom y luego comprueba si la dirección receptora es un contrato inteligente.
- Si no lo es, no hace ningún paso adicional
- Si lo es
- Intenta llamar a la función
onERC721Received()con los argumentos anteriores en el contrato que recibe el NFT - Si la llamada a la función se revierte o no se devuelve 0x150b7a02, se revierte
- Intenta llamar a la función
¿Por qué comprobar el selector de función?
Comprobar si onERC721Received() no revirtió no es lo suficientemente bueno para determinar si un contrato puede manejar correctamente los tokens ERC721.
Si un NFT se transfiere a un contrato inteligente con una función fallback, y no se comprueba el valor de retorno, la transacción no revertirá. Sin embargo, el contrato probablemente no tenga un mecanismo para manejar la recepción de NFTs solo porque tiene una función fallback.
Argumentos de función de onERC721Received
Cuando se llama a onERC721Received, se le pasan los siguientes argumentos, los cuales se describen a continuación
interface IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}
operator:
Operator es msg.sender desde la perspectiva de safeTransfer. Podría ser el propietario del NFT o una dirección aprobada para transferir ese NFT.
from:
From es el propietario del NFT. Los parámetros from y operator serán iguales si el propietario es quien está llamando a la transferencia.
tokenId:
El id del NFT que se está transfiriendo.
data:
Si safeTransferFrom fue llamada con data, esta se reenvía al contrato receptor. El parámetro data se discutirá en una sección posterior.
Consideraciones de seguridad de onERC721Received
Siempre comprueba msg.sender en onERC721Received
Por defecto, cualquiera puede llamar a onERC721Received() con parámetros arbitrarios, engañando al contrato para que piense que ha recibido un NFT que no tiene. Si tu contrato usa onERC721Received(), ¡debes comprobar que msg.sender es el contrato NFT que esperas!
Reentrada en safeTransfer
SafeTransfer y _safeMint ceden el control de ejecución a un contrato externo. Ten cuidado al usar safeTransfer para enviar un NFT a una dirección arbitraria, el receptor puede poner cualquier lógica que desee en la función onERC721Received(), lo que posiblemente conduzca a una reentrada. Si te defiendes adecuadamente contra la reentrada, esto no tiene por qué ser una preocupación.
Denegación de servicio en safeTransfer
Un receptor malicioso puede forzar la reversión de las transacciones revertiendo dentro de onERC721Received() o usando un bucle para consumir todo el gas. No debes asumir que safeTransferFrom a una dirección arbitraria tendrá éxito.
safeTransferFrom con data y por qué existe - Casos de Uso Prácticos y Eficiencia
ERC721 especifica que existen dos funciones safeTransferFrom:
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata data) external payable;
La segunda tiene un parámetro data adicional. El siguiente ejemplo demostrará el uso del parámetro data con onERC721Received().
Staking eficiente en gas, omitiendo la aprobación
Un patrón muy común es depositar un NFT en un contrato con el propósito de hacer staking. Por supuesto, el NFT no está “dentro” del contrato inteligente, sino que el ownerOf de ese id en particular es el contrato de staking, y el contrato de staking lleva un registro para rastrear al propietario original.
Una forma común, pero ineficiente de hacerlo se muestra en el siguiente fragmento de código. La razón por la que es ineficiente es porque requiere que el usuario llame a approve sobre Staking antes de llamar a deposit(). Hemos añadido la opción de votar mientras se hace staking como un ejemplo de adición de parámetros durante una transferencia.
contract Staking {
struct Stake {
uint8 voteId;
address originalOwner;
}
mapping(uint256 id => Stake stake) public stakes;
function deposit(uint256 id, uint8 _voteId) external {
stakes[id] = Stake({voteId: _voteId, originalOwner: msg.sender});
// user must approve Staking contract first
nft.transferFrom(msg.sender, address(this), id);
}
function withdraw(uint256 id) external {
require(msg.sender == staked[id].originalOwner, "not original owner");
delete stakes[id];
nft.transferFrom(address(this), msg.sender, id);
}
}
Una alternativa más eficiente en gas es simplemente hacer un safeTransfer para mover el activo hacia adentro. Esto permite al usuario omitir el paso de approve. Por supuesto, esto debe ser manejado por la aplicación frontend para reducir los errores del usuario. Ten en cuenta que el parámetro vote ahora está contenido en el argumento data.
contract ImprovedStaking is IERC721Receiver {
struct Stake {
uint8 voteId;
address originalOwner;
}
mapping(uint256 id => Stake stake) public stakes;
function onERC721Received(address operator, address from, uint256 id, bytes calldata data) external {
// important safety to check only allow calls from our intended NFT
require(msg.sender == address(nft), "wrong NFT");
uint8 voteId = abi.decode(data, (uint8));
originalOwners[id] = from; // from is the original owner
}
function withdraw(uint256 id) external {
address originalOwner = stakes[id].originalOwner;
require(msg.sender == originalOwner, "not owner");
delete stakes[id];
nft.transferFrom(address(this), msg.sender, id);
}
}
De nuevo, es extremadamente importante imponer que msg.sender sea el contrato NFT en onERC721Received; de lo contrario, cualquiera puede llamar a la función y proporcionarle datos maliciosos.
El ejemplo anterior ilustra cómo puede ser útil el parámetro data. El parámetro bytes calldata data nos da la flexibilidad de codificar cualquier dato que nos interese. Solo incluimos un uint8 voteId, pero si también quisiéramos añadir intendedDuration, delegate y otros parámetros, podemos hacer
(voteId, intendedDuration, delegate) = abi.decode(data, (uint8, uint256, address).
Consideraciones de gas para _safeMint y safeTransferFrom vs _mint y transferFrom
Si esperas que el receptor sea un EOA, entonces es preferible usar transferFrom o _mint porque comprobar si son un contrato (lo que hacen _safeMint y safeTransferFrom) sería un desperdicio de gas.
La función burn y la Destrucción de NFTs
Un NFT puede ser quemado transfiriéndolo a la dirección cero. Poder quemar NFTs no forma parte oficialmente de la especificación ERC, por lo que no se requiere que los contratos soporten esta operación.
Implementaciones de ERC721
La implementación de OpenZeppelin es la librería más amigable para desarrolladores principiantes y es ideal si se usa con el resto de los contratos actualizables. Los desarrolladores más experimentados deberían considerar la implementación de Solady ERC721 ya que ofrecerá ahorros considerables de gas.
Pon a prueba tus conocimientos
Debido a que ERC721 es tan ubicuo, los desarrolladores serios de Solidity deberían entender el protocolo por completo y ser capaces de implementar uno desde cero de memoria. Para ver si entendiste todo, intenta resolver los siguientes ejercicios de seguridad para ERC721:
Overmint 1 (RareSkills Riddles)
Overmint 2 (RareSkills Riddles)
Diamond Hands (RareSkills Riddles)
Jpeg Sniper (Mr Steal Yo Crypto)
Sigue aprendiendo: ERC721 Enumerable
La extensión Enumerable para ERC721 permite a un contrato inteligente listar todos los NFTs propiedad de una dirección. Consulta nuestro artículo sobre ERC721 Enumerable para continuar aprendiendo.
Aprende más con RareSkills
Por favor, consulta nuestro Solidity bootcamp líder en la industria para aprender más sobre el programa.
Publicado originalmente el 8 de noviembre de 2023