Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ERC721BatchTransfer and couple improvements #107

Merged
merged 3 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions contracts/mocks/MockERC721A.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.4;

import { ERC721A } from "erc721a/contracts/ERC721A.sol";

contract MockERC721A is ERC721A {
// solhint-disable-next-line no-empty-blocks
constructor() ERC721A("MOCK", "M") {}

function mint(address to) external {
_mint(to, 1);
}

function mintBatch(address to, uint256 quantity) external {
_mint(to, quantity);
}
}
151 changes: 151 additions & 0 deletions contracts/utils/ERC721BatchTransfer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
//SPDX-License-Identifier: MIT

pragma solidity ^0.8.4;

import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";

/**
* @title ERC721 Batch Transfer
*
* @notice Transfer ERC721 tokens in batches to a single wallet or multiple wallets. This supports ERC721M and ERC721CM contracts.
* @notice To use any of the methods in this contract the user has to approve this contract to control their tokens using either `setApproveForAll` or `approve` functions from the ERC721 contract.
*/
contract ERC721BatchTransfer {
error InvalidArguments();
error NotOwnerOfToken();

event BatchTransferToSingle(
address indexed contractAddress,
address indexed to,
uint256 amount
);

event BatchTransferToMultiple(
address indexed contractAddress,
uint256 amount
);

/**
* @notice Transfer multiple tokens to the same wallet using the ERC721.transferFrom method.
* @notice If you don't know what that means, use the `safeBatchTransferToSingleWallet` method instead
* @param erc721Contract the address of the nft contract
* @param to the address that will receive the nfts
* @param tokenIds the list of tokens that will be transferred
*/
function batchTransferToSingleWallet(
IERC721 erc721Contract,
address to,
channing-magiceden marked this conversation as resolved.
Show resolved Hide resolved
uint256[] calldata tokenIds
) external {
uint256 length = tokenIds.length;
channing-magiceden marked this conversation as resolved.
Show resolved Hide resolved
for (uint256 i; i < length; ) {
uint256 tokenId = tokenIds[i];
address owner = erc721Contract.ownerOf(tokenId);
if (msg.sender != owner) {
revert NotOwnerOfToken();
}
erc721Contract.transferFrom(owner, to, tokenId);
unchecked {
++i;
}
}
emit BatchTransferToSingle(address(erc721Contract), to, length);
}

/**
* @notice transfer multiple tokens to the same wallet using the `ERC721.safeTransferFrom` method
* @param erc721Contract the address of the nft contract
* @param to the address that will receive the nfts
* @param tokenIds the list of tokens that will be transferred
*/
function safeBatchTransferToSingleWallet(
IERC721 erc721Contract,
address to,
channing-magiceden marked this conversation as resolved.
Show resolved Hide resolved
uint256[] calldata tokenIds
) external {
uint256 length = tokenIds.length;
channing-magiceden marked this conversation as resolved.
Show resolved Hide resolved
for (uint256 i; i < length; ) {
uint256 tokenId = tokenIds[i];
address owner = erc721Contract.ownerOf(tokenId);
if (msg.sender != owner) {
revert NotOwnerOfToken();
}
erc721Contract.safeTransferFrom(owner, to, tokenId);
unchecked {
++i;
}
}
emit BatchTransferToSingle(address(erc721Contract), to, length);
}

/**
* @notice Transfer multiple tokens to multiple wallets using the ERC721.transferFrom method
* @notice If you don't know what that means, use the `safeBatchTransferToMultipleWallets` method instead
* @notice The tokens in `tokenIds` will be transferred to the addresses in the same position in `tos`
* @notice E.g.: if tos = [0x..1, 0x..2, 0x..3] and tokenIds = [1, 2, 3], then:
* 0x..1 will receive token 1;
* 0x..2 will receive token 2;
* 0x..3 will receive token 3;
* @param erc721Contract the address of the nft contract
* @param tos the list of addresses that will receive the nfts
* @param tokenIds the list of tokens that will be transferred
*/
function batchTransferToMultipleWallets(
IERC721 erc721Contract,
address[] calldata tos,
uint256[] calldata tokenIds
) external {
uint256 length = tokenIds.length;
if (tos.length != length) revert InvalidArguments();

for (uint256 i; i < length; ) {
uint256 tokenId = tokenIds[i];
address owner = erc721Contract.ownerOf(tokenId);
address to = tos[i];
if (msg.sender != owner) {
revert NotOwnerOfToken();
}
erc721Contract.transferFrom(owner, to, tokenId);
unchecked {
++i;
}
}

emit BatchTransferToMultiple(address(erc721Contract), length);
}

/**
* @notice Transfer multiple tokens to multiple wallets using the ERC721.safeTransferFrom method
* @notice The tokens in `tokenIds` will be transferred to the addresses in the same position in `tos`
* @notice E.g.: if tos = [0x..1, 0x..2, 0x..3] and tokenIds = [1, 2, 3], then:
* 0x..1 will receive token 1;
* 0x..2 will receive token 2;
* 0x..3 will receive token 3;
* @param erc721Contract the address of the nft contract
* @param tos the list of addresses that will receive the nfts
* @param tokenIds the list of tokens that will be transferred
*/
function safeBatchTransferToMultipleWallets(
IERC721 erc721Contract,
address[] calldata tos,
uint256[] calldata tokenIds
) external {
uint256 length = tokenIds.length;
if (tos.length != length) revert InvalidArguments();

for (uint256 i; i < length; ) {
uint256 tokenId = tokenIds[i];
address owner = erc721Contract.ownerOf(tokenId);
address to = tos[i];
if (msg.sender != owner) {
revert NotOwnerOfToken();
}
erc721Contract.safeTransferFrom(owner, to, tokenId);
unchecked {
++i;
}
}

emit BatchTransferToMultiple(address(erc721Contract), length);
}
}
21 changes: 14 additions & 7 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ import { setOnftMinDstGas } from './scripts/setOnftMinDstGas';
import { setTrustedRemote } from './scripts/setTrustedRemote';
import { sendOnft } from './scripts/sendOnft';
import { deployOwnedRegistrant } from './scripts/deployOwnedRegistrant';
import { getContractCodehash } from './scripts/dev/getContractCodehash';
import { deploy721BatchTransfer } from './scripts/dev/deploy721BatchTransfer';
import { send721Batch } from './scripts/send721Batch';

const config: HardhatUserConfig = {
solidity: {
Expand Down Expand Up @@ -346,12 +349,16 @@ task('deployOwnedRegistrant', 'Deploy OwnedRegistrant')

task('getContractCodehash', 'Get the code hash of a contract')
.addParam('contract', 'contract address')
.setAction(async (args, hre) => {
const [signer] = await hre.ethers.getSigners();
const provider = signer.provider;
let code = await provider!.getCode(args.contract);
const codehash = hre.ethers.utils.keccak256(code);
console.log(codehash);
});
.setAction(getContractCodehash);

task('deploy721BatchTransfer', 'Deploy ERC721BatchTransfer')
.setAction(deploy721BatchTransfer);

task('send721Batch', 'Send ERC721 tokens in batch')
.addParam('contract', 'contract address')
.addOptionalParam('transferfile', 'path to the file with the transfer details')
.addOptionalParam('to', 'recipient address (if not using transferFile)')
.addOptionalParam('tokenids', 'token ids (if not using transferFile), separate with comma')
.setAction(send721Batch);

export default config;
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"ethers": "^5.0.0"
},
"devDependencies": {
"@ethersproject/abstract-provider": "^5.7.0",
"@nomicfoundation/hardhat-network-helpers": "^1.0.6",
"@nomiclabs/hardhat-ethers": "^2.1.1",
"@nomiclabs/hardhat-etherscan": "^3.1.0",
Expand Down
2 changes: 2 additions & 0 deletions scripts/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,5 @@ export const ChainIds: Record<string, number> = {
'meter-testnet': 10156,
'zksync-testnet': 10165,
};

export const ERC721BatchTransferContract = '0x38F7ba911f7efc434D29D6E39c814E9d4De3FEef';
9 changes: 2 additions & 7 deletions scripts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { confirm } from '@inquirer/prompts';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
import { ContractDetails } from './common/constants';
import { estimateGas } from './utils/helper';

export interface IDeployParams {
name: string;
Expand Down Expand Up @@ -95,13 +96,7 @@ export const deploy = async (
JSON.stringify(args, null, 2),
);

const deployTx = await contractFactory.getDeployTransaction(...params);
const estimatedGasUnit = await hre.ethers.provider.estimateGas(deployTx);
const estimatedGasPrice = await hre.ethers.provider.getGasPrice();
const estimatedGas = estimatedGasUnit.mul(estimatedGasPrice);
console.log('Estimated gas unit: ', estimatedGasUnit.toString());
console.log('Estimated gas price (WEI): ', estimatedGasPrice.toString());
console.log('Estimated gas (ETH): ', hre.ethers.utils.formatEther(estimatedGas));
await estimateGas(hre, contractFactory.getDeployTransaction(...params));

if (!(await confirm({ message: 'Continue to deploy?' }))) return;

Expand Down
3 changes: 3 additions & 0 deletions scripts/deployBA.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { confirm } from '@inquirer/prompts';
import { ContractDetails } from './common/constants';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
import { estimateGas } from './utils/helper';

export interface IDeployParams {
name: string;
Expand Down Expand Up @@ -70,6 +71,8 @@ export const deployBA = async (
),
);

await estimateGas(hre, contractFactory.getDeployTransaction(...params));

if (!await confirm({ message: 'Continue to deploy?' })) return;

const contract = await contractFactory.deploy(...params);
Expand Down
21 changes: 21 additions & 0 deletions scripts/dev/deploy721BatchTransfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { confirm } from '@inquirer/prompts';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
import { estimateGas } from '../utils/helper';

export const deploy721BatchTransfer = async (
args: {},
hre: HardhatRuntimeEnvironment
) => {
const [signer] = await hre.ethers.getSigners();
const factory = await hre.ethers.getContractFactory('ERC721BatchTransfer', signer);

await estimateGas(hre, factory.getDeployTransaction());

if (!(await confirm({ message: 'Continue to deploy?' }))) return;

const contract = await factory.deploy();
await contract.deployed();
console.log('ERC721BatchTransfer deployed to:', contract.address);
}


12 changes: 12 additions & 0 deletions scripts/dev/getContractCodehash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { HardhatRuntimeEnvironment } from 'hardhat/types';

export const getContractCodehash = async (
args: { contract: string },
hre: HardhatRuntimeEnvironment
) => {
const [signer] = await hre.ethers.getSigners();
const provider = signer.provider;
let code = await provider!.getCode(args.contract);
const codehash = hre.ethers.utils.keccak256(code);
console.log(codehash);
}
9 changes: 6 additions & 3 deletions scripts/ownerMint.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { confirm } from '@inquirer/prompts';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
import { ContractDetails } from './common/constants';
import { estimateGas } from './utils/helper';

export interface IOwnerMintParams {
contract: string;
Expand All @@ -19,12 +20,14 @@ export const ownerMint = async (
const qty = ethers.BigNumber.from(args.qty ?? 1);
const to = args.to ?? (await contract.signer.getAddress());

const tx = await contract.populateTransaction.ownerMint(qty, to);
estimateGas(hre, tx);
console.log(`Going to mint ${qty.toNumber()} token(s) to ${to}`);
if (!await confirm({ message: 'Continue?' })) return;

const tx = await contract.ownerMint(qty, to);
const submittedTx = await contract.ownerMint(qty, to);

console.log(`Submitted tx ${tx.hash}`);
await tx.wait();
console.log(`Submitted tx ${submittedTx.hash}`);
await submittedTx.wait();
console.log(`Minted ${qty.toNumber()} token(s) to ${to}`);
};
Loading
Loading