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 4ea8a05
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 2 deletions.
45 changes: 44 additions & 1 deletion contracts/Batcher.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
pragma solidity 0.8.20;
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 All @@ -23,14 +27,21 @@ contract Batcher is Ownable2Step {
uint256 prevTransferGasLimit,
uint256 newTransferGasLimit
);
event BatchTransferLimitChange(
uint256 prevBatchTransferLimit,
uint256 newBatchTransferLimit
);

uint256 public lockCounter;
uint256 public transferGasLimit;
uint256 public batchTransferLimit;

constructor(uint256 _transferGasLimit) Ownable(msg.sender) {
constructor(uint256 _transferGasLimit, uint256 _batchTransferLimit) Ownable(msg.sender) {
lockCounter = 1;
transferGasLimit = _transferGasLimit;
batchTransferLimit = _batchTransferLimit;
emit TransferGasLimitChange(0, transferGasLimit);
emit BatchTransferLimitChange(0, batchTransferLimit);
}

modifier lockCall() {
Expand Down Expand Up @@ -78,6 +89,29 @@ 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");
require(recipients.length <= batchTransferLimit, "Too many recipients");
for (uint16 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 Expand Up @@ -109,6 +143,15 @@ contract Batcher is Ownable2Step {
transferGasLimit = newTransferGasLimit;
}

function changeBatchTransferLimit(uint256 newBatchTransferLimit)
external
onlyOwner
{
require(newBatchTransferLimit > 0, 'Batch transfer limit too low');
emit BatchTransferLimitChange(batchTransferLimit, newBatchTransferLimit);
batchTransferLimit = newBatchTransferLimit;
}

fallback() external payable {
revert('Invalid fallback');
}
Expand Down
193 changes: 192 additions & 1 deletion test/batcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ describe('Batcher', () => {
sender = accounts[0];
batcherOwner = accounts[8];

batcherInstance = await Batcher.new(21000, { from: batcherOwner });
batcherInstance = await Batcher.new(21000, 256, { from: batcherOwner });
reentryInstance = await Reentry.new(batcherInstance.address);
failInstance = await Fail.new();
gasGuzzlerInstance = await GasGuzzler.new();
Expand Down Expand Up @@ -664,6 +664,197 @@ describe('Batcher', () => {
});
});

describe('Batch Transfer Limit', () => {
it('Successfully changes batch transfer limit', async () => {
const newLimit = 100;
const tx = await batcherInstance.changeBatchTransferLimit(newLimit, {
from: batcherOwner
});

const {
logs: [
{
args: { prevBatchTransferLimit, newBatchTransferLimit }
}
]
} = tx;

assert.strictEqual(
prevBatchTransferLimit.toNumber(),
256,
"Previous limit incorrect"
);
assert.strictEqual(
newBatchTransferLimit.toNumber(),
newLimit,
"New limit incorrect"
);

const currentLimit = await batcherInstance.batchTransferLimit();
assert.strictEqual(
currentLimit.toNumber(),
newLimit,
"Limit not updated correctly"
);
});

it('Fails to set batch transfer limit to zero', async () => {
await assertVMException(
batcherInstance.changeBatchTransferLimit(0, { from: batcherOwner }),
'Batch transfer limit too low'
);
});

it('Fails when non-owner tries to change batch transfer limit', async () => {
await truffleAssert.reverts(
batcherInstance.changeBatchTransferLimit(100, { from: accounts[1] })
);
});
});

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'
);

// balance does not change
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 checkBalance(sender, 1100);
await checkBalance(recipients[0], 0);
await checkBalance(recipients[1], 0);

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

// balance does not change
await checkBalance(sender, 1100);
await checkBalance(recipients[0], 0);
await checkBalance(recipients[1], 0);
});

it('Fails with empty recipients array', async () => {
const sender = accounts[1];
await assertVMException(
batcherInstance.batchTransferFrom(tokenContract.address, [], [], {
from: sender
}),
'Too many recipients'
);
});

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 4ea8a05

Please sign in to comment.