Alert Source Discuss
🚧 Stagnant Standards Track: ERC

ERC-4546: 封装存款

用于管理资产存款的单例合约。

Authors Justice Hudson (@jchancehud)
Created 2021-12-11
Discussion Link https://ethereum-magicians.org/t/wrapped-deposit-contract-eip/7740

摘要

封装存款合约代表用户处理资产(以太币、ERC-20ERC-721)的存款。用户只需批准一次支出限额,然后可以将资产存入支持合约存款的任意数量的不同应用程序。

动机

目前在 dapps 中存入资产的用户流程不必要地昂贵且不安全。要存入 ERC-20 资产,用户必须:

  • 在存款之前,发送一笔批准交易,金额与发送的确切金额相同,然后在每次后续存款时重复此过程。
  • 在存款之前,发送一笔批准交易,金额为无限支出金额。

第一种选择不方便且昂贵。第二种选择不安全。此外,向新的或非技术用户解释批准令人困惑。这必须在_每个_支持 ERC20 存款的 dapp 中完成。

规范

封装存款合约应该部署在一个可识别的地址(例如 0x1111119a9e30bceadf9f939390293ffacef93fe9)。合约必须是不可升级的,并且不能更改状态变量。

封装存款合约必须具有以下公共函数:

depositERC20(address to, address token, uint amount) external;
depositERC721(address to, address token, uint tokenId) external;
safeDepositERC721(address to, address token, uint tokenId, bytes memory data) external;
safeDepositERC1155(address to, address token, uint tokenId, uint value, bytes calldata data) external;
batchDepositERC1155(address to, address token, uint[] calldata tokenIds, uint[] calldata values, bytes calldata data) external;
depositEther(address to) external payable;

如果 to 是代码大小为零的地址,则这些函数中的每一个都必须 revert。每个函数都必须尝试调用 to 地址上的一个方法,确认它愿意并且能够接受存款。如果此函数调用未返回 true 值,则执行必须 revert。如果资产转移不成功,则执行必须 revert。

希望接受存款的合约应该存在以下接口:

interface ERC20Receiver {
  function acceptERC20Deposit(address depositor, address token, uint amount) external returns (bool);
}

interface ERC721Receiver {
  function acceptERC721Deposit(address depositor, address token, uint tokenId) external returns (bool);
}

interface ERC1155Receiver {
  function acceptERC1155Deposit(address depositor, address token, uint tokenId, uint value, bytes calldata data) external returns (bool);
  function acceptERC1155BatchDeposit(address depositor, address token, uint[] calldata tokenIds, uint[] calldata values, bytes calldata data) external returns (bool);
}

interface EtherReceiver {
  function acceptEtherDeposit(address depositor, uint amount) external returns (bool);
}

接收合约可以根据需要实现这些函数中的任何一个。如果未实现给定的函数,则不得发送该资产类型的存款。

理由

拥有一个处理所有代币转移的合约允许用户为每个代币提交一个批准,以存入任意数量的合约。用户不必信任接收合约的代币支出批准,并且接收合约的复杂性降低,因为不必自己实现代币转移。

用户体验得到改善,因为可以使用以下消息传递方式实现简单的全局 dapp:“启用代币以在其他应用程序中使用”。

向后兼容性

此 EIP 不向后兼容。任何计划使用此存款系统的合约都必须实现特定函数才能接受存款。可升级的现有合约可以通过实现一个或多个接受存款函数来追溯添加对此 EIP 的支持。

升级后的合约可以使用旧系统(批准合约本身)和拟议的存款系统来允许存款,以保留现有批准。应提示新用户使用拟议的存款系统。

参考实现

pragma solidity ^0.7.0;

interface ERC20Receiver {
  function acceptERC20Deposit(address depositor, address token, uint amount) external returns (bool);
}

interface ERC721Receiver {
  function acceptERC721Deposit(address depositor, address token, uint tokenId) external returns (bool);
}

interface ERC1155Receiver {
  function acceptERC1155Deposit(address depositor, address token, uint tokenId, uint value, bytes calldata data) external returns (bool);
  function acceptERC1155BatchDeposit(address depositor, address token, uint[] calldata tokenIds, uint[] calldata values, bytes calldata data) external returns (bool);
}

interface EtherReceiver {
  function acceptEtherDeposit(address depositor, uint amount) external returns (bool);
}

interface IERC20 {
  function transferFrom(address sender, address recipient, uint amount) external returns (bool);
}

interface IERC721 {
  function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
  function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory data) external payable;
}

interface IERC1155 {
  function safeTransferFrom(address _from, address _to, uint _id, uint _value, bytes calldata _data) external;
  function safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external;
}

contract WrappedDeposit {
  function depositERC20(address to, address token, uint amount) public {
    _assertContract(to);
    require(ERC20Receiver(to).acceptERC20Deposit(msg.sender, token, amount));
    bytes memory data = abi.encodeWithSelector(
      IERC20(token).transferFrom.selector,
      msg.sender,
      to,
      amount
    );
    (bool success, bytes memory returndata) = token.call(data);
    require(success);
    // backward compat for tokens incorrectly implementing the transfer function
    // 用于不正确地实现转移函数的代币的向后兼容
    if (returndata.length > 0) {
      require(abi.decode(returndata, (bool)), "ERC20 operation did not succeed");
    }
  }

  function depositERC721(address to, address token, uint tokenId) public {
    _assertContract(to);
    require(ERC721Receiver(to).acceptERC721Deposit(msg.sender, token, tokenId));
    IERC721(token).transferFrom(msg.sender, to, tokenId);
  }

  function safeDepositERC721(address to, address token, uint tokenId, bytes memory data) public {
    _assertContract(to);
    require(ERC721Receiver(to).acceptERC721Deposit(msg.sender, token, tokenId));
    IERC721(token).safeTransferFrom(msg.sender, to, tokenId, data);
  }

  function safeDepositERC1155(address to, address token, uint tokenId, uint value, bytes calldata data) public {
    _assertContract(to);
    require(ERC1155Receiver(to).acceptERC1155Deposit(msg.sender, to, tokenId, value, data));
    IERC1155(token).safeTransferFrom(msg.sender, to, tokenId, value, data);
  }

  function batchDepositERC1155(address to, address token, uint[] calldata tokenIds, uint[] calldata values, bytes calldata data) public {
    _assertContract(to);
    require(ERC1155Receiver(to).acceptERC1155BatchDeposit(msg.sender, to, tokenIds, values, data));
    IERC1155(token).safeBatchTransferFrom(msg.sender, to, tokenIds, values, data);
  }

  function depositEther(address to) public payable {
    _assertContract(to);
    require(EtherReceiver(to).acceptEtherDeposit(msg.sender, msg.value));
    (bool success, ) = to.call{value: msg.value}('');
    require(success, "nonpayable");
  }

  function _assertContract(address c) private view {
    uint size;
    assembly {
      size := extcodesize(c)
    }
    require(size > 0, "noncontract");
  }
}

安全考虑

封装的存款实现应尽可能小,以降低出现错误的风险。合约应该足够小,以便工程师可以在几分钟内阅读和理解它。

接收合约必须验证 msg.sender 是否等于封装的存款合约。否则,任何人都可以模拟存款。

版权

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

Citation

Please cite this document as:

Justice Hudson (@jchancehud), "ERC-4546: 封装存款 [DRAFT]," Ethereum Improvement Proposals, no. 4546, December 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4546.