Skip to content

Commit

Permalink
feat: modify batcher contract to support erc20 sendMany
Browse files Browse the repository at this point in the history
Ticket: COIN-2782
  • Loading branch information
kamleshmugdiya committed Jan 29, 2025
1 parent d64bb5a commit 2506542
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 0 deletions.
27 changes: 27 additions & 0 deletions contracts/Batcher.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
pragma solidity 0.8.20;
import './TransferHelper.sol';
import '@openzeppelin/contracts/access/Ownable2Step.sol';

interface IERC20 {
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
}

// SPDX-License-Identifier: Apache-2.0

/**
Expand Down Expand Up @@ -78,6 +83,28 @@ contract Batcher is Ownable2Step {
require(totalSent == msg.value, 'Total sent out must equal total received');
}

/**
* @dev Batch transferFrom for an ERC20 token.
* @param token The address of the ERC20 token contract.
* @param recipients The array of recipient addresses.
* @param amounts The array of amounts to transfer to each recipient.
* Requirements:
* - `recipients` and `amounts` must have the same length.
* - The caller must have approved the contract to spend the tokens being transferred.
*/
function batchTransferFrom(
address token,
address[] calldata recipients,
uint256[] calldata amounts
) external lockCall {
require(recipients.length != 0, 'Must send to at least one person');
require(recipients.length == amounts.length, "Unequal recipients and values");
for (uint8 i = 0; i < recipients.length; i++) {
bool success = IERC20(token).transferFrom(msg.sender, recipients[i], amounts[i]);
require(success, "transferFrom failed");
}
}

/**
* Recovery function for the contract owner to recover any ERC20 tokens or ETH that may get lost in the control of this contract.
* @param to The recipient to send to
Expand Down
126 changes: 126 additions & 0 deletions test/batcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,132 @@ describe('Batcher', () => {
});
});

describe('Batch ERC20 Token Transfers', () => {
let tokenContract;
let totalSupply;
let tokenContractOwner;

beforeEach(async () => {
tokenContractOwner = accounts[9];
tokenContract = await FixedSupplyToken.new({
from: tokenContractOwner
});
totalSupply = await tokenContract.totalSupply();
});

const checkBalance = async (address, expectedAmt) => {
const balance = await tokenContract.balanceOf(address);
assert.strictEqual(
balance.toString(),
expectedAmt.toString(),
`Token balance of ${address} was ${balance.toString()} when ${expectedAmt.toString()} was expected`
);
};

it('Successfully transfers tokens to multiple recipients', async () => {
const sender = accounts[1];
const recipients = [accounts[2], accounts[3], accounts[4]];
const amounts = [100, 200, 300];

// Transfer tokens to sender
await tokenContract.transfer(sender, 1000, { from: tokenContractOwner });
await checkBalance(sender, 1000);

// Approve batcher to spend tokens
await tokenContract.approve(batcherInstance.address, 1000, { from: sender });

// Execute batch transfer
await batcherInstance.batchTransferFrom(
tokenContract.address,
recipients,
amounts,
{ from: sender }
);

// Verify balances
await checkBalance(sender, 400); // 1000 - (100 + 200 + 300)
await checkBalance(recipients[0], 100);
await checkBalance(recipients[1], 200);
await checkBalance(recipients[2], 300);
});

it('Fails when sender has insufficient balance', async () => {
const sender = accounts[1];
const recipients = [accounts[2], accounts[3]];
const amounts = [500, 600];

// Transfer fewer tokens than needed to sender
await tokenContract.transfer(sender, 500, { from: tokenContractOwner });
await tokenContract.approve(batcherInstance.address, 1100, { from: sender });

await checkBalance(sender, 500);
await checkBalance(recipients[0], 0);
await checkBalance(recipients[1], 0);

await assertVMException(
batcherInstance.batchTransferFrom(
tokenContract.address,
recipients,
amounts,
{ from: sender }
),
'transferFrom failed'
);

await checkBalance(sender, 500);
await checkBalance(recipients[0], 0);
await checkBalance(recipients[1], 0);
});

it('Fails when sender has not approved enough tokens', async () => {
const sender = accounts[1];
const recipients = [accounts[2], accounts[3]];
const amounts = [500, 600];

await tokenContract.transfer(sender, 1100, { from: tokenContractOwner });
await tokenContract.approve(batcherInstance.address, 500, { from: sender });

await assertVMException(
batcherInstance.batchTransferFrom(
tokenContract.address,
recipients,
amounts,
{ from: sender }
),
'transferFrom failed'
);
});

it('Fails with empty recipients array', async () => {
const sender = accounts[1];
await assertVMException(
batcherInstance.batchTransferFrom(
tokenContract.address,
[],
[],
{ from: sender }
),
'Must send to at least one person'
);
});

it('Fails with mismatched recipients and amounts arrays', async () => {
const sender = accounts[1];
const recipients = [accounts[2], accounts[3]];
const amounts = [100];

await assertVMException(
batcherInstance.batchTransferFrom(
tokenContract.address,
recipients,
amounts,
{ from: sender }
),
'Unequal recipients and values'
);
});
});

describe('Using recover for ERC20 Tokens', () => {
let tokenContract;
let totalSupply;
Expand Down

0 comments on commit 2506542

Please sign in to comment.