ERC-1363 Payable Token 深度学习文档
基于 RareSkills 文章整理,面向有 Solidity 基础的中级开发者 参考来源:https://rareskills.io/post/erc-1363
一、什么是 ERC-1363?
ERC-1363 是 ERC-20 的一个扩展标准,被称为 “Payable Token”(可支付代币)。
核心能力:
- 在 token 转账的同一笔交易中,自动调用接收方合约的回调函数
- 在 token 授权的同一笔交易中,自动调用被授权方合约的回调函数
- 将”转账 + 业务逻辑”合并为一步操作
一句话总结: ERC-1363 让 ERC-20 token 的转账和授权可以”附带通知”,接收方合约能够立即响应。
二、为什么需要 ERC-1363?—— ERC-20 的两步问题
2.1 ERC-20 的经典痛点:Approve + TransferFrom 两步操作
当用户想要通过 token 支付来使用某个 DApp 服务时,标准 ERC-20 需要两笔交易:
交易 1: 用户调用 token.approve(dappContract, amount) → 授权
交易 2: DApp 合约调用 token.transferFrom(user, dapp, amount) → 实际扣款
问题:
| 痛点 | 说明 |
|---|---|
| UX 糟糕 | 用户需要签署两次交易,新手极易困惑 |
| 多花 Gas | 两笔交易 = 两次 Gas 费 |
| 无法原子性 | 授权后用户可能不完成第二步,状态悬挂 |
| 合约无法感知转入 | ERC-20 的 transfer() 不会通知接收方合约 |
2.2 类比理解
- ERC-20 转账 → 像把钱丢进一个没人看管的信箱,收件人不知道钱来了
- ERC-1363 转账 → 像亲手递钱给收银员,收银员立刻开票并提供服务
- ETH 转账 → 原生就会触发
receive()/fallback(),ERC-1363 让 token 也有类似能力
2.3 ERC-1363 如何解决
一笔交易: 用户调用 token.transferAndCall(dappContract, amount, data)
→ 转账 + 通知 DApp 合约 → DApp 立即执行业务逻辑
三、ERC-1363 的接口定义
3.1 Token 合约侧的接口(IERC1363)
interface IERC1363 is IERC20, IERC165 {
/// @notice 转账并调用接收方的 onTransferReceived
function transferAndCall(address to, uint256 value) external returns (bool);
function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool);
/// @notice 代理转账并调用接收方的 onTransferReceived
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);
/// @notice 授权并调用被授权方的 onApprovalReceived
function approveAndCall(address spender, uint256 value) external returns (bool);
function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool);
}
3.2 接收方合约的接口(IERC1363Receiver)
interface IERC1363Receiver {
/// @notice 当 token 通过 transferAndCall/transferFromAndCall 转入时被调用
/// @param operator 调用 transferAndCall 的地址(msg.sender)
/// @param from token 的实际持有者(转出方)
/// @param value 转入的 token 数量
/// @param data 附带的任意数据
/// @return bytes4 必须返回 magic value 确认接收
function onTransferReceived(
address operator,
address from,
uint256 value,
bytes calldata data
) external returns (bytes4);
}
Magic Value: bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)")) = 0x88a7ca5c
3.3 被授权方合约的接口(IERC1363Spender)
interface IERC1363Spender {
/// @notice 当 token 通过 approveAndCall 被授权时调用
/// @param owner token 持有者(授权方)
/// @param value 授权的 token 数量
/// @param data 附带的任意数据
/// @return bytes4 必须返回 magic value 确认接收
function onApprovalReceived(
address owner,
uint256 value,
bytes calldata data
) external returns (bytes4);
}
Magic Value: bytes4(keccak256("onApprovalReceived(address,uint256,bytes)")) = 0x7b04a2d0
四、核心函数执行流程详解
4.1 transferAndCall 完整流程
用户 → token.transferAndCall(receiverContract, 100e18, data)
│
├─ Step 1: 执行标准 transfer(to, value)
│ 更新 balances mapping
│
├─ Step 2: 检查 to 是否是合约(to.code.length > 0)
│
├─ Step 3: 调用 IERC1363Receiver(to).onTransferReceived(
│ msg.sender, // operator
│ msg.sender, // from(transferAndCall 时两者相同)
│ value,
│ data
│ )
│
└─ Step 4: 验证返回值 == 0x88a7ca5c(magic value)
如果不是 → revert
4.2 transferFromAndCall 完整流程
operatorContract → token.transferFromAndCall(alice, receiverContract, 100e18, data)
│
├─ Step 1: 执行标准 transferFrom(from, to, value)
│ 检查 allowance → 更新 balances → 扣减 allowance
│
├─ Step 2: 检查 to 是否是合约
│
├─ Step 3: 调用 IERC1363Receiver(to).onTransferReceived(
│ msg.sender, // operator(调用者)
│ alice, // from(token 实际持有者)
│ value,
│ data
│ )
│
└─ Step 4: 验证返回值 == 0x88a7ca5c
注意 operator 和 from 的区别:
operator:实际调用transferFromAndCall的地址from:token 被转出的地址- 在
transferAndCall中,两者相同 - 在
transferFromAndCall中,两者可能不同
4.3 approveAndCall 完整流程
用户 → token.approveAndCall(spenderContract, 500e18, data)
│
├─ Step 1: 执行标准 approve(spender, value)
│ 更新 allowances mapping
│
├─ Step 2: 检查 spender 是否是合约
│
├─ Step 3: 调用 IERC1363Spender(spender).onApprovalReceived(
│ msg.sender, // owner(授权方)
│ value,
│ data
│ )
│
└─ Step 4: 验证返回值 == 0x7b04a2d0
五、内部实现源码详解
5.1 transferAndCall 实现
function transferAndCall(address to, uint256 value, bytes memory data) public returns (bool) {
// Step 1: 执行标准 ERC-20 transfer
transfer(to, value);
// Step 2 & 3: 检查回调
require(
_checkOnTransferReceived(msg.sender, msg.sender, to, value, data),
"ERC1363: receiver returned wrong data"
);
return true;
}
5.2 _checkOnTransferReceived 内部检查函数
function _checkOnTransferReceived(
address operator,
address from,
address to,
uint256 value,
bytes memory data
) internal returns (bool) {
// 如果接收方不是合约,直接返回 true(EOA 可以正常接收)
if (to.code.length == 0) {
return true;
}
// 调用接收方合约的 onTransferReceived
try IERC1363Receiver(to).onTransferReceived(operator, from, value, data) returns (bytes4 retval) {
// 验证返回值是否是正确的 magic value
return retval == IERC1363Receiver.onTransferReceived.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
revert("ERC1363: transfer to non-ERC1363Receiver implementer");
} else {
assembly {
revert(add(32, reason), mload(reason))
}
}
}
}
5.3 approveAndCall 实现
function approveAndCall(address spender, uint256 value, bytes memory data) public returns (bool) {
approve(spender, value);
require(
_checkOnApprovalReceived(spender, value, data),
"ERC1363: spender returned wrong data"
);
return true;
}
5.4 _checkOnApprovalReceived 内部检查函数
function _checkOnApprovalReceived(
address spender,
uint256 value,
bytes memory data
) internal returns (bool) {
if (spender.code.length == 0) {
return true;
}
try IERC1363Spender(spender).onApprovalReceived(msg.sender, value, data) returns (bytes4 retval) {
return retval == IERC1363Spender.onApprovalReceived.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
revert("ERC1363: approve a non-ERC1363Spender implementer");
} else {
assembly {
revert(add(32, reason), mload(reason))
}
}
}
}
六、ERC-165 接口检测
ERC-1363 要求实现 ERC-165 的 supportsInterface,使得其他合约可以在运行时检测是否支持 ERC-1363。
function supportsInterface(bytes4 interfaceId) public view override returns (bool) {
return interfaceId == type(IERC1363).interfaceId ||
interfaceId == type(IERC20).interfaceId ||
interfaceId == type(IERC165).interfaceId ||
super.supportsInterface(interfaceId);
}
接口 ID 列表:
| 接口 | Interface ID | 说明 |
|---|---|---|
| IERC165 | 0x01ffc9a7 | 基础接口检测 |
| IERC1363 | XOR of all function selectors | Token 合约侧 |
| IERC1363Receiver | 0x88a7ca5c | 接收方合约 |
| IERC1363Spender | 0x7b04a2d0 | 被授权方合约 |
七、Magic Value(魔术值)机制详解
7.1 为什么需要 Magic Value?
Magic Value 是一种安全确认机制:
Token 合约: "嘿,我把 100 token 转给你了,你能处理吗?"
接收方合约: 返回 0x88a7ca5c → "能,我已处理"
接收方合约: 返回其他值或 revert → "不能"→ 整个交易回滚
防止意外转入: 如果接收方合约没有实现 IERC1363Receiver,调用会失败,token 不会丢失。
7.2 Magic Value 的计算
// onTransferReceived 的 magic value
bytes4 constant MAGIC = bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"));
// = 0x88a7ca5c
// 等价于 IERC1363Receiver.onTransferReceived.selector
// onApprovalReceived 的 magic value
bytes4 constant MAGIC = bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"));
// = 0x7b04a2d0
// 等价于 IERC1363Spender.onApprovalReceived.selector
7.3 类比 ERC-721 的 onERC721Received
这个模式与 ERC-721 的 safeTransferFrom 完全一致:
- ERC-721:
onERC721Received→ 返回0x150b7a02 - ERC-1363:
onTransferReceived→ 返回0x88a7ca5c
八、data 参数的妙用
transferAndCall 和 approveAndCall 都支持传入一个 bytes calldata data 参数,这是极其灵活的设计:
8.1 用途示例
// 传递订单 ID
bytes memory data = abi.encode(orderId);
token.transferAndCall(shopContract, price, data);
// 传递操作类型
bytes memory data = abi.encode("stake", lockPeriod);
token.transferAndCall(stakingContract, amount, data);
// 传递多个参数
bytes memory data = abi.encode(productId, quantity, couponCode);
token.transferAndCall(merchantContract, totalPrice, data);
8.2 接收方解码 data
function onTransferReceived(
address operator,
address from,
uint256 value,
bytes calldata data
) external returns (bytes4) {
// 解码 data
(uint256 orderId) = abi.decode(data, (uint256));
// 执行业务逻辑
_processOrder(from, orderId, value);
return this.onTransferReceived.selector;
}
九、实际应用场景与代码
9.1 Token 支付服务(一步购买)
contract DigitalShop is IERC1363Receiver {
IERC20 public paymentToken;
mapping(address => bool) public hasPurchased;
uint256 public price = 100 * 1e18;
constructor(address _token) {
paymentToken = IERC20(_token);
}
function onTransferReceived(
address,
address from,
uint256 value,
bytes calldata
) external override returns (bytes4) {
require(msg.sender == address(paymentToken), "Wrong token");
require(value >= price, "Insufficient payment");
hasPurchased[from] = true;
return this.onTransferReceived.selector;
}
}
用户调用:
token.transferAndCall(address(shop), 100e18, "");
// 一笔交易:支付 + 获得访问权
9.2 Staking 合约
contract TokenStaking is IERC1363Receiver {
IERC20 public stakingToken;
mapping(address => uint256) public stakes;
mapping(address => uint256) public stakedAt;
constructor(address _token) {
stakingToken = IERC20(_token);
}
function onTransferReceived(
address,
address from,
uint256 value,
bytes calldata
) external override returns (bytes4) {
require(msg.sender == address(stakingToken), "Wrong token");
stakes[from] += value;
stakedAt[from] = block.timestamp;
return this.onTransferReceived.selector;
}
function unstake(uint256 amount) external {
require(stakes[msg.sender] >= amount, "Insufficient stake");
require(block.timestamp >= stakedAt[msg.sender] + 7 days, "Lock period");
stakes[msg.sender] -= amount;
stakingToken.transfer(msg.sender, amount);
}
}
用户调用:
token.transferAndCall(address(staking), 1000e18, "");
// 一步完成质押,无需先 approve
9.3 订阅服务
contract SubscriptionService is IERC1363Receiver {
IERC20 public token;
mapping(address => uint256) public expiresAt;
uint256 public monthlyFee = 10 * 1e18;
constructor(address _token) {
token = IERC20(_token);
}
function onTransferReceived(
address,
address from,
uint256 value,
bytes calldata
) external override returns (bytes4) {
require(msg.sender == address(token), "Wrong token");
require(value >= monthlyFee, "Insufficient payment");
uint256 months = value / monthlyFee;
uint256 startTime = expiresAt[from] > block.timestamp ?
expiresAt[from] : block.timestamp;
expiresAt[from] = startTime + (months * 30 days);
return this.onTransferReceived.selector;
}
function isActive(address user) external view returns (bool) {
return expiresAt[user] > block.timestamp;
}
}
9.4 使用 approveAndCall 的场景(DeFi 存款)
contract VaultDeposit is IERC1363Spender {
IERC20 public token;
mapping(address => uint256) public deposits;
constructor(address _token) {
token = IERC20(_token);
}
function onApprovalReceived(
address owner,
uint256 value,
bytes calldata data
) external override returns (bytes4) {
require(msg.sender == address(token), "Wrong token");
// 收到授权通知后,立即拉取 token
token.transferFrom(owner, address(this), value);
deposits[owner] += value;
return this.onApprovalReceived.selector;
}
}
用户调用:
token.approveAndCall(address(vault), 500e18, "");
// 一步完成:授权 + 合约自动拉取 + 记账
十、安全注意事项
10.1 重入攻击(Reentrancy)
ERC-1363 的回调机制天然引入了重入风险!
攻击流程:
1. 攻击者调用 token.transferAndCall(maliciousContract, amount, data)
2. token 合约执行 transfer → 更新余额
3. token 合约调用 maliciousContract.onTransferReceived()
4. maliciousContract 在回调中再次调用 transferAndCall → 重入!
防御措施:
// 方法 1: Checks-Effects-Interactions 模式
function transferAndCall(address to, uint256 value, bytes memory data) public returns (bool) {
// Effects: 先更新状态
_transfer(msg.sender, to, value);
// Interactions: 最后进行外部调用
_checkOnTransferReceived(msg.sender, msg.sender, to, value, data);
return true;
}
// 方法 2: ReentrancyGuard
function transferAndCall(...) public nonReentrant returns (bool) { ... }
10.2 msg.sender 验证
接收方合约必须验证 msg.sender 是合法的 token 合约:
function onTransferReceived(...) external returns (bytes4) {
// ⚠️ 关键安全检查!
require(msg.sender == address(expectedToken), "Unauthorized token");
// ...
}
如果不验证: 任何人可以部署一个假 token 合约调用你的 onTransferReceived,欺骗你执行业务逻辑。
10.3 返回值验证
Token 合约必须严格验证回调返回值:
// ✅ 正确:验证返回值
require(retval == IERC1363Receiver.onTransferReceived.selector);
// ❌ 错误:不验证返回值
IERC1363Receiver(to).onTransferReceived(...); // 即使返回错误值也不会 revert
10.4 data 参数的解码安全
// ⚠️ 要考虑 data 为空的情况
if (data.length > 0) {
(uint256 orderId) = abi.decode(data, (uint256));
// ...
}
十一、ERC-1363 vs ERC-777 vs ERC-20 对比
| 特性 | ERC-20 | ERC-777 | ERC-1363 |
|---|---|---|---|
| 转账触发回调 | ❌ | ✅ 所有转账自动触发 | ✅ 仅 *AndCall 方法触发 |
| 授权触发回调 | ❌ | ❌ | ✅ approveAndCall |
| 向后兼容 ERC-20 | - | 部分兼容 | ✅ 完全兼容 |
| 需要 ERC-1820 Registry | ❌ | ✅ | ❌ |
| 回调是否可选 | - | 强制(registered) | ✅ 用户选择调用哪个方法 |
| 复杂度 | 低 | 高 | 中 |
| operator 机制 | ❌ | ✅(复杂) | ✅(简单,仅区分 operator/from) |
| 安全风险 | 低 | 较高(所有转账都回调) | 中(仅特定方法回调) |
为什么选 ERC-1363 而不是 ERC-777?
- 更简单:无需部署和查询 ERC-1820 注册表
- 更可控:只有调用
*AndCall方法才触发回调,普通transfer行为不变 - 更安全:不会意外触发未知合约代码
- 完全兼容:所有 ERC-20 的工具、钱包、交易所无需任何修改
十二、完整 ERC-1363 Token 合约示例
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "@openzeppelin/contracts/utils/Address.sol";
interface IERC1363 is IERC20, IERC165 {
function transferAndCall(address to, uint256 value) external returns (bool);
function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool);
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);
function approveAndCall(address spender, uint256 value) external returns (bool);
function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool);
}
interface IERC1363Receiver {
function onTransferReceived(address operator, address from, uint256 value, bytes calldata data) external returns (bytes4);
}
interface IERC1363Spender {
function onApprovalReceived(address owner, uint256 value, bytes calldata data) external returns (bytes4);
}
contract MyPayableToken is ERC20, ERC165, IERC1363 {
constructor() ERC20("PayableToken", "PAY") {
_mint(msg.sender, 1_000_000 * 1e18);
}
// ===== ERC-165 =====
function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
return interfaceId == type(IERC1363).interfaceId || super.supportsInterface(interfaceId);
}
// ===== transferAndCall =====
function transferAndCall(address to, uint256 value) public override returns (bool) {
return transferAndCall(to, value, "");
}
function transferAndCall(address to, uint256 value, bytes calldata data) public override returns (bool) {
transfer(to, value);
_checkOnTransferReceived(msg.sender, msg.sender, to, value, data);
return true;
}
// ===== transferFromAndCall =====
function transferFromAndCall(address from, address to, uint256 value) public override returns (bool) {
return transferFromAndCall(from, to, value, "");
}
function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) public override returns (bool) {
transferFrom(from, to, value);
_checkOnTransferReceived(msg.sender, from, to, value, data);
return true;
}
// ===== approveAndCall =====
function approveAndCall(address spender, uint256 value) public override returns (bool) {
return approveAndCall(spender, value, "");
}
function approveAndCall(address spender, uint256 value, bytes calldata data) public override returns (bool) {
approve(spender, value);
_checkOnApprovalReceived(spender, value, data);
return true;
}
// ===== 内部检查函数 =====
function _checkOnTransferReceived(
address operator, address from, address to, uint256 value, bytes memory data
) private {
if (to.code.length > 0) {
bytes4 retval = IERC1363Receiver(to).onTransferReceived(operator, from, value, data);
require(retval == IERC1363Receiver.onTransferReceived.selector, "ERC1363: invalid receiver");
}
}
function _checkOnApprovalReceived(
address spender, uint256 value, bytes memory data
) private {
if (spender.code.length > 0) {
bytes4 retval = IERC1363Spender(spender).onApprovalReceived(msg.sender, value, data);
require(retval == IERC1363Spender.onApprovalReceived.selector, "ERC1363: invalid spender");
}
}
}
十三、函数调用关系图
┌─────────────────────────────────────────────────────────┐
│ 用户 / EOA / 合约 │
└──────────────┬──────────────────┬───────────────────────┘
│ │
transferAndCall() approveAndCall()
│ │
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ ERC-1363 Token 合约 │
│ │
│ 1. 执行 transfer/approve(更新状态) │
│ 2. 检查接收方是否是合约 │
│ 3. 调用接收方的回调函数 │
│ 4. 验证返回的 magic value │
└──────────────┬──────────────────┬───────────────────────┘
│ │
onTransferReceived() onApprovalReceived()
│ │
▼ ▼
┌──────────────────────┐ ┌───────────────────────┐
│ IERC1363Receiver │ │ IERC1363Spender │
│ (接收方合约) │ │ (被授权方合约) │
│ │ │ │
│ - 执行业务逻辑 │ │ - 收到授权通知 │
│ - 返回 0x88a7ca5c │ │ - 可立即 transferFrom │
│ │ │ - 返回 0x7b04a2d0 │
└──────────────────────┘ └───────────────────────┘
十四、关键概念回顾
- ERC-20 两步问题 — approve + transferFrom 需要两笔交易,UX 差、Gas 高
- transferAndCall — 转账 + 回调合二为一,接收方立即感知
- approveAndCall — 授权 + 通知合二为一,被授权方可立即行动
- Magic Value — 回调函数必须返回正确的 selector 确认处理,防止误转
- msg.sender 验证 — 接收方必须验证调用者是合法 token 合约
- ERC-165 — 运行时接口检测,确认合约是否支持 ERC-1363
- Checks-Effects-Interactions — 先更新状态再回调,防止重入
- data 参数 — 灵活的附加数据通道,可传递订单信息、操作类型等
- 与 ERC-777 的区别 — ERC-1363 更简单、更可控、无需 ERC-1820 注册表
- 向后兼容 — 普通
transfer()/approve()行为完全不变
十五、练习建议
- 实现一个 Crowdfunding 合约:使用
IERC1363Receiver接收捐款,自动记录每个支持者 - 实现一个 NFT Mint 合约:用户通过
transferAndCall支付 ERC-1363 token,自动 mint NFT - 测试重入攻击:编写一个恶意的
onTransferReceived尝试重入,验证防御措施是否有效 - 对比 Gas 消耗:测量
approve + transferFrom(两笔交易)vstransferAndCall(一笔交易)的 Gas 差异 - 实现 ERC-165 检测:编写一个合约,在接受 token 前先检查是否实现了 IERC1363
文档整理日期:2026-05-31 参考来源:RareSkills - ERC-1363 Payable Token