From e77cc4fd787639488468b057a5d5651770c40796 Mon Sep 17 00:00:00 2001 From: Channing Date: Tue, 13 Feb 2024 18:13:46 -0800 Subject: [PATCH 1/3] Add ERC721BatchTransfer --- contracts/utils/ERC721BatchTransfer.sol | 159 ++++++++++++++++++++++++ hardhat.config.ts | 21 ++-- scripts/common/constants.ts | 2 + scripts/deploy.ts | 9 +- scripts/deployBA.ts | 3 + scripts/dev/deploy721BatchTransfer.ts | 21 ++++ scripts/dev/getContractCodehash.ts | 12 ++ scripts/ownerMint.ts | 9 +- scripts/send721Batch.ts | 80 ++++++++++++ scripts/setStages.ts | 14 ++- scripts/utils/helper.ts | 14 +++ 11 files changed, 321 insertions(+), 23 deletions(-) create mode 100644 contracts/utils/ERC721BatchTransfer.sol create mode 100644 scripts/dev/deploy721BatchTransfer.ts create mode 100644 scripts/dev/getContractCodehash.ts create mode 100644 scripts/send721Batch.ts create mode 100644 scripts/utils/helper.ts diff --git a/contracts/utils/ERC721BatchTransfer.sol b/contracts/utils/ERC721BatchTransfer.sol new file mode 100644 index 0000000..a7005d8 --- /dev/null +++ b/contracts/utils/ERC721BatchTransfer.sol @@ -0,0 +1,159 @@ +//SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +/** + * @title ERC721 Batch Transfer + * + * @notice ETransfer 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(); + error InvalidCaller(); + + event BatchTransferToSingle( + address indexed contractAddress, + address indexed to, + uint256 amount + ); + + event BatchTransferToMultiple( + address indexed contractAddress, + uint256 amount + ); + + constructor() {} + + modifier noZero() { + if (msg.sender == address(0)) revert InvalidCaller(); + _; + } + + /** + * @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, + uint256[] calldata tokenIds + ) external noZero { + uint256 length = tokenIds.length; + 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, + uint256[] calldata tokenIds + ) external noZero { + uint256 length = tokenIds.length; + 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 noZero { + 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 noZero { + 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); + } +} \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index 8127d76..aa8588a 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -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: { @@ -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; diff --git a/scripts/common/constants.ts b/scripts/common/constants.ts index e564aa2..9270017 100644 --- a/scripts/common/constants.ts +++ b/scripts/common/constants.ts @@ -59,3 +59,5 @@ export const ChainIds: Record = { 'meter-testnet': 10156, 'zksync-testnet': 10165, }; + +export const ERC721BatchTransferContract = '0x9754f8cf81A8efA9BC5686BddbadA174E43b8aeb'; \ No newline at end of file diff --git a/scripts/deploy.ts b/scripts/deploy.ts index b4551fd..4a80551 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -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; @@ -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; diff --git a/scripts/deployBA.ts b/scripts/deployBA.ts index 17417b1..8a30c40 100644 --- a/scripts/deployBA.ts +++ b/scripts/deployBA.ts @@ -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; @@ -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); diff --git a/scripts/dev/deploy721BatchTransfer.ts b/scripts/dev/deploy721BatchTransfer.ts new file mode 100644 index 0000000..c7176ba --- /dev/null +++ b/scripts/dev/deploy721BatchTransfer.ts @@ -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); +} + + diff --git a/scripts/dev/getContractCodehash.ts b/scripts/dev/getContractCodehash.ts new file mode 100644 index 0000000..61bc195 --- /dev/null +++ b/scripts/dev/getContractCodehash.ts @@ -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); +} diff --git a/scripts/ownerMint.ts b/scripts/ownerMint.ts index 54d0030..bcdc993 100644 --- a/scripts/ownerMint.ts +++ b/scripts/ownerMint.ts @@ -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; @@ -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}`); }; diff --git a/scripts/send721Batch.ts b/scripts/send721Batch.ts new file mode 100644 index 0000000..114d9b2 --- /dev/null +++ b/scripts/send721Batch.ts @@ -0,0 +1,80 @@ +import { confirm } from '@inquirer/prompts'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { ERC721BatchTransferContract } from './common/constants'; +import fs from 'fs'; +import { estimateGas } from './utils/helper'; + +export interface ISend721BatchParams { + contract: string; + transferfile?: string; + to?: string; + tokenids?: string; +} + +export const send721Batch = async ( + args: ISend721BatchParams, + hre: HardhatRuntimeEnvironment, +) => { + // check if the BatchTransfer721 contract has the approval to transfer the tokens + const [signer] = await hre.ethers.getSigners(); + + const erc721Contract = (await hre.ethers.getContractFactory('ERC721A')).attach(args.contract); + const approved = await erc721Contract.isApprovedForAll(signer.address , ERC721BatchTransferContract); + if (!approved) { + console.warn('ERC721BatchTransfer contract is not approved to transfer tokens. Approving...'); + await erc721Contract.setApprovalForAll(ERC721BatchTransferContract, true ); + console.log('Approved'); + } + + const tokenids = args.tokenids?.split(',').map((id) => parseInt(id)); + const factory = await hre.ethers.getContractFactory('ERC721BatchTransfer'); + const batchTransferContract = factory.attach(ERC721BatchTransferContract); + + if (!args.transferfile) { + if (!args.to || !args.tokenids) { + console.error('Missing required arguments: to, tokenIds'); + return; + } + const tx = await batchTransferContract.populateTransaction.safeBatchTransferToSingleWallet(args.contract, args.to, tokenids!); + await estimateGas(hre, tx); + + if (!(await confirm({ message: 'Continue to transfer?' }))) return; + console.log(`Transferring tokens to ${args.to}...`); + const submittedTx = await batchTransferContract.safeBatchTransferToSingleWallet(args.contract, args.to, tokenids!); + + console.log(`Submitted tx ${submittedTx.hash}`); + await submittedTx.wait(); + console.log('Tokens transferred'); + } else { + const lines = fs.readFileSync(args.transferfile, 'utf-8').split('\n').filter(Boolean); + const tos = []; + const tokenIds = []; + + for (const line of lines) { + const [to, tokenId] = line.split(' '); + tos.push(to); + tokenIds.push(tokenId); + } + + if (tos.length !== tokenIds.length) { + console.error('Invalid transfer file'); + return; + } + + if (tos.length === 0) { + console.error('No transfers found'); + return; + } + + const tx = await batchTransferContract.populateTransaction.safeBatchTransferToMultipleWallets(args.contract, tos, tokenIds); + await estimateGas(hre, tx); + + if (!(await confirm({ message: 'Continue to transfer?' }))) return; + + console.log(`Transferring tokens...`); + const submittedTx = await batchTransferContract.safeBatchTransferToMultipleWallets(args.contract, tos, tokenIds); + console.log(`Submitted tx ${submittedTx.hash}`); + await submittedTx.wait(); + console.log('Tokens transferred'); + } +}; diff --git a/scripts/setStages.ts b/scripts/setStages.ts index 24c140c..e1c2fb9 100644 --- a/scripts/setStages.ts +++ b/scripts/setStages.ts @@ -3,6 +3,7 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { MerkleTree } from 'merkletreejs'; import fs from 'fs'; import { ContractDetails } from './common/constants'; +import { estimateGas } from './utils/helper'; export interface ISetStagesParams { stages: string; @@ -84,15 +85,16 @@ export const setStages = async ( ), ); - if (!await confirm({ message: 'Continue to set stages?' })) return; - - const tx = await contract.setStages(stages, overrides); + const tx = await contract.populateTransaction.setStages(stages, overrides); + estimateGas(hre, tx); - console.log(`Submitted tx ${tx.hash}`); + if (!await confirm({ message: 'Continue to set stages?' })) return; - await tx.wait(); + const submittedTx = await contract.setStages(stages, overrides); - console.log('Set stages:', tx.hash); + console.log(`Submitted tx ${submittedTx.hash}`); + await submittedTx.wait(); + console.log('Stages set'); for (let i = 0; i < stagesConfig.length; i++) { const [stage] = await contract.getStageInfo(i); diff --git a/scripts/utils/helper.ts b/scripts/utils/helper.ts new file mode 100644 index 0000000..6bca414 --- /dev/null +++ b/scripts/utils/helper.ts @@ -0,0 +1,14 @@ +import { Deferrable } from 'ethers/lib/utils'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { TransactionRequest } from "@ethersproject/abstract-provider"; + +export const estimateGas = async (hre: HardhatRuntimeEnvironment, tx: Deferrable) => { + const estimatedGasUnit = await hre.ethers.provider.estimateGas(tx); + 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)); + return estimatedGas; +} + From 1660144db37bb0666f9e7c31294d0a6e00015b94 Mon Sep 17 00:00:00 2001 From: Channing Date: Tue, 13 Feb 2024 18:23:29 -0800 Subject: [PATCH 2/3] Add package.json --- package-lock.json | 1 + package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/package-lock.json b/package-lock.json index 369db51..9390422 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "operator-filter-registry": "^1.4.2" }, "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", diff --git a/package.json b/package.json index 705e59d..ea0b142 100644 --- a/package.json +++ b/package.json @@ -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", From 3df9ca94c128ed49eb8f607f1954b881e990baad Mon Sep 17 00:00:00 2001 From: Channing Date: Tue, 13 Feb 2024 23:19:23 -0800 Subject: [PATCH 3/3] Fix and add test --- contracts/mocks/MockERC721A.sol | 18 +++++ contracts/utils/ERC721BatchTransfer.sol | 18 ++--- scripts/common/constants.ts | 2 +- test/ERC721BatchTransfer.test.ts | 98 +++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 14 deletions(-) create mode 100644 contracts/mocks/MockERC721A.sol create mode 100644 test/ERC721BatchTransfer.test.ts diff --git a/contracts/mocks/MockERC721A.sol b/contracts/mocks/MockERC721A.sol new file mode 100644 index 0000000..ee19d19 --- /dev/null +++ b/contracts/mocks/MockERC721A.sol @@ -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); + } +} \ No newline at end of file diff --git a/contracts/utils/ERC721BatchTransfer.sol b/contracts/utils/ERC721BatchTransfer.sol index a7005d8..d4bff3a 100644 --- a/contracts/utils/ERC721BatchTransfer.sol +++ b/contracts/utils/ERC721BatchTransfer.sol @@ -7,13 +7,12 @@ import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; /** * @title ERC721 Batch Transfer * - * @notice ETransfer ERC721 tokens in batches to a single wallet or multiple wallets. This supports ERC721M and ERC721CM contracts. + * @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(); - error InvalidCaller(); event BatchTransferToSingle( address indexed contractAddress, @@ -26,13 +25,6 @@ contract ERC721BatchTransfer { uint256 amount ); - constructor() {} - - modifier noZero() { - if (msg.sender == address(0)) revert InvalidCaller(); - _; - } - /** * @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 @@ -44,7 +36,7 @@ contract ERC721BatchTransfer { IERC721 erc721Contract, address to, uint256[] calldata tokenIds - ) external noZero { + ) external { uint256 length = tokenIds.length; for (uint256 i; i < length; ) { uint256 tokenId = tokenIds[i]; @@ -70,7 +62,7 @@ contract ERC721BatchTransfer { IERC721 erc721Contract, address to, uint256[] calldata tokenIds - ) external noZero { + ) external { uint256 length = tokenIds.length; for (uint256 i; i < length; ) { uint256 tokenId = tokenIds[i]; @@ -102,7 +94,7 @@ contract ERC721BatchTransfer { IERC721 erc721Contract, address[] calldata tos, uint256[] calldata tokenIds - ) external noZero { + ) external { uint256 length = tokenIds.length; if (tos.length != length) revert InvalidArguments(); @@ -137,7 +129,7 @@ contract ERC721BatchTransfer { IERC721 erc721Contract, address[] calldata tos, uint256[] calldata tokenIds - ) external noZero { + ) external { uint256 length = tokenIds.length; if (tos.length != length) revert InvalidArguments(); diff --git a/scripts/common/constants.ts b/scripts/common/constants.ts index 9270017..beaa5be 100644 --- a/scripts/common/constants.ts +++ b/scripts/common/constants.ts @@ -60,4 +60,4 @@ export const ChainIds: Record = { 'zksync-testnet': 10165, }; -export const ERC721BatchTransferContract = '0x9754f8cf81A8efA9BC5686BddbadA174E43b8aeb'; \ No newline at end of file +export const ERC721BatchTransferContract = '0x38F7ba911f7efc434D29D6E39c814E9d4De3FEef'; \ No newline at end of file diff --git a/test/ERC721BatchTransfer.test.ts b/test/ERC721BatchTransfer.test.ts new file mode 100644 index 0000000..92e883f --- /dev/null +++ b/test/ERC721BatchTransfer.test.ts @@ -0,0 +1,98 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import chai, { expect } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { ethers } from 'hardhat'; +import { ERC721BatchTransfer, MockERC721A } from '../typechain-types'; + +chai.use(chaiAsPromised); + +const addresses = { + addr1: '0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2', + addr2: '0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db', + addr3: '0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB', + addr4: '0x617F2E2fD72FD9D5503197092aC168c91465E7f2', + addr5: '0x17F6AD8Ef982297579C203069C1DbfFE4348c372', +} + +describe.only('ERC721BatchTransfer', function () { + let transferContract: ERC721BatchTransfer; + let nftContract: MockERC721A; + let owner: SignerWithAddress; + + beforeEach(async () => { + [owner] = await ethers.getSigners(); + + const batchTransfer = await (await ethers.getContractFactory('ERC721BatchTransfer')).deploy(); + await batchTransfer.deployed(); + transferContract = batchTransfer.connect(owner); + + const erc721a = await (await ethers.getContractFactory('MockERC721A')).deploy(); + await erc721a.deployed(); + nftContract = erc721a.connect(owner); + + await nftContract.setApprovalForAll(transferContract.address, true); + + await nftContract.mintBatch(owner.address, 5); + + for (let i = 0; i < 5; i++) { + const tokenOwner = await nftContract.ownerOf(i); + expect(tokenOwner).to.equal(owner.address); + } + }); + + it('batchTransferToSingleWallet', async () => { + await transferContract.batchTransferToSingleWallet(nftContract.address, addresses.addr1, [0, 1, 2, 3, 4]); + for (let i = 0; i < 5; i++) { + const tokenOwner = await nftContract.ownerOf(i); + expect(tokenOwner).to.equal(addresses.addr1); + } + }); + + it('safeBatchTransferToSingleWallet', async () => { + const tokenIds = [0, 1, 2, 3, 4]; + await transferContract.safeBatchTransferToSingleWallet(nftContract.address, addresses.addr1, [0, 1, 2, 3, 4]); + for (let i = 0; i < tokenIds.length; i++) { + const tokenOwner = await nftContract.ownerOf(tokenIds[i]); + expect(tokenOwner).to.equal(addresses.addr1); + } + }); + + it('batchTransferToMultipleWallets', async () => { + const tokenIds = [0, 1, 2, 3, 4]; + const tos = [addresses.addr1, addresses.addr2, addresses.addr3, addresses.addr4, addresses.addr5]; + await transferContract.batchTransferToMultipleWallets(nftContract.address, tos, tokenIds); + for (let i = 0; i < tokenIds.length; i++) { + const tokenOwner = await nftContract.ownerOf(tokenIds[i]); + expect(tokenOwner).to.equal(tos[i]); + } + }); + + it('safeBatchTransferToMultipleWallets', async () => { + const tokenIds = [4, 1, 2, 0, 3]; + const tos = [addresses.addr1, addresses.addr2, addresses.addr2, addresses.addr2, addresses.addr4]; + await transferContract.safeBatchTransferToMultipleWallets(nftContract.address, tos, tokenIds); + for (let i = 0; i < tokenIds.length; i++) { + const tokenOwner = await nftContract.ownerOf(tokenIds[i]); + expect(tokenOwner).to.equal(tos[i]); + } + }); + + it('revert if tokens not owned', async () => { + const tokenIds = [0, 1, 2, 3, 4]; + const tos = [addresses.addr1, addresses.addr2, addresses.addr3, addresses.addr4, addresses.addr5]; + await transferContract.batchTransferToMultipleWallets(nftContract.address, tos, tokenIds); + + await expect( + transferContract.batchTransferToMultipleWallets(nftContract.address, tos, tokenIds) + ).to.be.revertedWith('NotOwnerOfToken'); + }); + + it('revert if invalid arguments', async () => { + const tokenIds = [0, 1, 2, 3, ]; + const tos = [addresses.addr1, addresses.addr2, addresses.addr3, addresses.addr4, addresses.addr5]; + + await expect( + transferContract.batchTransferToMultipleWallets(nftContract.address, tos, tokenIds) + ).to.be.revertedWith('InvalidArguments'); + }); +});