ERC-1363 Payable Token:一笔交易完成转账与回调

深入解析 ERC-1363 Payable Token 标准,了解 transferAndCall/approveAndCall 如何将 ERC-20 的两步操作合并为一步,附接口定义、回调机制与实战合约示例。

· ☕ 14 分钟阅读
ERC-1363 Payable Token:一笔交易完成转账与回调

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

注意 operatorfrom 的区别:

  • 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-165supportsInterface,使得其他合约可以在运行时检测是否支持 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说明
IERC1650x01ffc9a7基础接口检测
IERC1363XOR of all function selectorsToken 合约侧
IERC1363Receiver0x88a7ca5c接收方合约
IERC1363Spender0x7b04a2d0被授权方合约

七、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 参数的妙用

transferAndCallapproveAndCall 都支持传入一个 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-20ERC-777ERC-1363
转账触发回调✅ 所有转账自动触发✅ 仅 *AndCall 方法触发
授权触发回调approveAndCall
向后兼容 ERC-20-部分兼容✅ 完全兼容
需要 ERC-1820 Registry
回调是否可选-强制(registered)✅ 用户选择调用哪个方法
复杂度
operator 机制✅(复杂)✅(简单,仅区分 operator/from)
安全风险较高(所有转账都回调)中(仅特定方法回调)

为什么选 ERC-1363 而不是 ERC-777?

  1. 更简单:无需部署和查询 ERC-1820 注册表
  2. 更可控:只有调用 *AndCall 方法才触发回调,普通 transfer 行为不变
  3. 更安全:不会意外触发未知合约代码
  4. 完全兼容:所有 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    │
└──────────────────────┘  └───────────────────────┘

十四、关键概念回顾

  1. ERC-20 两步问题 — approve + transferFrom 需要两笔交易,UX 差、Gas 高
  2. transferAndCall — 转账 + 回调合二为一,接收方立即感知
  3. approveAndCall — 授权 + 通知合二为一,被授权方可立即行动
  4. Magic Value — 回调函数必须返回正确的 selector 确认处理,防止误转
  5. msg.sender 验证 — 接收方必须验证调用者是合法 token 合约
  6. ERC-165 — 运行时接口检测,确认合约是否支持 ERC-1363
  7. Checks-Effects-Interactions — 先更新状态再回调,防止重入
  8. data 参数 — 灵活的附加数据通道,可传递订单信息、操作类型等
  9. 与 ERC-777 的区别 — ERC-1363 更简单、更可控、无需 ERC-1820 注册表
  10. 向后兼容 — 普通 transfer()/approve() 行为完全不变

十五、练习建议

  1. 实现一个 Crowdfunding 合约:使用 IERC1363Receiver 接收捐款,自动记录每个支持者
  2. 实现一个 NFT Mint 合约:用户通过 transferAndCall 支付 ERC-1363 token,自动 mint NFT
  3. 测试重入攻击:编写一个恶意的 onTransferReceived 尝试重入,验证防御措施是否有效
  4. 对比 Gas 消耗:测量 approve + transferFrom(两笔交易)vs transferAndCall(一笔交易)的 Gas 差异
  5. 实现 ERC-165 检测:编写一个合约,在接受 token 前先检查是否实现了 IERC1363

文档整理日期:2026-05-31 参考来源:RareSkills - ERC-1363 Payable Token

💬 评论