ERC-1363 permite que un smart contract detecte y responda a una transferencia entrante de tokens.

¿Qué problema resuelve ERC-1363?
Supongamos que un usuario transfiere un token ERC-20 a un contrato. El smart contract no puede acreditar al usuario por la transferencia porque no tiene ningún mecanismo para ver quién hizo la transferencia.
Aunque los eventos rastrean esta información, solo son utilizables por consumidores off-chain. Los smart contracts no pueden leer eventos sin un oracle.
Solución tradicional: en lugar de notificar al receptor, el receptor se transfiere los tokens a sí mismo usando transferFrom
Una solución típica para el problema descrito anteriormente es que el remitente de los tokens apruebe al smart contract receptor para transferir tokens en nombre del remitente.
contract ReceivingContract {
function deposit(uint256 amount) external {
// will revert if this contract is not approved
// or the user has an insufficient balance
ERC20(token).transferFrom(msg.sender, address.this, amount);
deposits[msg.sender] += amount;
}
}
Luego, el depositante invoca una función (deposit en el código de ejemplo anterior) en el smart contract receptor para transferir tokens del remitente al contrato. Dado que el contrato sabe que transfirió tokens del usuario, es capaz de acreditar su cuenta correctamente.
Sin embargo, agregar una transacción adicional para aprobar que el contrato transfiera los tokens aumenta el costo de gas.
Además, el usuario debería establecer la aprobación del contrato a cero después de aprobar el contrato, de lo contrario existe el peligro de que si el contrato es explotado, pueda retirar más tokens ERC-20 del usuario.
Transfer hooks
Un transfer hook es una función predefinida en el smart contract receptor que será llamada cuando reciba tokens. Es decir, el contrato del token, después de recibir una instrucción de transferencia, llama a la función predefinida en la dirección del destinatario.
Si la función no está presente, revierte o no devuelve el valor de éxito esperado, la transferencia revierte.
Los lectores que ya estén familiarizados con onERC721Received en el estándar ERC-721 estarán familiarizados con el transfer hook.
ERC-1363 extiende el estándar ERC-20, agregando transfer hooks.
Para implementar el estándar, el ERC-20 necesita funciones adicionales (explicadas más adelante) para transferir tokens y así activar el transfer hook en el receptor, y el receptor debe implementar el transfer hook de acuerdo con el estándar.
IERC1363Receiver
Para un contrato que desee ser notificado de que ha recibido tokens ERC-1363, debe implementar IERC1363Receiver (ver la implementación de OpenZeppelin aquí) que tiene una sola función onTransferReceived:
pragma solidity ^0.8.20;
interface IERC1363Receiver {
// returns `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))` on success
function onTransferReceived(
address operator,
address from,
uint256 value,
bytes calldata data
) external returns (bytes4);
}
operatores la dirección que inicia la transferenciafromes la cuenta ERC-1363 de la que se están deduciendo los tokensvaluees la cantidad de tokens que se están transfiriendodataes especificada por el operator para ser reenviada al receptor
Al implementar esta función, siempre comprueba que msg.sender es el token ERC-1363 que deseas recibir, porque cualquiera puede llamar a onTransferReceived() con valores arbitrarios.
Aquí hay un contrato de ejemplo mínimo que acepta tokens ERC-1363:
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/interfaces/IERC1363Receiver.sol";
import "@openzeppelin/contracts/interfaces/IERC1363.sol";
contract TokenReceiver is IERC1363Receiver {
address internal erc1363Token;
constructor(address erc1363Token_) {
erc1363Token = erc1363Token_;
}
mapping(address user => uint256 balance) public balances;
function onTransferReceived(
address operator,
address from,
uint256 value,
bytes calldata data
) external returns (bytes4) {
require(msg.sender == erc1363Token, "not the expected token");
balances[from] += value;
return this.onTransferReceived.selector;
}
function withdraw(uint256 value) external {
require(balances[msg.sender] >= value, "balance too low");
balances[msg.sender] -= value;
IERC1363(erc1363Token).transfer(msg.sender, value);
}
}
La forma tradicional en que un contrato sabe que recibió tokens ERC-20 es usando la función transferFrom, lo cual requiere una aprobación primero, pero con ERC-1363, el contrato es capaz de saber que ha recibido un token y también eliminar el paso de aprobación porque transferAndCall transfiere el token al contrato (sin aprobación) y llama a la función onTransferReceived.
Maximizando la Retrocompatibilidad con ERC-20
El problema con los nuevos estándares de tokens es que los protocolos existentes no podrán usarlos a menos que sean perfectamente compatibles con los estándares anteriores.
Para maximizar la retrocompatibilidad, ERC-1363 es un token ERC-20 que agrega funciones extra que los protocolos más antiguos no necesitan usar.
Todas las funciones ERC-20 existentes: name, symbol, decimals, totalSupply, balanceOf, transfer, transferFrom, approve, y allowance se comportan exactamente como lo especifica el estándar ERC-20.
El estándar ERC-1363 agrega nuevas funciones a ERC-20 para que los protocolos heredados aún puedan interactuar con el token ERC-1363 exactamente de la misma manera que lo hacen con los tokens ERC-20. Sin embargo, los protocolos más nuevos pueden aprovechar el transfer hook en ERC-1363 si lo desean.
Para ser un token ERC-1363 compatible, el código también debe implementar seis funciones adicionales:
- Dos versiones de
transferAndCall - Dos versiones de
transferFromAndCall - Dos versiones de
approveAndCall
Como su nombre indica, estas funciones realizarán la acción ERC-20 y luego llamarán a la función hook del destinatario.
Hay dos versiones de cada función, una con un parámetro data y otra sin él. El parámetro data es para que el remitente pueda reenviar datos al contrato receptor (mostraremos un ejemplo de esto más adelante).
Aparte de las funciones que toman el argumento data, estas funciones toman los mismos argumentos en el mismo orden que sus homólogas de ERC-20.
// There are two transferAndCall functions,
// one with a data argument and one without
function transferAndCall(
address to,
uint256 value
) external returns (bool);
function transferAndCall(
address to,
uint256 value,
bytes calldata data
) external returns (bool);
// There are two transferFromAndCall functions,
// one with a data argument and one without
function transferFromAndCall(
address from,
address to,
uint256 value
) external returns (bool);
function transferFromAndCall(
address from,
address to,
uint256 value,
bytes calldata data
) external returns (bool);
// There are two approveAndCall functions,// one with a data argument and one without
function approveAndCall(
address spender,
uint256 value
) external returns (bool);
function approveAndCall(
address spender,
uint256 value,
bytes calldata data
) external returns (bool);
Inspiración de ERC-721: transferFrom vs safeTransferFrom
De manera similar al estándar ERC-721, la diferencia entre transferFromAndCall y transferFrom en ERC-1363 es la misma diferencia que existe entre transferFrom y safeTransferFrom en ERC-721. Sin embargo, “safe” (seguro) no es un nombre de función ideal, ya que el transfer hook introduce un vector potencial de reentrancy, por lo que no es “seguro”. La adición de la palabra “call” que utiliza ERC-1363 hace más explícito lo que hace la función: llamar al receptor después de la transferencia para notificarle que se le transfirieron tokens.
Implementación de Referencia
Se puede encontrar una implementación de ERC-1363 aquí. Estaremos usando una cantidad significativa de código de ese ejemplo. Es más fácil explicar el código base pieza por pieza que pegar la implementación aquí de una sola vez. Para aquellos que implementen un token ERC-1363, por favor utilicen la implementación enlazada arriba. El código aquí es solo para fines ilustrativos.
ERC-1363 utiliza las mismas variables de almacenamiento para balances y aprobaciones que ERC-20. No almacena información adicional.
Resumen del código de ERC-1363
Heredando de ERC-20
Como se enfatizó anteriormente, ERC-1363 es un token ERC-20 con funciones adicionales. El primer paso para construir un ERC-1363 es heredar de ERC-20:
//SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import "@openzeppelin/[email protected]/token/ERC20/ERC20.sol";
contract ERC1363 is ERC20 {
constructor(
string memory name,
string memory symbol
)ERC20(name, symbol) {}
}
transferFromAndCall(address to, uint256 value) external returns (bool)
transferFromAndCall tiene éxito si y solo si la dirección receptora implementa onTransferReceived() y devuelve el selector de función de cuatro bytes de onTransferReceived().
function transferFromAndCall(
address from,
address to,
uint256 value,
bytes memory data
) public virtual returns (bool) {
// first call the ERC-20 transferFrom function in the parent
if (!transferFrom(from, to, value)) {
revert ERC1363TransferFromFailed(from, to, value);
}
// then call the receiver
_checkOnTransferReceived(from, to, value, data);
return true;
}
// this function has no data parameter and
// forwards empty data
function transferFromAndCall(
address from,
address to,
uint256 value
) public virtual returns (bool) {
// `data` is empty
return transferFromAndCall(from, to, value, "");
}
transferAndCall(address to, uint256 value) external returns (bool)
Esto es muy similar a transferFromAndCall, excepto que from es msg.sender.
function transferAndCall(
address to,
uint256 value,
bytes memory data
) public virtual returns (bool) {
if (!transfer(to, value)) {
revert ERC1363TransferFailed(to, value);
}
_checkOnTransferReceived(msgSender(), to, value, data);
return true;
}
function transferAndCall(
address to,
uint256 value
) public virtual returns (bool) {
return transferAndCall(to, value, "");
}
_checkOnTransferReceived()
Esta función comprueba si el receptor es un contrato, y si no, revierte. Luego intenta llamar a onTransferReceived y revierte si no recibe 0x88a7ca5c, el selector de función de onTransferReceived(address,address,uint256,bytes). Si onTransferReceived revierte, esta función revierte con el mensaje de error recibido de onTransferReceived.
Debido a que esta función revierte si se envía a una EOA (billetera regular), para transferir un ERC-1363 a una EOA se deberían usar las funciones ERC-20 transfer o transferFrom:
function _checkOnTransferReceived(
address from,
address to,
uint256 value,
bytes memory data
) private {
if (to.code.length == 0) {
revert ERC1363EOAReceiver(to);
}
try IERC1363Receiver(to).onTransferReceived(_msgSender(), from, value, data) returns (bytes4 retval) {
if (retval != IERC1363Receiver.onTransferReceived.selector) {
revert ERC1363InvalidReceiver(to);
}
} catch (bytes memory reason) {
if (reason.length == 0) {
revert ERC1363InvalidReceiver(to);
} else {
// this code causes the ERC-1363 to revert
// with the same revert string as the
// contract it called
assembly {
revert(add(32, reason), mload(reason))
}
}
}
}
approveAndCall
En los flujos de trabajo anteriores, el smart contract que es llamado es el destinatario de los tokens ERC-1363.
Sin embargo, ¿qué pasa si queremos que otro contrato sea el remitente de nuestros tokens? Por ejemplo, un contrato router, como el Uniswap V2 Router, no mantiene la custodia de los tokens. Los reenvía a Uniswap para intercambiarlos.
Tradicionalmente, tales arquitecturas utilizan el flujo de trabajo de “approve y luego transferFrom”, pero con ERC-1363 podemos hacer esto en una sola transacción con approveAndCall. Como el nombre sugiere, el contrato que acaba de recibir aprobación para gastar los tokens de otra dirección recibe una llamada a una función hook especial.
Al igual que con las funciones transferAndCall, proporcionar datos adicionales a la transacción es opcional dependiendo de qué approveAndCall se invoque:
function approveAndCall(
address spender,
uint256 value
) public virtual returns (bool) {
return approveAndCall(spender, value, "");
}
function approveAndCall(
address spender,
uint256 value,
bytes memory data
) public virtual returns (bool) {
if (!approve(spender, value)) {
revert ERC1363ApproveFailed(spender, value);
}
_checkOnApprovalReceived(spender, value, data);
return true;
}
IERC1363Spender
De manera similar a IERC1363Receiver, una función llamada onApprovalReceived se activa cuando se invoca un approvalAndCall.
Aquí está la interfaz proporcionada por OpenZeppelin para IERC1363Spender. Al código a continuación se le han eliminado los comentarios:
interface IERC1363Spender {
function onApprovalReceived(
address owner,
uint256 value,
bytes calldata data
) external returns (bytes4);
}
Solo el propietario de los tokens puede aprobar a otra dirección, por lo que no hay necesidad de un argumento operator — durante una aprobación, el operator y el owner deben ser la misma dirección. value es el tamaño de la cantidad de la aprobación.
El siguiente contrato, al recibir onApprovalReceived, reenvía los tokens a la dirección especificada en la data.
import "@openzeppelin/contracts/interfaces/IERC1363Spender.sol";
contract Router is IERC1363Spender {
// additional functions are needed for an approved
// wallet to add approved ERC-1363 tokens to this mapping
mapping(address => bool) isApprovedToken;
function onApprovalReceived(
address owner,
uint256 value,
bytes calldata data
) external returns (bytes4) {
require(isApprovedToken[msg.sender], "not an approved token");
// getTarget is not implemented here,
// see the next section for how it works
address target = getTarget(data);
bool success = IERC1363(msg.sender).transferFrom(owner, target, value);
require(success, "transfer failed");
return this.onApprovalReceived.selector;
}
}
Esta función debería comprobar si msg.sender es el contrato del token, porque si a cualquiera se le permite llamarla, eso podría llevar a un comportamiento inesperado.
Contrato receptor de ejemplo usando ERC-1363
El siguiente ejemplo demuestra un caso de uso para el argumento data.
interface ERC1363Receiver {
function onTransferReceived(
address operator,
address from,
uint256 value,
bytes memory data
) external returns (bytes4);
}
contract ReceiverContract is ERC1363Receiver {
mapping(address => uint256) public deposits;
address immutable token;
constructor(address token_) {
token = token_;
}
event Deposit(
address indexed from,
address indexed beneficiary,
uint256 value
);
function onTransferReceived(
address, // operator
address from,
uint256 value,
bytes memory data
) external returns (bytes4) {
require(msg.sender == token, "Caller not ERC1363 token");
address beneficiary;
if (data.length == 32) {
beneficiary = abi.decode(data, (address));
} else {
beneficiary = from;
}
deposits[from] += value;
emit Deposit(from, beneficiary, value);
return this.onTransferReceived.selector;
}
}
Estándares previos que intentaron resolver los token hooks
ERC-1363 no fue el primer estándar en agregar transfer hooks a ERC-20. Primero, se propuso ERC-223 en mayo de 2017 para agregar el transfer hook a transfer y transferFrom en ERC-20. Pero esto significaba que los smart contracts no podían recibir el token a menos que implementaran el transfer hook. Esto hizo que el estándar no fuera retrocompatible con protocolos que aceptaban tokens ERC-20, pero que no tenían un transfer hook.
ERC-777 fue introducido en noviembre de 2017. En este estándar, el receptor no recibiría una llamada al transfer hook a menos que hubiera registrado su dirección en el registro ERC-1820.
Sin embargo, los protocolos no estaban diseñados para que transfer o transferFrom en ERC-20 hicieran una llamada externa a otros contratos. Esto hizo a esos contratos vulnerables a reentrancy porque no esperaban que un token “ERC-20” hiciera llamadas a otro contrato. Vea el artículo sobre la vulnerabilidad de reentrancy en Uniswap V1 para más detalles.
Además, el estándar ERC-777 era bastante costoso desde la perspectiva del gas porque necesitaba hacer una llamada adicional al contrato de registro ERC-1820.
ERC-1363 resuelve todos estos problemas al dejar transfer y transferFrom en el estándar ERC-20 completamente inalterados. Todos los transfer hooks son llamados en funciones que tienen una llamada explícita en el nombre.
Cuándo usar el estándar ERC-1363
El estándar ERC-1363 se puede usar dondequiera que se usaría el estándar ERC-20. En opinión del autor, este estándar es un reemplazo deseable para ERC-20 ya que puede eliminar el paso de approve de ERC-20, el cual ha llevado a considerables pérdidas de fondos.
Aprende más con RareSkills
Consulta nuestro bootcamp de Solidity para aprender más sobre el desarrollo de smart contracts y estándares de tokens.
Publicado originalmente el 4 de abril de 2024