Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7246: Encumber - 分割所有权和保证

一个 token 接口,允许抵押 token 而无需转移所有权。

Authors Coburn Berry (@coburncoburn), Mykel Pereira (@mykelp), Scott Silver (@scott-silver)
Created 2023-06-27
Discussion Link https://ethereum-magicians.org/t/encumber-extending-the-erc-20-token-standard-to-allow-pledging-tokens-without-giving-up-ownership/14849
Requires EIP-20

摘要

这个 ERC 提出通过添加 Encumber 来扩展 ERC-20 token 标准——Encumber 允许一个账户授予另一个账户独占的权利来移动其部分余额。Encumber 是 ERC-20 授权的一个更强的版本。虽然 ERC-20 approve 授予另一个账户转移指定 token 数量的权限,但 encumber 在确保 token 在需要时可用的前提下授予相同的权限。

动机

这个扩展增加了 ERC-20 token 标准的灵活性,并满足了需要锁定 token 的用例,但在这种情况下,保持 token 的实际所有权是更优的选择。这个接口也可以以一种直接的方式适应其他 token 标准,例如 ERC-721

Token 持有者通常将他们的 token 转移到智能合约,这些合约将在特定条件下返回 token。在某些情况下,智能合约实际上不需要持有 token,但需要保证它们在必要时可用。由于授权不能提供足够强的保证,目前保证 token 可用性的唯一方法是将 token 转移到智能合约。锁定 token 而不移动它们可以更清楚地表明 token 的权利和所有权。这允许空投和所有权的其他辅助利益到达真正的所有者。它还增加了另一层安全性,其中耗尽 ERC-20 token 池可以在一次转移中完成,迭代账户以转移 encumber 的 token 在 gas 使用方面将具有显著的抑制作用。

规范

本文档中的关键词“必须”、“不得”、“必需”、“应该”、“不应”、“推荐”、“不推荐”、“可以”和“可选”应按照 RFC 2119 和 RFC 8174 中的描述进行解释。

一个兼容的 token 必须实现以下接口

/**
 * @dev ERC-7246 标准的接口。
 */
interface IERC7246{
    /**
     * @dev 当从 `owner` 到 `taker` encumber 了 `amount` 个 token 时触发。
     */
    event Encumber(address indexed owner, address indexed taker, uint amount);

    /**
     * @dev 当 `taker` 对 `owner` 的 encumber 减少了 `amount` 时触发。
     */
    event Release(address indexed owner, address indexed taker, uint amount);

    /**
     * @dev 返回 `owner` 拥有的当前被 encumber 的 token 总量。
     * 必须永远不超过 `balanceOf(owner)`
     *
     * 任何会将 balanceOf(owner) 减少到 encumberedBalanceOf(owner) 以下的函数都必须 revert
     */
    function encumberedBalanceOf(address owner) external returns (uint);

    /**
     * @dev 返回 `owner` encumber 给 `taker` 的 token 数量。
     *
     * 当 `owner` 或另一个被允许的账户调用 {encumber} 或 {encumberFrom} 时,这个值会增加。
     * 当 `taker` 调用 {release} 和 {transferFrom} 时,这个值会减少。
     */
    function encumbrances(address owner, address taker) external returns (uint);

    /**
     * @dev 将调用者 encumber 给 `taker` 的 token 数量增加 `amount`。
     * 授予 `taker` 一种保证的权利,可以通过使用 `transferFrom` 从调用者的余额中转移 `amount`。
     *
     * 如果调用者没有 `amount` 个可用的 token(例如,如果 `balanceOf(caller) - encumbrances(caller) < amount`),则必须 revert。
     *
     * 触发一个 {Encumber} 事件。
     */
    function encumber(address taker, uint amount) external;

    /**
     * @dev 将 `owner` encumber 给 `taker` 的 token 数量增加 `amount`。
     * 授予 `taker` 一种保证的权利,可以通过使用 transferFrom 从 `owner` 处转移 `amount`
     *
     * 除非所有者账户通过某种机制故意授权了消息的发送者,否则该函数应该 revert。
     *
     * 如果 `owner` 没有 `amount` 个可用的 token(例如,如果 `balanceOf(owner) - encumbrances(owner) < amount`),则必须 revert。
     *
     * 触发一个 {Encumber} 事件。
     */
    function encumberFrom(address owner, address taker, uint amount) external;

    /**
     * @dev 将从 `owner` 到调用者的 encumber 的 token 数量减少 `amount`
     *
     * 触发一个 {Release} 事件。
     */
    function release(address owner, uint amount) external;


    /**
     * @dev 用于读取地址的未 encumber 余额的便捷函数。
     * 可以简单地实现为 `balanceOf(owner) - encumberedBalanceOf(owner)`
     */
    function availableBalanceOf(address owner) public view returns (uint);
}

原理

该规范旨在补充和镜像 ERC-20 规范,以方便采用和理解。该规范有意地尽可能少地规定这种连接,其中唯一真正的要求是所有者不能转移 encumber 的 token。但是,示例实现包括一些关于在哪里连接 ERC-20 函数的值得注意的决策。它被设计为对标准 ERC-20 定义的最小侵入性。 - 该示例依赖于 approve 来促进 encumberFrom。如果需要,这个提案允许实现者定义另一种机制,例如 approveEncumber,它将镜像 ERC-20 授权。 - transferFrom(src, dst, amount) 被编写为首先减少 encumbrances(src, amount),然后随后从 allowance(src, msg.sender) 中支出。或者,可以实现 transferFrom 以同时从授权中支出 encumber。这将要求 approve 检查批准的余额不会减少到 encumber 余额所需的金额以下,并且还要使创建授权成为调用 encumber 的先决条件。

通过使用 tokenId 代替 amount 参数,可以将 Encumber 接口扩展到覆盖 ERC-721 token,因为它们都是 uint。该接口选择了最可能用例(ERC-20)的清晰性,即使它与其他格式兼容。

向后兼容性

此 EIP 与现有的 ERC-20 标准向后兼容。实现必须添加阻止转移 encumber 给另一个账户的 token 的功能。

参考实现

这可以通过修改转移函数来阻止转移 encumber 的 token,并在通过 transferFrom 支出时释放 encumber,从而实现为任何基本 ERC-20 合同的扩展。

// 一个通过阻止转移来实现 encumber 接口的 erc-20 token。

pragma solidity ^0.8.0;
import {ERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import { IERC7246 } from "./IERC7246.sol";

contract EncumberableERC20 is ERC20, IERC7246 {
    // Owner -> Taker -> 可以被拿走的金额
    mapping (address => mapping (address => uint)) public encumbrances;

    // token 所有者的 encumber 余额。对于一个用户来说,encumberedBalance 必须不超过 balanceOf
    // 请注意,这意味着重新调整 token 存在减少和违反此协议的风险
    mapping (address => uint) public encumberedBalanceOf;

    address public minter;

    constructor(string memory name, string memory symbol) ERC20(name, symbol) {
        minter = msg.sender;
    }

    function mint(address recipient, uint amount) public {
        require(msg.sender == minter, "only minter");
        _mint(recipient, amount);
    }

    function encumber(address taker, uint amount) external {
        _encumber(msg.sender, taker, amount);
    }

    function encumberFrom(address owner, address taker, uint amount) external {
        require(allowance(owner, msg.sender) >= amount);
       _encumber(owner, taker, amount);
    }

    function release(address owner, uint amount) external {
        _release(owner, msg.sender, amount);
    }

    // 如果使余额和 encumber 更接近相等,则必须检查
    function availableBalanceOf(address a) public view returns (uint) {
        return (balanceOf(a) - encumberedBalanceOf[a]);
    }

    function _encumber(address owner, address taker, uint amount) private {
        require(availableBalanceOf(owner) >= amount, "insufficient balance");
        encumbrances[owner][taker] += amount;
        encumberedBalanceOf[owner] += amount;
        emit Encumber(owner, taker, amount);
    }

    function _release(address owner, address taker, uint amount) private {
        if (encumbrances[owner][taker] < amount) {
          amount = encumbrances[owner][taker];
        }
        encumbrances[owner][taker] -= amount;
        encumberedBalanceOf[owner] -= amount;
        emit Release(owner, taker, amount);
    }

    function transfer(address dst, uint amount) public override returns (bool) {
        // 检查但不花费 encumber
        require(availableBalanceOf(msg.sender) >= amount, "insufficient balance");
        _transfer(msg.sender, dst, amount);
        return true;
    }

    function transferFrom(address src, address dst, uint amount) public override returns (bool) {
        uint encumberedToTaker = encumbrances[src][msg.sender];
        bool exceedsEncumbrance = amount > encumberedToTaker;
        if (exceedsEncumbrance)  {
            uint excessAmount = amount - encumberedToTaker;

            // 检查是否存在足够的 enencumber token 以从授权中支出
           require(availableBalanceOf(src) >= excessAmount, "insufficient balance");

           // 超过 Encumber,所以花费所有
            _spendEncumbrance(src, msg.sender, encumberedToTaker);

            _spendAllowance(src, dst, excessAmount);
        } else {
            _spendEncumbrance(src, msg.sender, amount);
        }

        _transfer(src, dst, amount);
        return true;
    }

    function _spendEncumbrance(address owner, address taker, uint256 amount) internal virtual {
        uint256 currentEncumbrance = encumbrances[owner][taker];
        require(currentEncumbrance >= amount, "insufficient encumbrance");
        uint newEncumbrance = currentEncumbrance - amount;
        encumbrances[owner][taker] = newEncumbrance;
        encumberedBalanceOf[owner] -= amount;
    }
}

安全考虑

依赖 balanceOf 来确定可用于转移的 token 数量的各方应改为依赖 balanceOf(account) - encumberedBalance(account),或者,如果已实现,则依赖 availableBalanceOf(account)

可以通过更改 transfertransferFrom 来阻止,从而以一种直接的方式实现 encumber 余额始终由 token 余额支持的属性。如果还有其他可以更改用户余额的函数,例如重新调整 token 或管理员销毁函数,则实现者必须添加额外的保护措施,以同样确保这些函数阻止将任何给定账户的 balanceOf(account) 减少到 encumberedBalanceOf(account) 以下。

版权

版权和相关权利通过 CC0 放弃。

Citation

Please cite this document as:

Coburn Berry (@coburncoburn), Mykel Pereira (@mykelp), Scott Silver (@scott-silver), "ERC-7246: Encumber - 分割所有权和保证 [DRAFT]," Ethereum Improvement Proposals, no. 7246, June 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7246.