Abstract

The wrapped deposit contract handles deposits of assets (Ether, ERC-20, ERC-721) on behalf of a user. A user must only approve a spend limit once and then an asset may be deposited to any number of different applications that support deposits from the contract.

Motivation

The current user flow for depositing assets in dapps is unnecessarily expensive and insecure. To deposit an ERC-20 asset a user must either:

  • send an approve transaction for the exact amount being sent, before making a deposit, and then repeat this process for every subsequent deposit.
  • send an approve transaction for an infinite spend amount before making deposits.

The first option is inconvenient, and expensive. The second option is insecure. Further, explaining approvals to new or non-technical users is confusing. This has to be done in every dapp that supports ERC20 deposits.

Specification

The wrapped deposit contract SHOULD be deployed at an identifiable address (e.g. 0x1111119a9e30bceadf9f939390293ffacef93fe9). The contract MUST be non-upgradable with no ability for state variables to be changed.

The wrapped deposit contract MUST have the following public functions:

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;

Each of these functions MUST revert if to is an address with a zero code size. Each function MUST attempt to call a method on the to address confirming that it is willing and able to accept the deposit. If this function call does not return a true value execution MUST revert. If the asset transfer is not successful execution MUST revert.

The following interfaces SHOULD exist for contracts wishing to accept deposits:

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);
}

A receiving contract MAY implement any of these functions as desired. If a given function is not implemented deposits MUST not be sent for that asset type.

Rationale

Having a single contract that processes all token transfers allows users to submit a single approval per token to deposit to any number of contracts. The user does not have to trust receiving contracts with token spend approvals and receiving contracts have their complexity reduced by not having to implement token transfers themselves.

User experience is improved because a simple global dapp can be implemented with the messaging: “enable token for use in other apps”.

Backwards Compatibility

This EIP is not backward compatible. Any contract planning to use this deposit system must implement specific functions to accept deposits. Existing contracts that are upgradeable can add support for this EIP retroactively by implementing one or more accept deposit functions.

Upgraded contracts can allow deposits using both the old system (approving the contract itself) and the proposed deposit system to preserve existing approvals. New users should be prompted to use the proposed deposit system.

Reference Implementation

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");
  }
}

Security Considerations

The wrapped deposit implementation should be as small as possible to reduce the risk of bugs. The contract should be small enough that an engineer can read and understand it in a few minutes.

Receiving contracts MUST verify that msg.sender is equal to the wrapped deposit contract. Failing to do so allows anyone to simulate deposits.

Copyright and related rights waived via CC0.