Una explicación del contrato ERC-20
Introducción
Uno de los usos más comunes para Ethereum es que un grupo cree un token intercambiable, en cierto sentido su propia moneda. Estos tókenes normalmente siguen un estándar, el ERC-20. Este estándar permite escribir herramientas, como reservas de liquidez y carteras, que funcionan con todos los tókenes ERC-20. En este artículo analizaremos la implementación de OpenZeppelin Solidity ERC20(opens in a new tab), así como la definición de interfaz(opens in a new tab).
Este es un código fuente anotado. Si quiere implementar ERC-20, lea este tutorial(opens in a new tab).
La Interfaz
El propósito de un estándar como ERC-20 es permitir la implementación de muchos tókenes y que sean interoperables a través de aplicaciones, como carteras e intercambios descentralizados. Para lograr esto, creamos una interfaz(opens in a new tab). Cualquier código que necesite usar el contrato de tókenes puede usar las mismas definiciones en la interfaz y ser compatible con todos los contratos de token que lo usan, ya sea una cartera como MetaMask, una DApp como etherscan.io, o un contrato diferente como la reserva de liquidez.
Si usted es un programador experto, problablemente recuerde haber visto estructuras similares en Java(opens in a new tab) o incluso en archivos en encabezado C(opens in a new tab).
Esta es la deinición de la Interfaz ERC-20(opens in a new tab) de OpenZeppelin. Es una derivación del estándar legible por humanos(opens in a new tab) al código de Solidity. Por supuesto, esta interfaz por si sóla no define como hacer nada. Esto se explica en el código fuente del contrato a continuación.
1// SPDX-License-Identifier: MITCopiar
Se supone que los archivos de Solidity incluyen un identificador de licencia. Puede ver la lista de licencias aquí(opens in a new tab). Si necesita una licencia diferente, indíquelo en los comentarios.
1pragma solidity >=0.6.0 <0.8.0;Copiar
El lenguaje de Solidity sigue evolucionando rápidamente, y las nuevas versiones pueden que no sean compatibles con el antiguo código (ver aquí(opens in a new tab)). Por lo tanto, es una buena idea especificar no solo una versión mínima del lenguaje, sino también una versión máxima: la última con la que probara el código.
1/**2 * @dev Interface of the ERC20 standard as defined in the EIP.3 */Copiar
El @dev
en el comentario es parte del formato NatSpec(opens in a new tab), utilizado para producir documentación desde el código fuente.
1interface IERC20 {Copiar
Por convención, los nombres de interfaz comienzan por I
.
1 /**2 * @dev Returns the amount of tokens in existence.3 */4 function totalSupply() external view returns (uint256);Copiar
Esta función es external
, lo que significa que sólo puede ser activada desde fuera del contrato(opens in a new tab). Devuelve el suministro total de tókenes en el contrato. Este valor se devuelve utilizando el tipo más común en Ethereum, 256 bits sin firma (256 bits es el tamaño nativo de la palabra de la EVM). Esta función también es una view
, lo que significa que no cambia el estado, así que se puede ejecutar en un solo nodo en lugar de tener cada nodo en la cadena de bloques ejecutándolo. Este tipo de función no genera una transacción y no cuesta gas.
Nota: En teoría puede parecer que el creador de un contrato puede hacer trampas al devolver una oferta total menor que el valor real, haciendo que cada token parezca más valioso de lo que realmente es. Sin embargo, ese temor ignora la verdadera naturaleza de la cadena de bloques. Todo lo que pasa en la cadena de bloques puede verificarse en cada nodo. Para lograrlo, cada maquína de contrato, código de lenguaje y almacenamiento está disponible en cada nodo. Aunque no está obligado a publicar el código de Solidity para su contrato, nadie le tomaría en serio, a menos que publicase el código fuente y la versión de Solidity con la que se compiló, para que pueda verificarlo con respecto al código de lenguaje de la máquina que proporcionó. Por ejemplo, vea este contrato(opens in a new tab).
1 /**2 * @dev Returns the amount of tokens owned by `account`.3 */4 function balanceOf(address account) external view returns (uint256);Copiar
Como indica su nombre, saldoDe
devuelve el saldo de una cuenta. Las cuentas de Ethereum son identificadas en Solidity usando el tipo de dirección
, el cual contiene 160 bits. También es externo
y vista
.
1 /**2 * @dev Moves `amount` tokens from the caller's account to `recipient`.3 *4 * Returns a boolean value indicating whether the operation succeeded.5 *6 * Emits a {Transfer} event.7 */8 function transfer(address recipient, uint256 amount) external returns (bool);Copiar
La función transfer
transfiere tókenes de la persona que lo invoca a una dirección diferente. Esto incluye un cambio de estado, así que no es una vista
. Cuando un usuario llama está función genera una transacción y cuesta gas. También emite un evento, Transferir
, para informar a todos en la cadena de bloques del evento.
La función tiene dos resultados distintos, una para cada tipo de activación:
- Usuarios que invocan la función directamente desde una interfaz de usuario. Normalmente, el usuario envía una transacción y no espera una respuesta, lo que podría tomar una cantidad indefinida de tiempo. El usuario puede ver lo que ocurrió buscando el recibo de la transacción (que se identifica por el hash de la transacción) o buscando el evento
Transferir
. - Otros contratos, que invocan la función como parte de una transacción general. Esos contratos obtienen resultados inmediatos, porque se ejecutan en la misma transacción, así que pueden usar el valor de retorno de la función.
El mismo tipo de resultados lo obtienen las otras funciones que cambian el estado del contrato.
Las licencias permiten que una cuenta utilice algunos tókenes que pertenecen a un propietario diferente. Esto es útil, por ejemplo, para los contratos que actúan como vendedores. Los contratos no pueden controlar eventos, así que si un comprador transfiriera tókenes al contrato del vendedor directamente ese contrato no sabría si se ha pagado. En cambio, el comprador permite al contrato de vendedor utilizar una cierta cantidad, y el vendedor transfiere esa cantidad. Esto se hace a través de una función que invoca el contrato de vendedor, por lo que el contrato de vendedor puede saber si ha salido bien.
1 /**2 * @dev Returns the remaining number of tokens that `spender` will be3 * allowed to spend on behalf of `owner` through {transferFrom}. This is4 * zero by default.5 *6 * This value changes when {approve} or {transferFrom} are called.7 */8 function allowance(address owner, address spender) external view returns (uint256);Copiar
La función allowance
permite a cualquiera consultar cuál es la asignación que una dirección (owner
) permite que otra dirección (spender
) se utilice.
1 /**2 * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.3 *4 * Returns a boolean value indicating whether the operation succeeded.5 *6 * IMPORTANT: Beware that changing an allowance with this method brings the risk7 * that someone may use both the old and the new allowance by unfortunate8 * transaction ordering. One possible solution to mitigate this race9 * condition is to first reduce the spender's allowance to 0 and set the10 * desired value afterwards:11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-26352472912 *13 * Emits an {Approval} event.14 */15 function approve(address spender, uint256 amount) external returns (bool);Mostrar todoCopiar
La función approve
crea una asignación. Asegúrate de leer el mensaje sobre cómo se puede abusar de él. En Ethereum usted controla el orden de sus propias transacciones, pero no puede controlar el orden en el que se ejecutarán las transacciones de otras personas, a menos que no envíe su propia transacción hasta que vea que se ha producido la transacción del otro lado.
1 /**2 * @dev Moves `amount` tokens from `sender` to `recipient` using the3 * allowance mechanism. `amount` is then deducted from the caller's4 * allowance.5 *6 * Returns a boolean value indicating whether the operation succeeded.7 *8 * Emits a {Transfer} event.9 */10 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);Mostrar todoCopiar
Por último, transferirDesde
lo utiliza el gastador para utilizar realmente la asignación.
12 /**3 * @dev Emitted when `value` tokens are moved from one account (`from`) to4 * another (`to`).5 *6 * Note that `value` may be zero.7 */8 event Transfer(address indexed from, address indexed to, uint256 value);910 /**11 * @dev Emitted when the allowance of a `spender` for an `owner` is set by12 * a call to {approve}. `value` is the new allowance.13 */14 event Approval(address indexed owner, address indexed spender, uint256 value);15}Mostrar todoCopiar
Estos eventos se emiten cuando cambia el estado del contrato ERC-20.
El contrato real
Este es el contrato real que implementa el estándar ERC-20, tomado desde aquí(opens in a new tab). No está pensado para ser usado tal cual, pero puede heredarlo(opens in a new tab) para pasarlo a algo utilizable.
1// SPDX-License-Identifier: MIT2pragma solidity >=0.6.0 <0.8.0;Copiar
Importar declaraciones
Además de las definiciones de interfaz de arriba, la definición del contrato importa otros dos archivos:
12import "../../GSN/Context.sol";3import "./IERC20.sol";4import "../../math/SafeMath.sol";Copiar
GSN/Context.sol
reúne las definiciones que se necesita utilizar OpenGSN(opens in a new tab): un sistema que permite a los usuarios sin ether usar la cadena de bloques. Tenga en cuenta que esta es una versión antigua, si desea integrar con OpenGSN utilice este tutorial(opens in a new tab).- La biblioteca SafeMath(opens in a new tab)que se utiliza para añadir y restar sin excesos. Esto es necesario, porque de lo contrario una persona podría tener un token, gastar dos tókenes y luego tener 2^256-1 tókenes.
Este comentario explica la finalidad del contrato.
1/**2 * @dev Implementation of the {IERC20} interface.3 *4 * This implementation is agnostic to the way tokens are created. This means5 * that a supply mechanism has to be added in a derived contract using {_mint}.6 * For a generic mechanism see {ERC20PresetMinterPauser}.7 *8 * TIP: For a detailed writeup see our guide9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How10 * to implement supply mechanisms].11 *12 * We have followed general OpenZeppelin guidelines: functions revert instead13 * of returning `false` on failure. This behavior is nonetheless conventional14 * and does not conflict with the expectations of ERC20 applications.15 *16 * Additionally, an {Approval} event is emitted on calls to {transferFrom}.17 * This allows applications to reconstruct the allowance for all accounts just18 * by listening to said events. Other implementations of the EIP may not emit19 * these events, as it isn't required by the specification.20 *21 * Finally, the non-standard {decreaseAllowance} and {increaseAllowance}22 * functions have been added to mitigate the well-known issues around setting23 * allowances. See {IERC20-approve}.24 */25Mostrar todoCopiar
Composición del contrato
1contract ERC20 is Context, IERC20 {Copiar
Esta línea especifica la herencia, en este caso de IERC20
desde arriba y Context
para OpenGSN.
12 using SafeMath for uint256;3Copiar
Esta línea une la biblioteca SafeMath
al tipo uint256
. Puede encontrar esta biblioteca aquí(opens in a new tab).
Definiciones de variables
Estas definiciones especifican las variables de estado del contrato. Hay variables declaradas commo privadas
, pero eso solo significa que otros contratos en la cadena de bloques no pueden leerlas. No hay secretos en la cadena de bloques, el software en cada nodo tiene el estado de cada contrato en cada bloque. Convencionalmente, a las variables de estado se les llama _<something>
.
Las primeras dos variables son mapeos(opens in a new tab), es decir, que se comportan aproximadamente igual que matrices asociativas(opens in a new tab), con la salvedad de que las claves son valores numéricos. El almacenamiento solo se asigna para entradas que tienen valores diferentes del predeterminado (cero).
1 mapping (address => uint256) private _balances;Copiar
El primer mapeo, _balances
, son direcciones y sus respectivos balances de este token. Para acceder al saldo, utilice esta frase: _balances[<address>]
.
1 mapping (address => mapping (address => uint256)) private _allowances;Copiar
Esta variable, _allowances
, almacena las asignaciones explicadas anteriormente. El primer índice es el propietario de los tókenes, y el segundo es el contrato con la asignación. Para acceder a la dirección A puede gastar desde la dirección B de la cuenta, utilice _allowances[B][A]
.
1 uint256 private _totalSupply;Copiar
Como el nombre sugiere, esta variable mantiene un seguimiento del suministro total de tókenes.
1 string private _name;2 string private _symbol;3 uint8 private _decimals;Copiar
Estas tres variables se utilizan para mejorar la legibilidad. Las dos primeras son autoexplicativas, pero _decimals
no lo es.
Por un lado, ethereum no tiene variables de punto flotante o fraccionales. Por otro lado, a los humanos les gusta poder dividir tókenes. Una de las razones por las que la gente se decantó por el oro como moneda fue que era difícil hacer intercambios cuando alguien quería comprar «gato por liebre».
La solución es llevar un registro de enteros, pero cuenta en lugar del token real un token fraccional que es cercano a no valer nada. En el caso del ether, el token fraccional se llama wei, y 10^18 wei es igual a un ETH. Al cierre de este artículo, 10.000.000.000.000 wei es aproximadamente un centavo de dólar estadounidense o euro.
Las aplicaciones necesitan saber cómo mostrar el saldo de tókenes. Si un usuario tiene 3.141.000.000.000.000.000 wei, ¿es eso 3,14 ETH? 31.41 ETH? ¿3,141 ETH? En el caso del ether se define 10^18 wei para el ETH, pero para su token puede seleccionar un valor diferente. Si dividir el token no tiene sentido, puede usar un valor de _decimals
de cero. Si desea utilizar el mismo estándar que ETH, utilice el valor 18.
El constructor
1 /**2 * @dev Sets the values for {name} and {symbol}, initializes {decimals} with3 * a default value of 18.4 *5 * To select a different value for {decimals}, use {_setupDecimals}.6 *7 * All three of these values are immutable: they can only be set once during8 * construction.9 */10 constructor (string memory name_, string memory symbol_) public {11 _name = name_;12 _symbol = symbol_;13 _decimals = 18;14 }Mostrar todoCopiar
Se le llama constructor cuando se crea el contrato por primera vez. Convencionalmente, los parámetros de la función se llaman <something>_
.
Funciones de la interfaz de usuario
1 /**2 * @dev Returns the name of the token.3 */4 function name() public view returns (string memory) {5 return _name;6 }78 /**9 * @dev Returns the symbol of the token, usually a shorter version of the10 * name.11 */12 function symbol() public view returns (string memory) {13 return _symbol;14 }1516 /**17 * @dev Returns the number of decimals used to get its user representation.18 * For example, if `decimals` equals `2`, a balance of `505` tokens should19 * be displayed to a user as `5,05` (`505 / 10 ** 2`).20 *21 * Tokens usually opt for a value of 18, imitating the relationship between22 * ether and wei. This is the value {ERC20} uses, unless {_setupDecimals} is23 * called.24 *25 * NOTE: This information is only used for _display_ purposes: it in26 * no way affects any of the arithmetic of the contract, including27 * {IERC20-balanceOf} and {IERC20-transfer}.28 */29 function decimals() public view returns (uint8) {30 return _decimals;31 }Mostrar todoCopiar
Estas funciones, name
, symbol
y decimals
ayudan a las interfaces de usuario a conocer su contrato, para que puedan mostrarlo correctamente.
El tipo de retorno es memoria de cadena
, lo que significa que devuelve una cadena que se almacena en la memoria. Las variables, como cadenas, pueden almacenarse en tres ubicaciones:
Tiempo de vida | Acceso al contrato | Coste del gas | |
---|---|---|---|
Memoria | Activación de una función | Leer/Escribir | Decenas o centenas (más altas para ubicaciones más altas) |
Calldata | Activación de una función | Sólo lectura | No se puede utilizar como tipo de retorno, solo un tipo de parámetro de función |
Almacenamiento | Hasta que cambie | Leer/Escribir | Alta (800 para leer, 20.000 para escribir) |
En este caso, memory
es la mejor opción.
Leer información del token
Estas son funciones que proporcionan información sobre el token, ya sea el suministro total o el saldo de una cuenta.
1 /**2 * @dev See {IERC20-totalSupply}.3 */4 function totalSupply() public view override returns (uint256) {5 return _totalSupply;6 }Copiar
La función totalSupply
devuelve el suministro total de tókenes.
1 /**2 * @dev See {IERC20-balanceOf}.3 */4 function balanceOf(address account) public view override returns (uint256) {5 return _balances[account];6 }Copiar
Leer el saldo de una cuenta. Ten en cuenta que cualquiera puede obtener el saldo de otra persona. No tiene sentido intentar ocultar esta información, porque está disponible en cada nodo de todos modos. No hay secretos en la cadena de bloques.
Transferir tókenes
1 /**2 * @dev See {IERC20-transfer}.3 *4 * Requirements:5 *6 * - `recipient` cannot be the zero address.7 * - the caller must have a balance of at least `amount`.8 */9 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {Mostrar todoCopiar
La función transfer
se invoca para transferir tókenes desde la cuenta del remitente a otra diferente. Nótese que aunque devuelve un valor booleano, ese valor es siempre verdadero o true. Si la transferencia falla el contrato revierte la activación.
1 _transfer(_msgSender(), recipient, amount);2 return true;3 }Copiar
La función _transfer
hace el trabajo actual. Es una función privada que solo pueden activar otras funciones del contrato. Convencionalmente, a las funciones privadas se les llama _<something>
, al igual que las variables de estado.
Normalmente, en Sodity usamos msg.sender
para el remitente del mensaje. Sin embargo, eso rompe OpenGSN(opens in a new tab). Si queremos permitir transacciones sin etherless con nuestro token, necesitamos usar _msgSender()
. Devuelve msg.sender
para transacciones normales, pero para las transacciones si ether devuelve el firmante original y no el contrato que reenvió el mensaje.
Funciones de asignación
Estas son las funciones que implementan la funcionalidad de asignación: allowance
, approve
, transferFrom
y _approve
. Adicionalmente, la implementación de OpenZeppelin va más allá de los estándares básicos para incluir algo de funcionalidad que mejora la seguridad: increaseAllwance
y decreaseAllowance
.
La función de «allowance»
1 /**2 * @dev See {IERC20-allowance}.3 */4 function allowance(address owner, address spender) public view virtual override returns (uint256) {5 return _allowances[owner][spender];6 }Copiar
La función allowance
permite a todos revisar cualquier asignación.
La función de «approve»
1 /**2 * @dev See {IERC20-approve}.3 *4 * Requirements:5 *6 * - `spender` cannot be the zero address.7 */8 function approve(address spender, uint256 amount) public virtual override returns (bool) {Copiar
Esta función se invoca para crear una asignación. Es similar a la función transfer
que se encuentra más arriba:
- Esta función solo invoca una función interna (en este caso,
_approve
) que hace el verdadero trabajo. - La función devuelve
true
(si tiene éxito) o revierte (si no lo tiene).
1 _approve(_msgSender(), spender, amount);2 return true;3 }Copiar
Usamos funciones internas para minimizar la cantidad de lugares donde suceden los cambios de estado. Cualquier función que cambia el estado es un potencial riesgo de seguridad que necesita ser auditado por cuestiones de seguridad. De esta manera tenemos una menor probabilidad de hacerlo mal.
La función transferFrom
Esta es la función que un gastador llama para gastar en asignación. Esto requiere dos operaciones: transferir la cantidad gastada y reducir la asignación por esa cantidad.
1 /**2 * @dev See {IERC20-transferFrom}.3 *4 * Emits an {Approval} event indicating the updated allowance. This is not5 * required by the EIP. See the note at the beginning of {ERC20}.6 *7 * Requirements:8 *9 * - `sender` and `recipient` cannot be the zero address.10 * - `sender` must have a balance of at least `amount`.11 * - the caller must have allowance for ``sender``'s tokens of at least12 * `amount`.13 */14 function transferFrom(address sender, address recipient, uint256 amount) public virtual15 override returns (bool) {16 _transfer(sender, recipient, amount);Mostrar todoCopiar
La activación de la función a.sub(b, "message")
hace dos cosas. Primero, calcula a-b
, que es la nueva asignación. En segundo lugar, comprueba que este resultado no es negativo. Si es negativo la llamada se revierte con el mensaje proporcionado. Tenga en cuenta que cuando una activación revierte cualquier procesamiento realizado previamente durante esa activación se ignora, por lo tanto, no necesitamos deshacer la _transfer
.
1 _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,2 "ERC20: transfer amount exceeds allowance"));3 return true;4 }Copiar
Adiciones de seguridad a OpenZeppelin
Es peligroso establecer una asignación diferente de cero a otro valor distinto de cero, porque solo controla el orden de sus propias transacciones y no las de nadie más. Imagine que tiene dos usuarios: Alice que es ingenua y Bill que es un tramposo. Alice quiere algún servicio de Bill, que piensa que cuesta cinco tókenes, por lo que le da a Bill una asignación de cinco tókenes.
Entonces algo cambia y el precio de Bill sube a diez tókenes. Alice, quien todavía quiere el servicio, envía una transacción que establece la asignación de Bill a diez. En el momento en que Bill ve esta nueva transacción en el fondo de transacciones envía una transacción que gasta los cinco tókenes de Alice y tiene un mayor precio de gas por lo que se minará más rápido. De esa manera Bill puede gastar los primeros cinco tókenes y luego, una vez que se extraiga la nueva asignación de Alice gastará diez más por un precio total de quince tókenes. Más de lo que Alicia quería autorizar. A esta técnica se le llama anticiparse(opens in a new tab).
Transacción de Alice | Nonce de Alice | Transacción de Bill | Nonce de Bill | Asignación de Bill | Ingresos totales de Bill procedentes de Alice |
---|---|---|---|---|---|
approve(Bill, 5) | 10 | 5 | 0 | ||
transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
approve(Bill, 10) | 11 | 10 | 5 | ||
transferFrom(Alice, Bill, 10) | 10,124 | 0 | 15 |
Para evitar este problema. estas dos funciones (increaseAllowance
y decreaseAllowance
) le permiten modificar la autorización en una cantidad específica. Así que si Bill ya había gastado cinco tókenes, solo podrá gastar cinco más. Dependiendo del momento, hay dos maneras en las que esto puede funcionar y en ambas Bill acaba recibiendo solo diez tókenes:
A:
Transacción de Alice | Nonce de Alice | Transacción de Bill | Nonce de Bill | Asignación de Bill | Ingresos totales de Bill procedentes de Alice |
---|---|---|---|---|---|
approve(Bill, 5) | 10 | 5 | 0 | ||
transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
increaseAllowance(Bill, 5) | 11 | 0+5 = 5 | 5 | ||
transferFrom(Alice, Bill, 5) | 10,124 | 0 | 10 |
B:
Transacción de Alice | Nonce de Alice | Transacción de Bill | Nonce de Bill | Asignación de Bill | Ingresos totales de Bill procedentes de Alice |
---|---|---|---|---|---|
approve(Bill, 5) | 10 | 5 | 0 | ||
increaseAllowance(Bill, 5) | 11 | 5+5 = 10 | 0 | ||
transferFrom(Alice, Bill, 10) | 10,124 | 0 | 10 |
1 /**2 * @dev Atomically increases the allowance granted to `spender` by the caller.3 *4 * This is an alternative to {approve} that can be used as a mitigation for5 * problems described in {IERC20-approve}.6 *7 * Emits an {Approval} event indicating the updated allowance.8 *9 * Requirements:10 *11 * - `spender` cannot be the zero address.12 */13 function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {14 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue));15 return true;16 }Mostrar todoCopiar
La función a.add(n)
es una adición segura. En el caso poco probable de que a
+b
>=2^256
no se sume de la manera normal en que la adición lo hace.
12 /**3 * @dev Atomically decreases the allowance granted to `spender` by the caller.4 *5 * This is an alternative to {approve} that can be used as a mitigation for6 * problems described in {IERC20-approve}.7 *8 * Emits an {Approval} event indicating the updated allowance.9 *10 * Requirements:11 *12 * - `spender` cannot be the zero address.13 * - `spender` must have allowance for the caller of at least14 * `subtractedValue`.15 */16 function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {17 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue,18 "ERC20: decreased allowance below zero"));19 return true;20 }Mostrar todoCopiar
Funciones que modifican la información del token
Estas son las cuatro funciones que hacen el verdadero trabajo: _transfer
, _mint
, _burn
y _approve
.
La función _transfer {#_transfer}
1 /**2 * @dev Moves tokens `amount` from `sender` to `recipient`.3 *4 * This is internal function is equivalent to {transfer}, and can be used to5 * e.g. implement automatic token fees, slashing mechanisms, etc.6 *7 * Emits a {Transfer} event.8 *9 * Requirements:10 *11 * - `sender` cannot be the zero address.12 * - `recipient` cannot be the zero address.13 * - `sender` must have a balance of at least `amount`.14 */15 function _transfer(address sender, address recipient, uint256 amount) internal virtual {Mostrar todoCopiar
Esta función, _transfer
, transfiere tókenes de una cuenta a otra. La invocan ambas funciones transfer
(para transferencias desde la cuenta propia del emisor) y transferFrom
(para usar asignaciones que transfieran desde la cuenta de alguien más).
1 require(sender != address(0), "ERC20: transfer from the zero address");2 require(recipient != address(0), "ERC20: transfer to the zero address");Copiar
Actualmente, nadie poseé la dirección cero en Ethereum (es decir, nadie conoce una clave privada cuya clave pública conocida se transforma en la dirección cero). Cuando las personas usan esa dirección, usualmente es un error del programa, por lo que fallamos si la dirección cero es usada como el emisor o receptor.
1 _beforeTokenTransfer(sender, recipient, amount);2Copiar
Hay dos maneras de usar este contrato:
- Usarlo como plantilla para su propio código.
- Herédalo(opens in a new tab) y sobrescribir sólo aquellas funciones que necesite modificar.
El segundo método es mucho mejor pues el código ERC de OpenZeppelin ya ha sido auditado y demostrado ser seguro. Cuando utiliza la herencia queda claro cuáles son las funciones que modifica, y para confiar en su contrato, la gente sólo necesita auditar esas funciones específicas.
A menudo es útil realizar una función cada vez que los tókenes cambian de mano. Sin embargo,_transfer
es una función muy importante y es posible escribirla de forma insegura (ver abajo), así que lo mejor no anularlo. La solución es _beforeTokenTransfer
, una función de gancho(opens in a new tab). Puede anular esta función y se activará en cada transferencia.
1 _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");2 _balances[recipient] = _balances[recipient].add(amount);Copiar
Estas son las líneas que en realidad hacen la transferencia. Note que no hay nada entre ellas y que restamos la cantidad transferida desde el emisor antes de agregarla al receptor. Esto es importante, porque si se invocó un contrato diferente de por medio, este se pudo usar para engañar a este contrato. De esta manera la transferencia es atómica, nada puede suceder en medio.
1 emit Transfer(sender, recipient, amount);2 }Copiar
Finalmente, emite un evento Transfer
. Los eventos no son accesibles por los contratos inteligentes, pero el código en ejecución fuera de la cadena de bloques puede escuchar eventos y reaccionar a ellos. Por ejemplo, una billetera puede mantener un registro de cuando el propietario obtiene más tokens.
Las funciones _mint y _burn {#_mint-y-_burn}
Estas dos funciones (_mint
y _burn
) modifican el suministro total de tókenes. Son internas y no hay ninguna función que las invoque en este contrato, entonces sólo son útiles si las hereda desde un contrato y añade su propia lógica para decidir en qué condiciones quiere acuñar nuevos tóekens o quemar los existentes.
NOTA: cada token ERC-20 tiene su propia lógica de negocio que dicta la administración del token. Por ejemplo, un contrato de suministro fijo solo podría activar _mint
en el constructor y nunca activar _burn
. Un contrato que vende tókenes activará _mint
cuando se pague y, presumiblemente, active _burn
en cierto punto para evitar una inflación galopante.
1 /** @dev Creates `amount` tokens and assigns them to `account`, increasing2 * the total supply.3 *4 * Emits a {Transfer} event with `from` set to the zero address.5 *6 * Requirements:7 *8 * - `to` cannot be the zero address.9 */10 function _mint(address account, uint256 amount) internal virtual {11 require(account != address(0), "ERC20: mint to the zero address");12 _beforeTokenTransfer(address(0), account, amount);13 _totalSupply = _totalSupply.add(amount);14 _balances[account] = _balances[account].add(amount);15 emit Transfer(address(0), account, amount);16 }Mostrar todoCopiar
Asegúrese de actualizar _totalSupply
cuando la cantidad total de tókenes cambie.
1 /**2 * @dev Destroys `amount` tokens from `account`, reducing the3 * total supply.4 *5 * Emits a {Transfer} event with `to` set to the zero address.6 *7 * Requirements:8 *9 * - `account` cannot be the zero address.10 * - `account` must have at least `amount` tokens.11 */12 function _burn(address account, uint256 amount) internal virtual {13 require(account != address(0), "ERC20: burn from the zero address");1415 _beforeTokenTransfer(account, address(0), amount);1617 _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance");18 _totalSupply = _totalSupply.sub(amount);19 emit Transfer(account, address(0), amount);20 }Mostrar todo
La función _burn
es casi idéntica a _mint
, excepto que esta va en otra dirección.
La función _approve {#_approve}
Esta es la función que actualmente especifica asignaciones. Observe que esta permite especificar una asignación que es mayor al balance actual de la cuenta del propietario. Esto es correcto, porque el saldo se revisa en el momento de la transferencia y puede ser diferente del saldo cuando se creó la asignación.
1 /**2 * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.3 *4 * This internal function is equivalent to `approve`, and can be used to5 * e.g. set automatic allowances for certain subsystems, etc.6 *7 * Emits an {Approval} event.8 *9 * Requirements:10 *11 * - `owner` cannot be the zero address.12 * - `spender` cannot be the zero address.13 */14 function _approve(address owner, address spender, uint256 amount) internal virtual {15 require(owner != address(0), "ERC20: approve from the zero address");16 require(spender != address(0), "ERC20: approve to the zero address");1718 _allowances[owner][spender] = amount;Mostrar todoCopiar
Emite un evento Approval
. Dependiendo de cómo se escriba la aplicación, se le puede informar al contrato gastador sobre la aprobación, ya sea por el propietario o por un servidor que escucha a estos eventos.
1 emit Approval(owner, spender, amount);2 }3Copiar
Modificar la variable Decimals
123 /**4 * @dev Sets {decimals} to a value other than the default one of 18.5 *6 * WARNING: This function should only be called from the constructor. Most7 * applications that interact with token contracts will not expect8 * {decimals} to ever change, and may work incorrectly if it does.9 */10 function _setupDecimals(uint8 decimals_) internal {11 _decimals = decimals_;12 }Mostrar todoCopiar
Esta función modifica la variable _decimals
que sirve para decirle a las interfaces de usuario cómo deben interpretar la cantidad. Debería activarla desde el constructor. Sería desleal activarla desde cualquier punto subsecuente y las aplicaciones no están diseñadas para manejarla.
Hooks
12 /**3 * @dev Hook that is called before any transfer of tokens. This includes4 * minting and burning.5 *6 * Calling conditions:7 *8 * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens9 * will be to transferred to `to`.10 * - when `from` is zero, `amount` tokens will be minted for `to`.11 * - when `to` is zero, `amount` of ``from``'s tokens will be burned.12 * - `from` and `to` are never both zero.13 *14 * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].15 */16 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }17}Mostrar todoCopiar
Esta es la función gancho a ser llamada durante las transferencias. Aquí está vacía, pero si necesita hacer algo puede sobrescribirla.
Conclusión
Para revisión, he aquí hay algunas de las ideas importantes en este contrato (en mi opinión, porque usted puede pensar de otra manera):
- No hay secretos en la cadena de bloques.. Cualquier información a la que un contrato inteligente pueda acceder está disponible para todo el mundo.
- Puedes controlar el orden de tus propias transacciones, pero no cuando ocurren las transacciones de otras personas. Esta es la razón por la que cambiar una asignación puede ser peligroso, por que permite que el gastador gaste la suma de ambos permisos.
- Valores del tipo
uint256
se envuelven alrededor. En otras palabras 0-1=2^256-1. Si no se desea ese comportamiento, tiene que comprobarlo (o use la biblioteca SafeMath, que lo hace en su nombre). Tome en cuenta que esto cambió en Solidity 0.8.0(opens in a new tab). - Haz todos los cambios de estado de un tipo específico en un lugar en específico, porque esto facilita la auditoría. Esta es la función que tenemos, por ejemplo,
_approve
, la cual se invoca medianteapprove
,transferFrom
,increaseAllowance
ydecreaseAllowance
- Los cambios de estado deben ser atómicos, sin otra acción de por medio (como puedes ver en
_transfer
). Esto se debe a que durante el cambio de estado tiene un estado inconsistente. Por ejemplo, entre el momento en que deduce desde el saldo del emisor y el momento en que añade al saldo del receptor, hay menos tókenes en existencia de los que debería. Se podría abusar de esto potencialmente, si hay operaciones entre ellos, especialmente inivocadas a un contrato diferente.
Ahora que ha visto cómo se escribe un contrato ERC-20 de OpenZeppelin y especialmente cómo se hace más seguro, escriba sus propias aplicaciones y contratos seguros.
Última edición: @nhsz(opens in a new tab), 18 de febrero de 2024