diff --git a/blockchain/contracts/SendToMany.sol b/blockchain/contracts/SendToMany.sol index 674ebab6..9c076642 100644 --- a/blockchain/contracts/SendToMany.sol +++ b/blockchain/contracts/SendToMany.sol @@ -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 { require(addresses.length == amounts.length); uint sum = 0; for(uint i = 0; i < amounts.length; i++) { @@ -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"); + recipient.transfer(amount); + } + } + } } diff --git a/blockchain/test/sendToMany.js b/blockchain/test/sendToMany.js index a6910a91..9b89a830 100644 --- a/blockchain/test/sendToMany.js +++ b/blockchain/test/sendToMany.js @@ -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(); @@ -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"); } }); @@ -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"); + }); }); diff --git a/blockchain/truffle.js b/blockchain/truffle.js index f2fa9745..0477f81d 100644 --- a/blockchain/truffle.js +++ b/blockchain/truffle.js @@ -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) }, + }, diff --git a/package.json b/package.json index b0a170ad..0b2b87f3 100644 --- a/package.json +++ b/package.json @@ -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",