# Discussion for EIP6670 - Batched Transfers for ERC721

**URL:** <https://forum.openzeppelin.com/t/discussion-for-eip6670-batched-transfers-for-erc721/35181>\
**Category:** Support\
**Tags:** erc721, proxies, solidity\
**Created:** [March 15, 2023, 3:58am UTC](https://forum.openzeppelin.com/t/discussion-for-eip6670-batched-transfers-for-erc721/35181 "2023-03-15T03:58:08Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![Collect9](https://sea2.discourse-cdn.com/flex022/user_avatar/forum.openzeppelin.com/collect9/32/17871_2.png) [@Collect9](https://forum.openzeppelin.com/u/Collect9)\
**Post date:** [March 15, 2023, 3:58am UTC](https://forum.openzeppelin.com/t/discussion-for-eip6670-batched-transfers-for-erc721/35181/1 "2023-03-15T03:58:08Z")

</div>

| eip | title | author | discussions-to | status | type | category | created | requires |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| X | Safe Batched Transfers | Arthur Suszko (@collect9) | [EIPs git](https://github.com/ethereum/EIPs/issues/6686) | Draft | Standards Track | ERC | 2023-03-14 | 721 |

## Simple Summary

An extension of the ERC721 standard that provides safe batch transfer functions. Another similar ERC already exists [1], but feels incomplete. This EIP expands on what ERC1412 did but with additional efficiency improvements and capabilities.

## Abstract

Smart contracts are not efficiently mutable; thus they must be future proofed as much as possible prior to deployment. The ERC721 extension presented in the EIP accomplishes some of that future-proofing, by standardizing a way to transfer multiple ERC721 tokens requiring multiple transactions. Current methods, such as delegating batch transfer to an operator approved proxy contract, use more gas than is necessary. The benchmarks presented demonstrate the savings of native batch transfer capability, versus that of an operator approved proxy contract. Even more savings are realized when calling the contract direct.

## Motivation

We have witnessed Ethereum gas fees sustain 100+ GWEI. Such fees make transferring many NFTs between two wallets too expensive to perform. Often, operator approved proxy contracts are used to facilitate NFT batch transfers, effectively reducing the transfer cost per token.

Contrary to popular belief, delegation of batch transfers to proxy contracts is not nearly as efficient as an ERC721 contract being able to natively batch transfer itself; the benchmarks of this EIP below show a 24-32% reduction in safe transfer costs from a proxy contract for batches sized 10-100. Additionally, individual users may perform batch transfer without the overhead of a proxy contract, further increasing the gas savings to 28-35% for batched sized 10-100. Such reduction in gas usage would go a long way in reducing promoting the health of the network by lowering its demand load and thus gas fees.

## Specification

#### The Interface

```auto
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/* @dev Note: Interface for EIP-XXXX
 */
interface IERCXXXX {
    // Only one emit event needed per TransferBatch
    event TransferBatch(address indexed from, address indexed to, uint256[] tokenIds); 

    // Batch transfer from
    function safeTransferBatchFrom(address from, address to, uint256[] calldata tokenIds) external;

    // Batch transfer from (bytes memory data ERC721 spec version)
    function safeTransferBatchFrom(address from, address to, uint256[] calldata tokenIds, bytes memory data) external;

    // Batch transfer from to batch
    function safeBatchTransferBatchFrom(address from, address[] calldata to, uint256[][] calldata tokenIds) external;

    // Batch transfer from to batch (bytes memory data ERC721 spec version)
    function safeBatchTransferBatchFrom(address from, address[] calldata to, uint256[][] calldata tokenIds, bytes[] memory data) external;
}

```

#### Additional

```auto
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId)
    public view
    override(IERC165, ERC721)
    returns (bool) {
        return interfaceId == type(IERCXXXX).interfaceId || super.supportsInterface(interfaceId);
}

```

#### Implementation Examples

```auto
error InputSizesMisMatch();
error NonERC721Receiver();

/**
 * @dev See {IERCXXXX-safeTransferBatchFrom}.
 */
function safeTransferBatchFrom(address from, address to, uint256[] calldata tokenIds)
public virtual override {
    _transferBatch(from, to, tokenIds);
    // Only need to check the first token when going to same address
    if (!_checkOnERC721Received(from, to, tokenIds[0], "")) {
        revert NonERC721Receiver();
    }
}

/**
 * @dev See {IERCXXXX-safeBatchTransferBatchFrom}.
 */
function safeBatchTransferBatchFrom(address from, address[] calldata to, uint256[][] calldata tokenIds)
public virtual override {
    uint256 _batchSize = tokenIds.length;
    if (to.length != _batchSize) {
        revert InputSizesMisMatch();
    }
    for (uint256 i; i<_batchSize;) {
        safeTransferBatchFrom(from, to[i], tokenIds[i]);
        unchecked {++i;}
    }
}

```

## Test Cases

Two ERC721 contracts are benchmarked within the Remix VM: one with the extension - referred to as the test contract, and another without - the control contract, which the base ERC721 standard contract. Both contracts are called from an approved operator proxy contract. The transaction gas costs and reductions in costs are as follows:

| Transfer Batch Size | Proxy to Control Gas Cost | Proxy to Test Gas Cost | Direct to Test Gas Cost | Proxy Reduction | Direct Reduction |
| --- | --- | --- | --- | --- | --- |
| 1 | 52332 | 50730 | 46921 | 3.1% | 10.3% |
| 2 | 65302 | 59397 | 55582 | 9.0% | 14.9% |
| 5 | 104212 | 85398 | 81565 | 18.1% | 21.7% |
| 10 | 169062 | 128733 | 124870 | 23.9% | 26.1% |
| 100 | 1336362 | 908808 | 868302 | 32.0% | 35.0% |

#### Test Code

##### Base ERC721 Derived Contract

```auto
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

/**
* @dev Original ERC721 without any extensions.
*/
contract OriginalTokenContract is ERC721 {
    constructor() ERC721("Original NFTs", "ONFTs") {
        for (uint256 i; i<144;) {
            _mint(_msgSender(), i);
            unchecked {++i;}
        }
    }
}

```

##### ERCXXXX ERC721 Derived Contract

```auto
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./erc721-EIPXXXX.sol"; // ERC721 with IERCXXXX implemented

/**
* @dev Updated ERC721 with EIP extension.
*/
contract UpdatedTokenContract is ERC721 {
    constructor() ERC721("EIP NFTs", "ENFTs") {
        for (uint256 i; i<144;) {
            _mint(_msgSender(), i);
            unchecked {++i;}
        }
    }
}

```

##### Proxy Contract

```auto
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "./interfaces/IERCXXXX.sol";

/**
* @dev This a proxy contract that one would build to 
* achieve batching of the original ERC721 contract.
* Note: While a proxy contract is no longer needed 
* with the EIP extension, we still include it here for 
* benchmark purposes as other dApps will act as a 
* proxy anyway, thus benchmarks are taken from the proxy.
*/
contract Proxy {

    address public immutable contractTokenOriginal;
    address public immutable contractTokenUpdated;

    constructor(address _contractTokenOriginal, address _contractTokenUpdated) {
        contractTokenOriginal = _contractTokenOriginal;
        contractTokenUpdated = _contractTokenUpdated;
    }

    /**
     * @dev Batched transfer from the original ERC721 contract.
     */
    function safeTransfer(address from, address to, uint256[] calldata tokenIds)
    public {
        uint256 _batchSize = tokenIds.length;
        for (uint256 i; i<_batchSize;) {
            IERC721(contractTokenOriginal).safeTransferFrom(from, to, tokenIds[i]);
            unchecked {++i;}
        }
    }

    /**
     * @dev Batched transfer from the updated ERC721 contract.
     */
    function safeTransferBatch(address from, address to, uint256[] calldata tokenIds)
    public {
        IERCXXXX(contractTokenUpdated).safeTransferBatchFrom(from, to, tokenIds);
    }
}

```

## Rationale

Benchmarks show the EIP extended contract sees immediate gas savings over the control contract with just a batch of two tokens. The test contract exceeds gas savings of over ~4000 gas units per token over larger transfers (\>10 tokens). A batch of 100 tokens were tested to save ~4300 gas units per token, suggesting a plateau around this number.

If this were to become widely adopted, the cumulative gas savings of batched transfers could be massive.

Such batching also introduces the capability for a proxy contract to batch transfer to a batch of addresses, as implemented by `safeBatchTransferBatchFrom(...)`. For users interacting directly with the ERC721 contract, such a capability opens up the new possibilities such as being able to more cheaply gift NFTs to multiple addresses at once, without the overhead of a proxy contract.

## Backwards Compatibility

To take full advantage of batching, the following _MUST_ be done:

1. Only a single event, `event TransferBatch(...)` _MUST_ be emitted to summarizes all token transfers per batch to address.
2. Token owner balances _MUST_ only be updated one time per batch.
3. `_checkOnERC721Received()` _MUST_ only be called one time per batch to address.

Thus, the original private `_transfer()` function is not suitable for batched transfer, as it performs event emissions and balance update for every transfer. To maintain backwards compatibility, the ERC _SHOULD_ include a new private `_transferBatch()` function. However, it is highly _RECOMMENDED_ to update the original `_transfer()` so that duplicate code does not need to be inlined within internal `_transferBatch()`.

A new internal `_transferBatch()` function cannot be written into the extension by itself as it tries to read from storage that is marked as private in the ERC721 contract. Thus, to maintain full backwards compatibility, it would be _REQUIRED_ for `_transferBatch()` to be hard coded into the ERC721 base contract. Alternatively, state variables this EIP reads from _MAY_ be changed to internal.

## Copyright

Copyright and related rights waived via CC0

## Citations

[1] [https://github.com/ethereum/EIPs/issues/1412](https://github.com/ethereum/EIPs/issues/1412)

---

<div class="post-metadata">

**Author:** ![JulissaDantes](https://sea2.discourse-cdn.com/flex022/user_avatar/forum.openzeppelin.com/julissadantes/32/15410_2.png) [@JulissaDantes](https://forum.openzeppelin.com/u/JulissaDantes)\
**Post date:** [March 15, 2023, 12:56pm UTC](https://forum.openzeppelin.com/t/discussion-for-eip6670-batched-transfers-for-erc721/35181/2 "2023-03-15T12:56:17Z")

</div>

Hi, looks like you want to create your own EIP, but the steps to do so are:

> Before submitting a proposal, one must read [EIP-1](https://eips.ethereum.org/EIPS/eip-1) which outlines the EIP process and how to write an EIP, and solicit feedback on [Ethereum Magicians](https://ethereum-magicians.org/), where proposals are first discussed with the community before a draft is submitted.

For the discussion link to be valid this needs to be posted here: [Ethereum Magicians](https://ethereum-magicians.org/). (Not in the OpenZeppelin forum)
