一文说清楚ERC1363规范为什么是ERC20的理想替代

  • Louis
  • 更新于 2024-07-13 12:03
  • 阅读 971

为什么会有ERC1363标准?我们知道,ERC20是用于在以太坊区块链上创建和管理代币的流行标准。它定义了一组函数和事件,用于转账、批准和查询代币余额。虽然ERC20标准已经成功用于创建各种代币,但它也存在一些已知的缺点和漏洞,这些漏洞被黑客利用,盗取了很多代币,让项目损失惨重。

相关背景:为什么会有ERC1363标准?

我们知道,ERC20是用于在以太坊区块链上创建和管理代币的流行标准。它定义了一组函数和事件,用于转账、批准和查询代币余额。虽然 ERC20 标准已经成功用于创建各种代币,但它也存在一些已知的缺点和漏洞,这些漏洞被黑客利用,盗取了很多代币,让项目损失惨重。

ERC1363 是对 ERC20 标准的扩展,它引入了在转账或批准后执行回调函数的功能。这意味着符合 ERC1363 的代币除了具有 ERC20 代币的所有功能之外,还具有以下一些特点和优势

ERC1363的一些优势:

提高可组合性: ERC1363 代币可以与其他智能合约无缝交互,并在转账或批准发生时触发操作。这使得它们适用于需要代币与其他去中心化应用程序 (dApp) 集成的各种用例。

增强安全性: ERC1363 代币可以用来防止代币丢失或锁定在合约中。例如,在将代币转账给接收者合约之前,可以调用回调函数来检查接收者合约是否有效。这可以帮助防止代币意外发送到无法处理它们的合约中。

改善用户体验: ERC1363 代币可以用于创建更原子性的交易,并减少用户需要确认的交易数量。例如,ERC1363 代币可以用于将代币转账和批准操作组合成单个交易。这可以提高交易速度并改善用户体验。

TokenBank合约存在问题:

手把手教你实现TokenBank智能合约中,假设用户向合约转账 ERC20 代币。由于没有机制可以查看是谁进行了转账,智能合约无法为转账用户记账。

一个比较典型的解决方案是:接收者使用 transferFrom 将代币转账给自己,但是前提条件是:代币发送方批准接收智能合约代表发送方转账代币。

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

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

然而,为了批准合约转账代币,需要增加额外的交易费用。并且前置的授权操作,会让用户感觉有些繁琐。

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

预定义一个转账Hook (钩子)

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

如果这个预定义函数不存在、回滚或未返回预期的成功值,则这笔转账交易会回滚。

ERC1363扩展了ERC20标准,添加了转账Hook

要实现ERC1363这个标准,ERC20需要额外的函数(稍后会解释)来转账代币并同时触发接收方的转账Hook,并且接收方必须根据标准实现转账Hook。

IERC1363Receiver

对于接收ERC1363代币的合约,我们要想通知到它,它就必须实现 IERC1363Receiver,(请查看OpenZeppelin实现),其中包含一个名为 onTransferReceived 的函数:

这个函数成功的时候会返回:

bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))

pragma solidity ^0.8.20;

interface IERC1363Receiver {
    function onTransferReceived(
            address operator,
            address from,
            uint256 value,
            bytes calldata data
    ) external returns (bytes4);
}
  • operator 是发起转账的地址
  • from 是从中扣除 ERC1363 代币的账户
  • value 是转账的代币数量
  • data 由operator指定以转发给接收方

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

下面是一个代码实例,这个合约可以接收指定的ERC1363代币:

pragma solidity ^0.8.20;

import "@openzeppelin/contracts/interfaces/IERC1363Receiver.sol";
import "@openzeppelin/contracts/interfaces/IERC1363.sol";

contract TokenReceiver is IERC1363Receiver {
    address internal erc1363Token;

        // 部署此合约的时候,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);
    }

}

接收代币的合约知道自己收到ERC20代币的传统方式是使用transferFrom函数,该函数需要首先进行代币额度批准,但是使用 ERC1363 后,合约能够知道自己已收到代币,并且还能够跳过批准步骤,因为 transferAndCall 将EOA账户通过 transfer 将代币转账给接收者合约(无需批准)并调用 onTransferReceived 函数。

ERC1363做到了通过ERC20最大化向后兼容性

这个新代币标准的问题在于现有协议无法使用它们,为了最大化向后兼容性,ERC1363 是一种 ERC20 代币,它添加了旧协议不需要使用的额外函数,并不会影响原有功能。

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

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

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

  • 两个版本的 transferAndCall
  • 两个版本的 transferFromAndCall
  • 两个版本的 approveAndCall

顾名思义,这些函数将执行 ERC20 操作,然后调用接收方的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);

ERC1363实现参考

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

ERC1363 使用与 ERC20 相同的余额和批准存储变量。它不存储额外信息。

继承ERC20

正如前面强调的,ERC1363 是一个具有附加功能的 ERC20 代币。构建 ERC1363 的第一步是继承 ERC20

//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(常规钱包)会回滚,所以将 ERC1363 转账给 EOA 应使用 ERC20 函数 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

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

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

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

与 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 {        
    // 需要额外的功能,以便批准的钱包将批准的 ERC1363 代币添加到此映射中
    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是否为代币合约,因为如果任何人都可以调用它,可能会导致意外行为。

使用 ERC1363 的示例接收合约:

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

一些早期的方案:

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

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

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

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

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

何时使用 ERC1363 标准

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

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

0 条评论

请先 登录 后评论
Louis
Louis
web3 developer,技术交流或者有工作机会可加VX: magicalLouis