diff --git a/contracts/utils/MultiSendERC20.sol b/contracts/utils/MultiSendERC20.sol new file mode 100644 index 00000000..c767bdef --- /dev/null +++ b/contracts/utils/MultiSendERC20.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MultiSendERC20 is Ownable { + uint8 public arrayLimit; + ERC20 public token; + + event Multisended(uint256 total, address token); + + constructor(address _owner, address _token) Ownable(_owner) { + arrayLimit = 200; + token = ERC20(_token); + } + + function setArrayLimit(uint8 _arrayLimit) external onlyOwner { + arrayLimit = _arrayLimit; + } + + function multisendToken(address[] calldata _recipients, uint256[] calldata _amounts) public { + uint256 total = 0; + require(_recipients.length <= arrayLimit, "Array length exceeds limit"); + uint8 i = 0; + for (i; i < _recipients.length; i++) { + require(token.transferFrom(msg.sender, _recipients[i], _amounts[i]), "Transfer failed"); + total += _amounts[i]; + } + + emit Multisended(total, address(token)); + } +} diff --git a/test/contracts/utils/MultiSendERC20.ts b/test/contracts/utils/MultiSendERC20.ts new file mode 100644 index 00000000..49b53d39 --- /dev/null +++ b/test/contracts/utils/MultiSendERC20.ts @@ -0,0 +1,65 @@ +import chai from "chai"; +import { ethers, waffle } from "hardhat"; +import { solidity } from "ethereum-waffle"; + +import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +import { MultiSendERC20, ERC20Mock } from "../../../typechain-types"; +import { Artifacts } from "../../shared"; + +chai.use(solidity); + +const { expect } = chai; +const { deployContract } = waffle; + +describe("MultiSendERC20", () => { + let admin: SignerWithAddress; + let recipientOne: SignerWithAddress; + let recipientTwo: SignerWithAddress; + let recipientThree: SignerWithAddress; + + let token: ERC20Mock; + let multiSend: MultiSendERC20; + + beforeEach(async () => { + [admin, recipientOne, recipientTwo, recipientThree] = await ethers.getSigners(); + token = (await deployContract(admin, Artifacts.ERC20Mock, [admin.address])) as ERC20Mock; + }); + + async function builder() { + return deployContract(admin, Artifacts.MultiSendERC20, [admin.address, token.address]); + } + + describe("Deploy base contract", () => { + beforeEach(async () => { + multiSend = (await builder()) as MultiSendERC20; + }); + + it("deploys and set ups the initial state correctly", async () => { + expect(await multiSend.token()).to.eq(token.address); + expect(await multiSend.owner()).to.eq(admin.address); + expect(await multiSend.arrayLimit()).to.eq(200); + }); + }); + + describe("Transfer behaviour", () => { + beforeEach(async () => { + multiSend = (await builder()) as MultiSendERC20; + await token.approve(multiSend.address, ethers.utils.parseEther("1000000000")); + }); + + it("Should transfer tokens between one account", async function () { + await multiSend.connect(admin).multisendToken([recipientOne.address], [50]); + const recipientOneBalance = await token.balanceOf(recipientOne.address); + expect(recipientOneBalance).to.equal(50); + }); + + it("Should transfer tokens between multiple accounts", async function () { + await multiSend.connect(admin).multisendToken([recipientOne.address, recipientTwo.address], [50, 100]); + const recipientOneBalance = await token.balanceOf(recipientOne.address); + const recipientTwoBalance = await token.balanceOf(recipientTwo.address); + expect(recipientOneBalance).to.equal(50); + expect(recipientTwoBalance).to.equal(100); + }); + }); +}); diff --git a/test/shared/artifacts.ts b/test/shared/artifacts.ts index faadab73..fc117016 100644 --- a/test/shared/artifacts.ts +++ b/test/shared/artifacts.ts @@ -9,6 +9,7 @@ import SmartBuilderScore from "../../artifacts/contracts/passport/SmartBuilderSc import PassportSources from "../../artifacts/contracts/passport/PassportSources.sol/PassportSources.json"; import TalentTGEUnlock from "../../artifacts/contracts/talent/TalentTGEUnlock.sol/TalentTGEUnlock.json"; import PassportWalletRegistry from "../../artifacts/contracts/passport/PassportWalletRegistry.sol/PassportWalletRegistry.json"; +import MultiSendERC20 from "../../artifacts/contracts/utils/MultiSendERC20.sol/MultiSendERC20.json"; export { PassportRegistry, @@ -22,4 +23,5 @@ export { PassportSources, TalentTGEUnlock, PassportWalletRegistry, + MultiSendERC20, };