Ir al contenido principal

Una explicación del contrato ERC-20

solidityerc-20
Principiante
Ori Pomerantz
9 de marzo de 2021
28 minuto leído minute read

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.

Illustración de la interfaz ERC-20

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: MIT
Copiar

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 be
3 * allowed to spend on behalf of `owner` through {transferFrom}. This is
4 * 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 risk
7 * that someone may use both the old and the new allowance by unfortunate
8 * transaction ordering. One possible solution to mitigate this race
9 * condition is to first reduce the spender's allowance to 0 and set the
10 * desired value afterwards:
11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
12 *
13 * Emits an {Approval} event.
14 */
15 function approve(address spender, uint256 amount) external returns (bool);
Mostrar todo
Copiar

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 the
3 * allowance mechanism. `amount` is then deducted from the caller's
4 * 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 todo
Copiar

Por último, transferirDesde lo utiliza el gastador para utilizar realmente la asignación.

1
2 /**
3 * @dev Emitted when `value` tokens are moved from one account (`from`) to
4 * another (`to`).
5 *
6 * Note that `value` may be zero.
7 */
8 event Transfer(address indexed from, address indexed to, uint256 value);
9
10 /**
11 * @dev Emitted when the allowance of a `spender` for an `owner` is set by
12 * a call to {approve}. `value` is the new allowance.
13 */
14 event Approval(address indexed owner, address indexed spender, uint256 value);
15}
Mostrar todo
Copiar

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: MIT
2pragma 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:

1
2import "../../GSN/Context.sol";
3import "./IERC20.sol";
4import "../../math/SafeMath.sol";
Copiar

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 means
5 * 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 guide
9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How
10 * to implement supply mechanisms].
11 *
12 * We have followed general OpenZeppelin guidelines: functions revert instead
13 * of returning `false` on failure. This behavior is nonetheless conventional
14 * 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 just
18 * by listening to said events. Other implementations of the EIP may not emit
19 * 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 setting
23 * allowances. See {IERC20-approve}.
24 */
25
Mostrar todo
Copiar

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.

1
2 using SafeMath for uint256;
3
Copiar

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} with
3 * 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 during
8 * construction.
9 */
10 constructor (string memory name_, string memory symbol_) public {
11 _name = name_;
12 _symbol = symbol_;
13 _decimals = 18;
14 }
Mostrar todo
Copiar

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 }
7
8 /**
9 * @dev Returns the symbol of the token, usually a shorter version of the
10 * name.
11 */
12 function symbol() public view returns (string memory) {
13 return _symbol;
14 }
15
16 /**
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 should
19 * be displayed to a user as `5,05` (`505 / 10 ** 2`).
20 *
21 * Tokens usually opt for a value of 18, imitating the relationship between
22 * ether and wei. This is the value {ERC20} uses, unless {_setupDecimals} is
23 * called.
24 *
25 * NOTE: This information is only used for _display_ purposes: it in
26 * no way affects any of the arithmetic of the contract, including
27 * {IERC20-balanceOf} and {IERC20-transfer}.
28 */
29 function decimals() public view returns (uint8) {
30 return _decimals;
31 }
Mostrar todo
Copiar

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 vidaAcceso al contratoCoste del gas
MemoriaActivación de una funciónLeer/EscribirDecenas o centenas (más altas para ubicaciones más altas)
CalldataActivación de una funciónSólo lecturaNo se puede utilizar como tipo de retorno, solo un tipo de parámetro de función
AlmacenamientoHasta que cambieLeer/EscribirAlta (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 todo
Copiar

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 not
5 * 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 least
12 * `amount`.
13 */
14 function transferFrom(address sender, address recipient, uint256 amount) public virtual
15 override returns (bool) {
16 _transfer(sender, recipient, amount);
Mostrar todo
Copiar

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 AliceNonce de AliceTransacción de BillNonce de BillAsignación de BillIngresos totales de Bill procedentes de Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
approve(Bill, 10)11105
transferFrom(Alice, Bill, 10)10,124015

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 AliceNonce de AliceTransacción de BillNonce de BillAsignación de BillIngresos totales de Bill procedentes de Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
increaseAllowance(Bill, 5)110+5 = 55
transferFrom(Alice, Bill, 5)10,124010

B:

Transacción de AliceNonce de AliceTransacción de BillNonce de BillAsignación de BillIngresos totales de Bill procedentes de Alice
approve(Bill, 5)1050
increaseAllowance(Bill, 5)115+5 = 100
transferFrom(Alice, Bill, 10)10,124010
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 for
5 * 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 todo
Copiar

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.

1
2 /**
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 for
6 * 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 least
14 * `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 todo
Copiar

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 to
5 * 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 todo
Copiar

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);
2
Copiar

Hay dos maneras de usar este contrato:

  1. Usarlo como plantilla para su propio código.
  2. 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`, increasing
2 * 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 todo
Copiar

Asegúrese de actualizar _totalSupply cuando la cantidad total de tókenes cambie.

1 /**
2 * @dev Destroys `amount` tokens from `account`, reducing the
3 * 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");
14
15 _beforeTokenTransfer(account, address(0), amount);
16
17 _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 to
5 * 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");
17
18 _allowances[owner][spender] = amount;
Mostrar todo
Copiar

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 }
3
Copiar

Modificar la variable Decimals

1
2
3 /**
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. Most
7 * applications that interact with token contracts will not expect
8 * {decimals} to ever change, and may work incorrectly if it does.
9 */
10 function _setupDecimals(uint8 decimals_) internal {
11 _decimals = decimals_;
12 }
Mostrar todo
Copiar

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

1
2 /**
3 * @dev Hook that is called before any transfer of tokens. This includes
4 * minting and burning.
5 *
6 * Calling conditions:
7 *
8 * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
9 * 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 todo
Copiar

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 mediante approve, transferFrom, increaseAllowance y decreaseAllowance
  • 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

¿Le ha resultado útil este tutorial?