详解 ERC-1363 代币标准

ERC-1363 标准在 ERC-20 标准上的补充,可以在转账的时候触发 Hook 的调用。

ERC-1363 使智能合约能够检测并响应代币的转账。

ERC-1363 标准解释

ERC-1363 解决了什么问题?

假设用户向合约转账 ERC-20 代币。由于没有机制可以查看是谁进行了转账,智能合约无法为转账用户记账。

虽然事件可以跟踪此信息,但只能被链下消费者使用。智能合约无法在没有预言机的情况下读取事件。

传统方案:通知接收者的替代方法是接收者使用 transferFrom 将代币转账给自己

上述问题的典型解决方法是代币发送方批准接收智能合约代表发送方转账代币。

contract ReceivingContract {
    function deposit(uint256 amount) external {
        // 如果未经批准或用户余额不足,将会回滚
        ERC20(token).transferFrom(msg.sender, address.this, amount);

        deposits[msg.sender] += amount;
    }
}

然后存款人调用接收智能合约的函数(在上面的示例代码中为 deposit)来从发送方转账代币到合约。由于合约知道它从用户那里转账了代币,因此能够正确记账。

然而,为了批准合约转账代币,需要增加额外的交易费用。

此外,用户在批准合约后应将批准设置为零,否则存在合约被利用的风险,可能导致合约从用户那里提取更多 ERC-20 代币。

转账 Hook(钩子)

转账 Hook 是接收智能合约中的预定义函数,当它接收到代币时将被调用。也就是说,代币合约在接收到转账指令后,会在接收地址上调用预定义函数。

如果函数不存在、回滚或未返回预期的成功值,则转账会回滚。

已经熟悉 ERC-721 标准中的 onERC721Received 的读者将对转账Hook很熟悉。

ERC-1363 扩展了 ERC-20 标准,添加了转账Hook

要实现该标准,ERC-20 需要额外的函数(稍后会解释)来转账代币以触发接收方的转账Hook,并且接收方必须根据标准实现转账Hook。

IERC1363Receiver

对于希望被通知其已收到 ERC-1363 代币的合约,它们必须实现 IERC1363Receiver(请查看此处的 OpenZeppelin 实现 ),其中包含一个名为 onTransferReceived 的函数:

pragma solidity ^0.8.20;

interface IERC1363Receiver {
// 成功时返回`bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))`
    function onTransferReceived(
        address operator,
        address from,
        uint256 value,
        bytes calldata data
    ) external returns (bytes4);
}
  • operator 是发起转账的地址

  • from 是从中扣除 ERC-1363 代币的账户

  • value 是转账的代币数量

  • data 由operator指定以转发给接收方

在实现此函数时,请始终检查 msg.sender 是否是你希望接收 ERC-1363 代币的代币,因为任何人都可以使用任意值调用 onTransferReceived()。

以下是一个接受 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);
    }

}

合约知道自己收到 ERC-20 代币的传统方式是使用 transferFrom 函数,该函数需要首先进行批准,但是使用 ERC-1363 后,合约能够知道自己已收到代币,并且还能够消除批准步骤,因为 transferAndCall 将代币转账给合约(无需批准)并调用 onTransferReceived 函数。·

通过 ERC-20 最大化向后兼容性

新代币标准的问题在于现有协议无法使用它们,除非它们与先前的标准完全兼容。

为了最大化向后兼容性,ERC-1363 是一种 ERC-20 代币,它添加了旧协议不需要使用的额外函数。

所有现有的 ERC-20 函数:name、symbol、decimals、totalSupply、balanceOf、transfer、transferFrom、approve 和 allowance 的行为都与 ERC-20 标准规定的完全一致。

ERC-1363 标准添加了新函数到 ERC-20,以便旧协议仍然可以与 ERC-1363 代币交互,就像与 ERC-20 代币一样。但是,如果需要,新协议可以利用 ERC-1363 上的转账Hook。

要成为符合 ERC-1363 标准的代币,代码还必须实现六个额外的函数:

  • 两个版本的 transferAndCall

  • 两个版本的 transferFromAndCall

  • 两个版本的 approveAndCall

顾名思义,这些函数将执行 ERC-20 操作,然后调用接收方的Hook函数。

每个函数都有两个版本,一个带有数据参数,一个不带。数据参数是为了发送方能够将数据转发给接收合约(稍后我们将展示一个示例)。

// 有两个 transferAndCall 函数,
// 一个带有数据参数,一个不带

function transferAndCall(
    address to,
    uint256 value
) external returns (bool);

function transferAndCall(
    address to,
    uint256 value,
    bytes calldata data
) external returns (bool);

// 有两个 transferFromAndCall 函数,
// 一个带有数据参数,一个不带

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

// 有两个 approveAndCall 函数,// 一个带有数据参数,一个不带```
function approveAndCall(
    address spender,
    uint256 value
) external returns (bool);

function approveAndCall(
    address spender,
    uint256 value,
    bytes calldata data
) external returns (bool);

ERC-721 inspiration: transferFrom vs safeTransferFrom

与 ERC-721 标准类似,ERC-1363 中 transferFromAndCall 和 transferFrom 之间的区别与 ERC-721 中 transferFrom 和 safeTransferFrom 之间的区别相同。然而,“safe”不是一个理想的函数名称,因为转账挂钩引入了潜在的重入向量,所以它并不“安全”。ERC-1363 使用的“call”一词的添加使得函数正在做什么更加明确:在转账后调用接收者通知其代币已转移给它。

参考实现

可以在这里找到一个 ERC-1363 实现 。我们将使用该示例中的大量代码。逐步解释代码库比一次性粘贴实现更容易。对于实现 ERC-1363 代币的人,请使用上面链接的实现。这里的代码仅供说明目的。

ERC-1363 使用与 ERC-20 相同的余额和批准存储变量。它不存储额外信息。

ERC-1363 代码概述

继承 ERC-20

正如前面强调的,ERC-1363 是一个具有附加功能的 ERC-20 代币。构建 ERC-1363 的第一步是继承 ERC-20:

//SPDX-License-Identifier: MIT

pragma solidity 0.8.24;
import "@openzeppelin/contracts@5.0.0/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)

仅当接收地址实现 onTransferReceived()并返回 onTransferReceived()的四字节函数选择器时,transferFromAndCall 才成功。

function transferFromAndCall(
    address from,
    address to,
    uint256 value,
    bytes memory data
) public virtual returns (bool) {

    // 首先调用父级中的 ERC-20 transferFrom 函数
    if (!transferFrom(from, to, value)) {
        revert ERC1363TransferFromFailed(from, to, value);
    }

    // 然后调用接收者
    _checkOnTransferReceived(from, to, value, data);
    return true;
}

// 此函数没有数据参数,转发空数据

function transferFromAndCall(
    address from,
    address to,
    uint256 value
) public virtual returns (bool) {
    // \`data\`为空
    return transferFromAndCall(from, to, value, "");
}

transferAndCall(address to, uint256 value) external returns (bool)

这与 transferFromAndCall 非常相似,只是 from 是 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()

此函数检查接收者是否为合约,如果不是则回滚。然后尝试调用 onTransferReceived,如果未收到 onTransferReceived(address,address,uint256,bytes) 的函数选择器 0x88a7ca5c,则回滚。如果 onTransferReceived 回滚,则此函数使用从 onTransferReceived 接收的错误消息回滚。

因为此函数如果发送到 EOA(常规钱包)会回滚,所以将 ERC-1363 转账给 EOA 应使用 ERC-20 函数 transfer 或 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 {                        

            // 此代码导致 ERC-1363 回滚                        
            // 与其调用的合约相同的回滚字符串                    
            assembly {                        
                revert(add(32, reason), mload(reason))                    
            }                
        }            
    }
}

approveAndCall

在上述工作流程中,被调用的智能合约是 ERC-1363 代币的接收者。

然而,如果我们希望另一个合约成为我们代币的发送者怎么办?例如,路由器合约,如 Uniswap V2 Router,不持有代币的保管权。它将它们转发给 Uniswap 进行交易。

传统上,这样的架构使用“先批准再转账”工作流程,但使用 ERC-1363,我们可以在一笔交易中完成这个操作。顾名思义,刚刚获得批准以花费另一个地址的代币的合约会得到一个特殊的挂钩函数调用。

与 transferAndCall 函数一样,根据调用的 approveAndCall,向交易提供附加数据是可选的:

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

类似于 IERC1363Receiver,当调用 approvalAndCall 时,会触发名为 onApprovalReceived 的函数。

这是 OpenZeppelin 提供的 IERC1363Spender 接口 。下面的代码已删除注释:

interface IERC1363Spender {
    
    function onApprovalReceived(            
        address owner,            
        uint256 value,            
        bytes calldata data
    ) external returns (bytes4);

}

只有代币的所有者可以批准另一个地址,因此不需要操作员参数 —— 在批准期间,操作员和所有者必须是相同的地址。value 是批准金额的大小。

以下合约在收到 onApprovalReceived 后将代币转发到数据中指定的地址。

import "@openzeppelin/contracts/interfaces/IERC1363Spender.sol";

contract Router is IERC1363Spender {        
    // 需要额外的功能,以便批准的钱包将批准的 ERC-1363 代币添加到此映射中        

    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 to it work                
    address target = getTarget(data);                
    bool success = IERC1363(msg.sender).transferFrom(owner, target, value);                

    require(success, "transfer failed");                

    return this.onApprovalReceived.selector; 
       
}

此函数应检查msg.sender是否为代币合约,因为如果任何人都可以调用它,可能会导致意外行为。

使用 ERC-1363 的示例接收合约

以下示例演示了对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;        
    }
}

早期尝试解决代币Hook的标准

ERC-1363 不是第一个向 ERC-20 添加转账挂钩的标准。首先,于 2017 年 5 月提出了 ERC-223,以在 ERC-20 的转账和 transferFrom 中添加转账挂钩。但这意味着智能合约无法接收代币,除非它们实现了转账挂钩。这使得该标准与接受 ERC-20 代币但没有转账挂钩的协议不兼容。

ERC-777 于 2017 年 11 月推出。在此标准中,除非接收方在 ERC-1820 注册表 中注册了他们的地址,否则接收方不会收到转账挂钩调用。

然而,协议并未设计 ERC-20 的转账或 transferFrom 以调用其他合约。这使得这些合约容易受到重入攻击,因为它们没有预期“ERC-20” 代币会调用其他合约。有关更多信息,请参阅 Uniswap V1 重入漏洞解析

此外,ERC-777 标准在 gas 方面相当昂贵,因为它需要向 ERC-1820 注册表合约发出额外的调用。

ERC-1363 通过完全不改变 ERC-20 标准中的转账和 transferFrom 来解决所有这些问题。所有的转账挂钩都在具有显式调用名称的函数中调用。

何时使用 ERC-1363 标准

ERC-1363 标准可以在任何应用 ERC-20 标准的地方使用。在作者看来,这个标准是 ERC-20 的理想替代品,因为它可以消除 ERC-20 的批准步骤,这导致了大量资金的损失。

查看我们的 Solidity BootCamp (英文) 以了解更多关于智能合约开发和代币标准的知识。

登链社区有线下中文集训营 欢迎了解。

我是 AI 翻译官,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/