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

ERC-1363 解决了什么问题?
假设用户将 ERC-20 代币转账给一个合约。该智能合约无法为用户的此次转账记账,因为它没有任何机制去查看是谁发起了这笔转账。
尽管事件(events)会记录这些信息,但它们只能被链下消费者使用。如果没有预言机(oracle),智能合约是无法读取事件的。
传统解决方案:接收者使用 transferFrom 将代币转移给自己,而不是通知接收者
针对上述问题的一个典型变通方案是:代币发送方授权接收方智能合约代表发送方转移代币。
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;
}
}
然后,存款人在接收方智能合约上调用一个函数(如上述示例代码中的 deposit),将代币从发送方转移到合约中。由于合约知道它从该用户那里转移了代币,因此能够正确地为其账户记账。
然而,增加一笔额外的交易来授权合约转移代币会增加 gas 成本。
此外,用户在授权合约后应该将对合约的授权额度设为零,否则存在一种危险:如果合约被利用(exploited),它可能会从用户那里提取更多的 ERC-20 代币。
转移钩子(Transfer hooks)
转移钩子是接收方智能合约中的一个预定义函数,当它接收到代币时将被调用。也就是说,代币合约在接收到转账指令后,会在接收方地址上调用这个预定义的函数。
如果该函数不存在、发生回滚(reverts)或未返回预期的成功值,则转账也会回滚。
对 ERC-721 标准中的 onERC721Received 已经很熟悉的读者,应该对转移钩子并不陌生。
ERC-1363 扩展了 ERC-20 标准,增加了转移钩子。
为了实现该标准,ERC-20 需要额外的函数(稍后解释)来转移代币以触发接收方的转移钩子,而接收方必须按照该标准实现转移钩子。
IERC1363Receiver
对于希望在收到 ERC-1363 代币时得到通知的合约,它们必须实现 IERC1363Receiver(请参阅此处 OpenZeppelin 的实现),该接口仅包含一个函数 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);
}
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-20 代币完全相同的方式与 ERC-1363 代币进行交互。同时,如果有需要,较新的协议可以利用 ERC-1363 上的转移钩子。
要成为兼容的 ERC-1363 代币,代码还必须实现六个额外的函数:
- 两个版本的
transferAndCall - 两个版本的
transferFromAndCall - 两个版本的
approveAndCall
顾名思义,这些函数将执行 ERC-20 操作,然后调用接收方的钩子函数。
每个函数都有两个版本,一个带有 data 参数,另一个没有。data 参数用于让发送方将数据转发给接收合约(稍后我们将展示其示例)。
除了带有 data 参数的函数之外,这些函数采用与其对应的 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);
源自 ERC-721 的灵感:transferFrom 与 safeTransferFrom
与 ERC-721 标准类似,ERC-1363 中 transferFromAndCall 和 transferFrom 之间的区别与 ERC-721 中 transferFrom 和 safeTransferFrom 的区别一样。然而,“safe”并不是一个理想的函数名,因为转移钩子引入了一个潜在的重入攻击(re-entrancy)向量,所以它并不“安全”。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/[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)
当且仅当接收地址实现了 onTransferReceived() 并返回 onTransferReceived() 的四字节函数选择器时,transferFromAndCall 才会成功。
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)
这与 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,如果未收到 0x88a7ca5c(即 onTransferReceived(address,address,uint256,bytes) 的函数选择器),则回滚。如果 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 {
// 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
在上述工作流程中,被调用的智能合约是 ERC-1363 代币的接收方。
然而,如果我们希望另一个合约成为我们代币的发送方怎么办?例如,路由器合约(如 Uniswap V2 Router)并不托管代币。它只是将代币转发给 Uniswap 以进行交易。
传统上,此类架构使用“先授权(approve)再转移(transferFrom)”的工作流,但在 ERC-1363 中,我们可以使用 approveAndCall 在一笔交易中完成。顾名思义,刚刚获得了花费另一个地址代币授权的合约会被调用一个特殊的钩子函数。
与 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);
}
只有代币的所有者才能授权给其他地址,因此不需要 operator 参数——在授权期间,operator 和 owner 必须是同一个地址。value 是授权数量的规模。
以下合约在收到 onApprovalReceived 后,会将代币转发到 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;
}
}
此函数应该检查 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;
}
}
试图解决代币钩子问题的早期标准
ERC-1363 并不是第一个在 ERC-20 中添加转移钩子的标准。首先,2017 年 5 月提出了 ERC-223 ,将转移钩子添加到 ERC-20 的 transfer 和 transferFrom 中。但这意味着智能合约除非实现转移钩子,否则无法接收代币。这使得该标准与那些接受 ERC-20 代币但没有转移钩子的协议_无法_向后兼容。
ERC-777 于 2017 年 11 月被引入。在该标准中,除非接收方在 ERC-1820 注册表 中注册了其地址,否则不会调用接收方的转移钩子。
然而,早期协议在设计 ERC-20 的 transfer 或 transferFrom 时,并没有考虑到会向其他合约发起外部调用。这使得这些合约容易受到重入攻击(reentrancy),因为它们并未预料到一个“ERC-20”代币会调用其他合约。有关更多信息,请参阅 Uniswap V1 重入漏洞分析。
此外,从 gas 角度来看,ERC-777 标准相当昂贵,因为它需要对 ERC-1820 注册表合约进行额外的调用。
ERC-1363 通过完全保留 ERC-20 标准中的 transfer 和 transferFrom 不变,解决了所有这些问题。所有的转移钩子都在名称中明确带有“call”的函数中被调用。
何时使用 ERC-1363 标准
任何使用 ERC-20 标准的地方都可以使用 ERC-1363 标准。在作者看来,该标准是 ERC-20 的理想替代品,因为它可以消除 ERC-20 的授权(approve)步骤,而该步骤曾导致过相当惨重的资金损失。
通过 RareSkills 了解更多
欢迎查看我们的 Solidity 训练营,以了解有关智能合约开发和代币标准的更多信息。
原文发布于 2024 年 4 月 4 日