Skip to content
48 changes: 44 additions & 4 deletions blockchain/contracts/SendToMany.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ pragma solidity ^0.4.23;
import "./IERC20.sol";

contract SendToMany {

address owner;
mapping(address => uint) private tokenSums;

constructor() {
constructor() public {
owner = msg.sender;
}

modifier isOwner() {
require(msg.sender == owner);
require(msg.sender == owner, "must be the owner address");
_;
}

function sendMany(address[] addresses, uint[] amounts, address tokenContract) public payable isOwner {
function sendMany(address[] memory addresses, uint[] memory amounts, address tokenContract) public payable isOwner {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reduce duplication

require(addresses.length == amounts.length);
uint sum = 0;
for(uint i = 0; i < amounts.length; i++) {
Expand All @@ -26,10 +28,48 @@ contract SendToMany {
require(token.transferFrom(msg.sender, addresses[i], amounts[i]), "token transfer failed");
}
} else {
require((address(this).balance + msg.value) >= sum, "ETH balance too low for this batch");
require((msg.value) >= sum, "must send enough ETH");
for(i = 0; i < addresses.length; i++) {
addresses[i].transfer(amounts[i]);
}
}
}


function validateBatch(address[] memory addresses, uint[] memory amounts, address[] memory tokenContracts, uint sentValue, address sender) public view returns(bool) {
require(addresses.length == amounts.length, "must provide same length addresses and amounts");
require(addresses.length == tokenContracts.length, "must provide same length addresses and tokenContracts");
for(uint i = 0; i < addresses.length; i++) {
address tokenContract = tokenContracts[i];
uint amount = amounts[i];
tokenSums[tokenContract] += amount;
uint sum = tokenSums[tokenContract];
if(tokenContract != 0x0) {
IERC20 token = IERC20(tokenContract);
require(token.allowance(sender, address(this)) >= sum, "This contract is not allowed enough funds for this batch");
} else {
require(sentValue >= sum, "must send enough ETH");
}
}
return true;
}

function batchSend(address[] memory addresses, uint[] memory amounts, address[] memory tokenContracts) public payable {
require(addresses.length == amounts.length, "must provide same length addresses and amounts");
require(addresses.length == tokenContracts.length, "must provide same length addresses and tokenContracts");
uint ethSum = 0;
for(uint i = 0; i < addresses.length; i++) {
address tokenContract = tokenContracts[i];
address recipient = addresses[i];
uint amount = amounts[i];
if(tokenContract != 0x0) {
IERC20 token = IERC20(tokenContract);
require(token.transferFrom(msg.sender, recipient, amount), "token transfer failed");
} else {
ethSum += amount;
require(msg.value >= ethSum, "must send enough ETH");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make exact equal

recipient.transfer(amount);
}
}
}
}
112 changes: 98 additions & 14 deletions blockchain/test/sendToMany.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
const SendToMany = artifacts.require('SendToMany');
const CryptoErc20 = artifacts.require('CryptoErc20');
const ZERO_ADDR = '0x0000000000000000000000000000000000000000';
const receivers = [
'0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5',
'0x06b8c5883ec71bc3f4b332081519f23834c8706e',
'0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c'
]
contract('SendToMany', (accounts) => {
it('should exist', async() => {
const batcher = await SendToMany.deployed();
Expand All @@ -9,17 +14,18 @@ contract('SendToMany', (accounts) => {

it('should send ether', async() => {
const batcher = await SendToMany.deployed();
const receivers = accounts.slice(1);
const balances = await Promise.all(receivers.map( r => web3.eth.getBalance(r)));
const amounts = new Array(receivers.length).fill(1e18.toString());
const balanceBefore = await web3.eth.getBalance(accounts[0]);;
console.log('Token balance before', balanceBefore.toString());
const balanceBefore = await web3.eth.getBalance(accounts[0]);
const sum = (1e18*receivers.length).toString();
await batcher.sendMany(receivers, amounts, ZERO_ADDR, {value: sum});
const balanceAfter = await web3.eth.getBalance(accounts[0]);;
console.log('ETH balance after', balanceAfter.toString());
for(const receiver of receivers) {
const balanceAfter = await web3.eth.getBalance(accounts[0]);
assert(balanceBefore > balanceAfter, "Account zero should have lower balance");
for(let i = 0; i < receivers.length; i++) {
const receiver = receivers[i];
const balanceBefore = balances[i];
const balance = await web3.eth.getBalance(receiver);
console.log('ETH Balance', receiver, ':', balance.toString());
assert(balance > balanceBefore, "Balance should increase");
}
});

Expand All @@ -31,19 +37,97 @@ contract('SendToMany', (accounts) => {
it('should send tokens', async() => {
const batcher = await SendToMany.deployed();
const token = await CryptoErc20.deployed();
const receivers = accounts.slice(1);
const balances = await Promise.all(receivers.map( r => token.balanceOf(r)));
const amounts = new Array(receivers.length).fill(1e18.toString());
const sum = (1e18*receivers.length).toString();
const balanceBefore = await token.balanceOf(accounts[0]);
console.log('Token balance before', balanceBefore.toString());
await token.approve(batcher.address, sum);
await batcher.sendMany(receivers, amounts, token.address);
const balanceAfter = await token.balanceOf(accounts[0]);
console.log('Token balance after', balanceAfter.toString());
for(const receiver of receivers) {
for(let i = 0; i < receivers.length; i++) {
const receiver = receivers[i];
const balanceBefore = balances[i];
const balance = await token.balanceOf(receiver);
console.log('Token Balance', receiver, ':', balance.toString());
assert(balance > balanceBefore, "Balance should increase");
}
});


it('should send many different tokens', async() => {
const batcher = await SendToMany.deployed();
const token = await CryptoErc20.deployed();
const balances = await Promise.all(receivers.map( r => token.balanceOf(r)));
const amounts = new Array(receivers.length).fill(1e18.toString());
const sum = (1e18*receivers.length).toString();
await token.approve(batcher.address, sum);
const tokens = new Array(receivers.length).fill(token.address)

//send one ETH
tokens[0] = ZERO_ADDR;
balances[0] = await web3.eth.getBalance(receivers[0]);


await batcher.batchSend(receivers, amounts, tokens, {value: 1e18.toString()});
for(let i = 0; i < receivers.length; i++) {
const tokenAddress = tokens[i];
const receiver = receivers[i];
const balanceBefore = balances[i];
const ethBalance = await web3.eth.getBalance(receiver);
const tokenBalance = await token.balanceOf(receiver);
if(tokenAddress != ZERO_ADDR) {
assert(tokenBalance > balanceBefore, "Balance should increase");
} else {
assert(ethBalance > balanceBefore, "Balance should increase");
}
}
});


it('should validate a batch', async() => {
const batcher = await SendToMany.deployed();
const token = await CryptoErc20.deployed();
const balances = await Promise.all(receivers.map( r => token.balanceOf(r)));
const amounts = new Array(receivers.length).fill(1e18.toString());
const sum = (1e18*receivers.length).toString();
await token.approve(batcher.address, sum);
const tokens = new Array(receivers.length).fill(token.address)

//send one ETH
tokens[0] = ZERO_ADDR;
balances[0] = await web3.eth.getBalance(receivers[0]);

const isValid = await batcher.validateBatch.call(receivers, amounts, tokens, 1e18.toString(), accounts[0]);
assert(isValid, "Validate should return true");
console.log("Validate returned true");

try {
const isNotValid = await batcher.validateBatch.call(receivers, amounts, tokens, 1e17.toString(), accounts[0]);
assert(true == false, "Validate should have thrown");
} catch(e) {
console.log("Validate threw");
assert(e, "Validate threw");
}


try {
await token.approve(batcher.address, 0);
const isNotValid = await batcher.validateBatch.call(receivers, amounts, tokens, 1e18.toString(), accounts[0]);
assert(true == false, "Validate should have thrown");
} catch(e) {
console.log("Validate threw");
assert(e, "Validate threw");
}

let threw = false;
try {
await token.approve(batcher.address, sum);
await batcher.validateBatch(receivers, amounts, tokens, 1e18.toString(), accounts[0]);
await batcher.validateBatch(receivers, amounts, tokens, 1e18.toString(), accounts[0]);
await batcher.validateBatch.call(receivers, amounts, tokens, 1e18.toString(), accounts[0]);
} catch(e) {
threw = true;
console.log(e);
console.log("Validate (send) threw");
assert(e, "Validate threw");
}
assert(threw === false, "validateBatch should not throw");
});
});
7 changes: 7 additions & 0 deletions blockchain/truffle.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,18 @@ module.exports = {
// options below to some value.
//
development: {
host: 'localhost', // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
gas: 4700000, // Ropsten has a lower block limit than mainnet
network_id: '*' // Any network (default: none)
},
ci: {
host: 'parity', // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
gas: 4700000, // Ropsten has a lower block limit than mainnet
network_id: '*' // Any network (default: none)
},

},


Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
"test:bats": "tests/cli_runner",
"lint": "./node_modules/.bin/eslint .",
"truffle:compile": "cd blockchain/ && ../node_modules/.bin/truffle compile",
"truffle:test": "cd blockchain/ && ../node_modules/.bin/truffle test",
"truffle:migrate": "cd blockchain/ && ../node_modules/.bin/truffle migrate"
"truffle:test": "cd blockchain/ && ../node_modules/.bin/truffle test --network ci",
"truffle:migrate": "cd blockchain/ && ../node_modules/.bin/truffle migrate --network ci"
},
"author": "Micah Riggan",
"license": "ISC",
Expand Down