├── .babelrc ├── contracts ├── ERC20.sol ├── TestMintableToken.sol └── Multiplexer.sol ├── .eslintrc ├── migrations └── 1_deploy.js ├── test ├── RunScriptWithWorkingWeb3.js └── Multiplexer.js ├── README.md ├── package.json ├── truffle.js ├── LICENSE └── scripts └── dgd-reward.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "env", "stage-0" ] 3 | } 4 | -------------------------------------------------------------------------------- /contracts/ERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.16; 2 | 3 | contract ERC20 { 4 | function transferFrom( address from, address to, uint value) returns (bool ok); 5 | } 6 | -------------------------------------------------------------------------------- /contracts/TestMintableToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | import 'zeppelin-solidity/contracts/token/MintableToken.sol'; 4 | 5 | contract TestMintableToken is MintableToken { } 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env" : { "mocha": true }, 4 | "globals" : { 5 | "assert": true, 6 | "artifacts": true, 7 | "contract": true, 8 | "web3": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /migrations/1_deploy.js: -------------------------------------------------------------------------------- 1 | var Multiplexer = artifacts.require("./Multiplexer.sol"); 2 | 3 | module.exports = function(deployer) { 4 | if (process.env.RUN_SCRIPT) { return null; } 5 | deployer.deploy(Multiplexer); 6 | }; 7 | -------------------------------------------------------------------------------- /test/RunScriptWithWorkingWeb3.js: -------------------------------------------------------------------------------- 1 | // i'm having to use this file to run the script.... 2 | // https://github.com/trufflesuite/truffle/issues/526 3 | 4 | // use: 5 | // RUN_SCRIPT='true' truffle test ./test/RunScriptWithWorkingWeb3.js 6 | // to make this work 7 | 8 | if (!process.env.RUN_SCRIPT) { return null; } 9 | 10 | require('../scripts/dgd-reward.js')(); 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multiplexer 2 | 3 | See `/build/contracts/Multiplexer.json` for deployment details. 4 | 5 | ## Usage 6 | 7 | You must pass an array of addresses and an array of values to send to those addresses (by index) 8 | 9 | ### Send Ether 10 | 11 | ``` 12 | function sendEth(address[] _to, uint256[] _value) payable returns (bool _success) 13 | ``` 14 | 15 | ### Send ERC20 Tokens 16 | 17 | First you must `approve` the multiplexer contract address with enough tokens to process your request 18 | 19 | ``` 20 | function sendErc20(address _tokenAddress, address[] _to, uint256[] _value) returns (bool _success) 21 | ``` 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hitchcott/multiplexer", 3 | "version": "0.0.1", 4 | "description": "Contract for sending ETH or ERC20 to multiple addresses in one transaction", 5 | "scripts": { 6 | "test": "truffle test", 7 | "script": "RUN_SCRIPT='true' truffle test ./test/RunScriptWithWorkingWeb3.js --network mainnet" 8 | }, 9 | "devDependencies": { 10 | "@digix/truffle-lightwallet-provider": "^0.1.2", 11 | "awaiting": "^3.0.0", 12 | "babel-eslint": "^7.2.3", 13 | "babel-preset-env": "^1.6.0", 14 | "babel-preset-stage-0": "^6.24.1", 15 | "eslint": "^4.6.1", 16 | "eslint-config-airbnb": "^15.1.0", 17 | "eslint-plugin-import": "^2.7.0", 18 | "eslint-plugin-jsx-a11y": "^5.1.1", 19 | "eslint-plugin-react": "^7.3.0", 20 | "zeppelin-solidity": "^1.2.0" 21 | }, 22 | "author": "", 23 | "license": "ISC" 24 | } 25 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const LightWalletProvider = require('@digix/truffle-lightwallet-provider'); 3 | 4 | const { KEYSTORE, PASSWORD } = process.env; 5 | 6 | if (!KEYSTORE || !PASSWORD) { throw new Error('You must export KEYSTORE and PASSWORD (see truffle.js)'); } 7 | 8 | module.exports = { 9 | networks: { 10 | development: { 11 | host: 'localhost', 12 | port: 6545, 13 | network_id: '*', // Match any network id 14 | }, 15 | kovan: { 16 | provider: new LightWalletProvider({ 17 | keystore: KEYSTORE, 18 | password: PASSWORD, 19 | rpcUrl: 'https://kovan.infura.io/', 20 | pollingInterval: 2000, 21 | }), 22 | network_id: '42', 23 | }, 24 | mainnet: { 25 | provider: new LightWalletProvider({ 26 | keystore: KEYSTORE, 27 | password: PASSWORD, 28 | rpcUrl: 'https://mainnet.infura.io/', 29 | pollingInterval: 5000, 30 | }), 31 | gas: 350000, 32 | gasPrice: 5e9, 33 | network_id: '1', 34 | }, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /contracts/Multiplexer.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.16; 2 | 3 | import './ERC20.sol'; 4 | 5 | /// @title Multiplexer 6 | /// @author Chris Hitchcott 7 | /// :repository https://github.com/DigixGlobal/multiplexer 8 | 9 | contract Multiplexer { 10 | 11 | function sendEth(address[] _to, uint256[] _value) payable returns (bool _success) { 12 | // input validation 13 | assert(_to.length == _value.length); 14 | assert(_to.length <= 255); 15 | // count values for refunding sender 16 | uint256 beforeValue = msg.value; 17 | uint256 afterValue = 0; 18 | // loop through to addresses and send value 19 | for (uint8 i = 0; i < _to.length; i++) { 20 | afterValue = afterValue + _value[i]; 21 | assert(_to[i].send(_value[i])); 22 | } 23 | // send back remaining value to sender 24 | uint256 remainingValue = beforeValue - afterValue; 25 | if (remainingValue > 0) { 26 | assert(msg.sender.send(remainingValue)); 27 | } 28 | return true; 29 | } 30 | 31 | function sendErc20(address _tokenAddress, address[] _to, uint256[] _value) returns (bool _success) { 32 | // input validation 33 | assert(_to.length == _value.length); 34 | assert(_to.length <= 255); 35 | // use the erc20 abi 36 | ERC20 token = ERC20(_tokenAddress); 37 | // loop through to addresses and send value 38 | for (uint8 i = 0; i < _to.length; i++) { 39 | assert(token.transferFrom(msg.sender, _to[i], _value[i]) == true); 40 | } 41 | return true; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, DigixGlobal Private Limited 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /scripts/dgd-reward.js: -------------------------------------------------------------------------------- 1 | const Multiplexer = artifacts.require('./Multiplexer.sol'); 2 | const multiplexer = Multiplexer.at(Multiplexer.address); 3 | 4 | console.log('Multiplex Address', Multiplexer.address); 5 | 6 | const ERC20 = artifacts.require('./TestMintableToken.sol'); 7 | const token = ERC20.at('0xe0b7927c4af23765cb51314a0e0521a9645f0e2a'); 8 | const tokenWeiPerAddress = 100000000; 9 | const addresses = [ 10 | '0xc9281a5609b232a6c5ad610c635d552c14f51a9a', 11 | '0x67436a2c32f8e72e9de5bcf1b5f6fff8172d4ad2', 12 | '0xa61b8d13d20a3bddf00702bfe4092981701a7358', 13 | '0xee90ce56e68b011fc1b9e84aac62156b8c27763f', 14 | '0xc2b82e7db33d926ebfc1d92f45a9b234e531b67b', 15 | '0xfca73eff8771c0103ba3cc1a9c259448c72abf0b', 16 | '0x002ffa6895c01838107153adf84c2978166a220f', 17 | '0x79e063a8288dffb4156f42210d765b975aeb0957', 18 | '0x2c34f01fe0fe83fa047c7d891ab661a4cbf882af', 19 | '0x64d683a2cafe27181a30cfe229336cc87bcb5f41', 20 | '0x4a4d417a4766113b8dff28473af6ea401fd23d57', 21 | '0x1fb6bfc715831378def113c9e54122b43842b3cb', 22 | '0xd70e7ae5bfd1bd4561b5bac445d5022b16d5440f', 23 | '0x53c8f3032a89baae2dd8bfd426660775a00efaf3', 24 | '0xe13cec7b2c10574e5e89a780582b66d2c3c71b80', 25 | '0xf58499644d1d27c04df2f1191c4cfe2620a98b5c', 26 | '0x9efe2a4863c1a134667a26deff65490a1cabf687', 27 | '0xea7ec11702a8a13ee56fe0e3742cd35f0c1d3cf9', 28 | '0xff6ef346d8e6c0612a129f135b107123bea60f2b', 29 | '0xa592d5e6134abbdb05f18159331045460d22ae19', 30 | '0x2397c5a2c15ee5224bb3a1bdd902695b15059689', 31 | '0x146764a316ec64237ef6a20bc663b1b276e27c02', 32 | '0x29b2d45dbd7ce98aa9303e31e715b45b94ccfcfa', 33 | '0x00942a25b32de6fc88ef056610aed3ec677966a4', 34 | '0x133d5cb0506fb3406a7fcb0da80636af8755bf63', 35 | '0x2da3f6e73cf65064c2ed31cbbae4e39f0e3c3c04', 36 | '0xff6590510f959b93754d8e399b5b6a0c9c5ba2fc', 37 | '0x5592c1d00d3024bdeb0e5cd5a6096edde5bd2838', 38 | '0xb074278ef8a4612ec0a0d5387447857f1097a157', 39 | '0x33661b5d68c274dea912e801837968abbadeff7e', 40 | '0x00fefa26320ab4ab6ec67bddccc92cf0d7d2d0fb', 41 | '0x5b0733b9adb031c580493b88cdd52f761c195da0', 42 | '0x9c1478134d61258dd2c35de561bd0ff94678ab5e', 43 | '0xbb88dac0ae203632bb5ec80c00fbb6f5d82ec974', 44 | '0x65870575837f98511793041ee5c0cc4db911b01e', 45 | '0x1bcfe4f499db23909a5ddc33d6e2d879531176ec', 46 | '0xafe54cd3dc41336e769096f6ea8080a682a3d9e2', 47 | '0x7205fc2ec03ae1109b35d983407a223ac82c4871', 48 | '0xdae48d7b5c73a95f74caae08f7f92d3b7927f449', 49 | '0xd122d84d5bc9da9a307050019358f6cc4cc6d8fb', 50 | '0xc597044ed3da76cab487f730df41c5d7bad4feb9', 51 | '0x1b00e80c64164c76299157bd5723deb184b0c6d0', 52 | '0x02b1295c5b7d3aa02669f542634f66462b8c9abf', 53 | '0x2a477f1f93d17457a23ba72368743c4b1bff2a28', 54 | '0x1c82ccd574044df0318e039b5d7d9f1f84685e61', 55 | '0x19a588c4867a91402dd3e9d477212289cf9aeca9', 56 | '0xd1ebae724248cb8a14483c6987feeb6c928d8172', 57 | '0x0b450b9d5a9769a6f7adbd8dc2dda0dfb47c5c3a', 58 | '0x63812dbd7d5665e00f92c8e73b36e49fe1b15743', 59 | '0x6375325cb611dee5208499e8296ee640e32dfd18', 60 | '0xf881e5f99a448b95954028071dcb24153381ee74', 61 | '0xe34c208f1d1d10d07e9e948455c64f69d6e6d838', 62 | '0x87c280c758aa49b6aefeeec7257a1de7e8a54214', 63 | '0xc85cf7e6a0cd95abb46d4e6048d7e5c0c6e85205', 64 | '0x3f474cab8fa6f861bb533a70e10c3e944cb0b410', 65 | '0xb55cb753291d726d620502c10843f265e447a6fe', 66 | '0x98c80268276ac6306f27b6430c49e8d0473d753d', 67 | '0xfc5bbc2bbe5fb0a9e2665a7a9f2290ee34d3c64e', 68 | '0x7f2b9a08f35b7b4de85354260f4540302ce1877e', 69 | '0x74e74d0194286014b500186e6224b088153da54e', 70 | '0x5814ba8ac95575318c8f1692dd2f7b687d07b821', 71 | '0xa5da57311bc233b112ab817c732804d2a4c0297c', 72 | '0xdcab44b6fda7d1e0ef6f9d9ead6508e0e1558264', 73 | '0x3af76d7e196322ce4f62dfefedd5778e12df7eaa', 74 | '0xbb1965f10fe59ef9df35be181945d9538eb798b6', 75 | '0x56446712b219fcc34a5604f5e7af5d50d65b6647', 76 | '0x90a64ed7b118631520ac1d71f348ff213ca51817', 77 | '0x45a4175d7cb99dd229aa51c059c68c27adcb94db', 78 | '0xa59a170fa8ae723fd9962e7b72aa1fdb01e90608', 79 | '0xe7c613dd9f8658205556c89f042dcdc7e10c23a0', 80 | '0xf9a8aa349a7774d25a4af1815c24494389995cb8', 81 | '0x9c1363e8a8dac64147c3e5e6533dacf269471247', 82 | '0x51c61b0b86d1c2a742c30b188dcf82acbc9fb09b', 83 | '0x3cc03e1a54f2255beb485187d75a66fb5a574593', 84 | '0x9b422e571eb2cb9837efdc4f9087194d65fb070a', 85 | '0x9b8ab4aaf18f4ea4b18a954462b53469b2dc4b84', 86 | '0x5b237a7cb07c71f4bbe7a4706c7540f76954fde2', 87 | '0xd82811fd89f18a6361286bf7501f5464edb3b789', 88 | '0x9230ad3c01fc8c1929644af5bd360d5115623ddb', 89 | '0x4163eb44702bea470431db626091e310575244ef', 90 | '0xbeb1fd865feea5006c74bcb615ebb00f1a05d469', 91 | '0x1a6e2f78cf14939864b8a701a28eeedf62d9a952', 92 | '0x2a3d46aa866d1be217e33455732948faff9a45b4', 93 | '0x7997894f43112be3bc1c57a910a12b0a350fcf28', 94 | '0x8c0a0ba507ce136c29ec050781ee80697a06e3e9', 95 | '0x2f2b0ea87b0ac0546efa511a5e9c6423e0d9a699', 96 | '0x2947f743dd35f2206098e18a9b3a3928d05d293f', 97 | '0x7b1b26bf04b5b6e1e78f4d105e5726205458b4ef', 98 | '0xe54a5487ea8816559d88eabf26b783fe343eda49', 99 | '0x69017e33796601bf924805b4c5fdb14b0ca7f064', 100 | '0x114f7a44cd8b68ca3841340f1ea54dcece073f5f', 101 | '0x62eca0fb900a26695dd6c0a4fe719987307d4eae', 102 | '0xa297a4d809e4681fbcfbcdfbf389abce02e39d15', 103 | '0xe43e41b65400c8f5da681feb34710bd7552b95de', 104 | '0x1efb00cca7e2b858fcb42f522a400cc77b74783f', 105 | '0xf30bc8ab61e7719961bc9cd4ff1af0f3d62e6d44', 106 | '0xd9486aa30e2e4cc3d4c7145d32b8bc0f365b506a', 107 | '0xc12fd6bf9cb58c76aebf57d3051b869a434e7d26', 108 | '0xfe7fb7f348336508130973ca872251541bb92cd1', 109 | '0x908f47089c11b4a1ee452bd4b72efa244601835e', 110 | '0x1c6f4dfc666da461a42e9b5d890c1d1ec5c6ba16', 111 | '0xfdd01842b99e96dfb0c58d28ecf755c933f3d1d2', 112 | '0x24ab5e1fc2925e54a505ed9fa52b29ee019d2764', 113 | '0xfec13845ea2cb90757501bab2284f378fd20aec2', 114 | '0x34b90a87882d4dc26fac53ea8a5d57938783bcf2', 115 | '0xf1713729be985a4d42730a6884890fe7be9596d0', 116 | '0x72af8296d1272deffe909926d1db18ee418542a8', 117 | '0x0ccd062e8a71fa30a74d06f353db199e5657b6e6', 118 | '0x9dbaee36c621e6c13cf1875fda1ee851940b202e', 119 | '0x882249acdd6f2e0ed067e5c66a6667baa7386b38', 120 | '0xaa4eb3b318e0bf21ad42a6799478ab7f4c634ffe', 121 | '0x36d04a65fb975e4ff71949eb8b568ffb01bd1e15', 122 | '0x14730014eea880f2d52ac0b67442c485242d9b9c', 123 | '0xf7dbc54d3fbba3f1ed9881ae539f1c65d8bce166', 124 | '0xd929ad1e45b7963bf98bb25bcbd3e6db858e9f45', 125 | '0xdae300f0627e35a17f2dfcaac6fda998e393b7d0', 126 | '0xcc9e7ce4c5423abcc24af376ae1a7f456d7b440d', 127 | '0x6fb7f22e5b989004ea1691fcb7ac61cf2df1e3d0', 128 | '0xf1f0c294d247e8691d1118a86ccc669da3c75ca8', 129 | '0x70764718f4f9ac8d5c3691781ebb030e3d6d0444', 130 | '0x65af03a3825d186cdae0296e4bbb4c843b9c1559', 131 | '0xd487ee360225e0a01b32615e0c24798b42860266', 132 | '0xc86e32838e72e728c93296b0ef11303b3d97a7a7', 133 | '0xb17391eb239387c225c481245355299ea3a9c17f', 134 | '0x2253c1f60648cf6190a349d2453c549555feca10', 135 | '0x5bba175464707a3eb9465e12379f7fda215242ab', 136 | '0xb3ddf32ff40efdece57a620c071cd0437e0277fb', 137 | '0x3027a0ef7693523e0f59e843f5faf2259c54e728', 138 | '0xf86a1a041cea941e78af95f4f11179c056b1c817', 139 | '0x3cf2b135b6aa323a5989f7902a806b055bf05649', 140 | '0x061d9764142f8277954c80c610d30b58aae08cdc', 141 | '0xf616b3fc9d272a064d098be6722f7b3dac5022ba', 142 | '0x02e92cc5aff51dbac02fad20c54949dcefca7e28', 143 | ]; 144 | const balances = addresses.map(() => tokenWeiPerAddress); 145 | const total = balances.reduce((t, n) => t + n, 0); 146 | 147 | module.exports = async () => { 148 | console.log(`Sending ${total} token wei`); 149 | // console.log('Setting new allowance', await token.approve(multiplexer.address, total, { gas: 6000000, gasPrice: 12e9 })); 150 | console.log('Sending', await multiplexer.sendErc20(token.address, addresses, balances, { gas: 6000000, gasPrice: 12e9 })); 151 | }; 152 | -------------------------------------------------------------------------------- /test/Multiplexer.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | const crypto = require('crypto'); 3 | 4 | const Multiplexer = artifacts.require('./Multiplexer.sol'); 5 | const ERC20 = artifacts.require('./TestMintableToken.sol'); 6 | 7 | const bn = web3.toBigNumber; 8 | 9 | function getBalance(address) { 10 | return new Promise((resolve) => { 11 | web3.eth.getBalance(address, (e, res) => resolve(res)); 12 | }); 13 | } 14 | 15 | function getBalances(addresses) { 16 | return Promise.all(addresses.map(getBalance)); 17 | } 18 | 19 | function getTokenBalance(token, address) { 20 | return token.balanceOf.call(address); 21 | } 22 | 23 | function getTokenBalances(token, addresses) { 24 | return Promise.all(addresses.map(a => getTokenBalance(token, a))); 25 | } 26 | 27 | 28 | function randomAddress() { 29 | return `0x${crypto.randomBytes(20).toString('hex')}`; 30 | } 31 | 32 | function randomAddresses(n) { 33 | return Array(n).fill().map(randomAddress); 34 | } 35 | 36 | contract('Multiplexer', ([from]) => { 37 | let multiplexer; 38 | before(async () => { 39 | multiplexer = await Multiplexer.new(); 40 | }); 41 | describe('sendEth', () => { 42 | it('sends to a few users', async () => { 43 | const amounts = [1, 2, 3]; 44 | const value = amounts.reduce((n, a) => n + a, 0); 45 | const recipients = randomAddresses(amounts.length); 46 | const before = await getBalances(recipients); 47 | await multiplexer.sendEth(recipients, amounts, { value }); 48 | const after = await getBalances(recipients); 49 | amounts.forEach((a, i) => assert.deepEqual(after[i], before[i].add(amounts[i]))); 50 | assert.deepEqual(await getBalance(multiplexer.address), bn(0)); 51 | }); 52 | it('sends to a lot of users', async () => { 53 | const amounts = Array.from(Array(100).keys()); 54 | const value = amounts.reduce((n, a) => n + a, 0); 55 | const recipients = randomAddresses(amounts.length); 56 | const before = await getBalances(recipients); 57 | await multiplexer.sendEth(recipients, amounts, { value }); 58 | const after = await getBalances(recipients); 59 | amounts.forEach((a, i) => assert.deepEqual(after[i], before[i].add(amounts[i]))); 60 | assert.deepEqual(await getBalance(multiplexer.address), bn(0)); 61 | }); 62 | it('returns to sender when sending more value', async () => { 63 | const amounts = [1, 2, 3, 20]; 64 | const actualValue = amounts.reduce((n, a) => n + a, 0); 65 | const value = 200; 66 | const senderBalance = await getBalance(from); 67 | const recipients = randomAddresses(amounts.length); 68 | const before = await getBalances(recipients); 69 | const gasPrice = 4e9; 70 | const { receipt: { gasUsed } } = await multiplexer.sendEth(recipients, amounts, { value, gasPrice }); 71 | const gasCost = gasPrice * gasUsed; 72 | const after = await getBalances(recipients); 73 | amounts.forEach((a, i) => assert.deepEqual(after[i], before[i].add(amounts[i]))); 74 | assert.deepEqual(await getBalance(multiplexer.address), bn(0)); 75 | assert.deepEqual(await getBalance(from), senderBalance.sub(gasCost).minus(actualValue)); 76 | }); 77 | it('throws when sending to too many users (out of gas)', async () => { 78 | const amounts = Array.from(Array(200).keys()); 79 | const value = amounts.reduce((n, a) => n + a, 0); 80 | const recipients = randomAddresses(amounts.length); 81 | const before = await getBalances(recipients); 82 | let thrown = false; 83 | try { 84 | await multiplexer.sendEth(recipients, amounts, { value }); 85 | } catch (e) { 86 | thrown = true; 87 | } 88 | assert.equal(thrown, true); 89 | const after = await getBalances(recipients); 90 | amounts.forEach((a, i) => assert.deepEqual(after[i], before[i])); 91 | assert.deepEqual(await getBalance(multiplexer.address), bn(0)); 92 | }); 93 | it('throws when sending to too many users (greater than uint8)', async () => { 94 | const amounts = Array.from(Array(256).keys()); 95 | const value = amounts.reduce((n, a) => n + a, 0); 96 | const recipients = randomAddresses(amounts.length); 97 | const before = await getBalances(recipients); 98 | let thrown = false; 99 | try { 100 | await multiplexer.sendEth(recipients, amounts, { value }); 101 | } catch (e) { 102 | thrown = true; 103 | } 104 | assert.equal(thrown, true); 105 | const after = await getBalances(recipients); 106 | amounts.forEach((a, i) => assert.deepEqual(after[i], before[i])); 107 | assert.deepEqual(await getBalance(multiplexer.address), bn(0)); 108 | }); 109 | it('throws when not sending enough value', async () => { 110 | const amounts = [1, 2, 3]; 111 | const recipients = randomAddresses(amounts.length); 112 | const before = await getBalances(recipients); 113 | let thrown = false; 114 | try { 115 | await multiplexer.sendEth(recipients, amounts, { value: 3 }); 116 | } catch (e) { 117 | thrown = true; 118 | } 119 | assert.equal(thrown, true); 120 | const after = await getBalances(recipients); 121 | amounts.forEach((a, i) => assert.deepEqual(after[i], before[i])); 122 | assert.deepEqual(await getBalance(multiplexer.address), bn(0)); 123 | }); 124 | }); 125 | 126 | describe('sendErc20', () => { 127 | const initialTokens = 5000; 128 | let erc20; 129 | beforeEach(async () => { 130 | erc20 = await ERC20.new(); 131 | await erc20.mint(from, initialTokens); 132 | }); 133 | it('sends to a few users', async () => { 134 | const amounts = [1, 2, 3]; 135 | const value = amounts.reduce((n, a) => n + a, 0); 136 | const recipients = randomAddresses(amounts.length); 137 | const before = await getTokenBalances(erc20, recipients); 138 | await erc20.approve(multiplexer.address, value); 139 | await multiplexer.sendErc20(erc20.address, recipients, amounts); 140 | const after = await getTokenBalances(erc20, recipients); 141 | amounts.forEach((a, i) => assert.deepEqual(after[i], before[i].add(amounts[i]))); 142 | assert.deepEqual(await getTokenBalance(erc20, from), bn(initialTokens - value)); 143 | }); 144 | it('sends to a lot of users', async () => { 145 | const amounts = Array.from(Array(100).keys()); 146 | const value = amounts.reduce((n, a) => n + a, 0); 147 | const recipients = randomAddresses(amounts.length); 148 | const before = await getTokenBalances(erc20, recipients); 149 | await erc20.approve(multiplexer.address, value); 150 | await multiplexer.sendErc20(erc20.address, recipients, amounts); 151 | const after = await getTokenBalances(erc20, recipients); 152 | amounts.forEach((a, i) => assert.deepEqual(after[i], before[i].add(amounts[i]))); 153 | assert.deepEqual(await getTokenBalance(erc20, from), bn(initialTokens - value)); 154 | }); 155 | it('throws when sending to too many users (out of gas)', async () => { 156 | const amounts = Array.from(Array(200).keys()); 157 | const value = amounts.reduce((n, a) => n + a, 0); 158 | const recipients = randomAddresses(amounts.length); 159 | const before = await getTokenBalances(erc20, recipients); 160 | await erc20.approve(multiplexer.address, value); 161 | let thrown = false; 162 | try { 163 | await multiplexer.sendErc20(erc20.address, recipients, amounts); 164 | } catch (e) { 165 | thrown = true; 166 | } 167 | assert.equal(thrown, true); 168 | const after = await getBalances(recipients); 169 | amounts.forEach((a, i) => assert.deepEqual(after[i], before[i])); 170 | assert.deepEqual(await getTokenBalance(erc20, from), bn(initialTokens)); 171 | }); 172 | it('throws when sending to too many users (greater than uint8)', async () => { 173 | const amounts = Array.from(Array(256).keys()); 174 | const value = amounts.reduce((n, a) => n + a, 0); 175 | const recipients = randomAddresses(amounts.length); 176 | const before = await getTokenBalances(erc20, recipients); 177 | await erc20.approve(multiplexer.address, value); 178 | let thrown = false; 179 | try { 180 | await multiplexer.sendErc20(erc20.address, recipients, amounts); 181 | } catch (e) { 182 | thrown = true; 183 | } 184 | assert.equal(thrown, true); 185 | const after = await getBalances(recipients); 186 | amounts.forEach((a, i) => assert.deepEqual(after[i], before[i])); 187 | assert.deepEqual(await getTokenBalance(erc20, from), bn(initialTokens)); 188 | }); 189 | it('throws when not approved enough', async () => { 190 | const amounts = [1, 2, 3]; 191 | const recipients = randomAddresses(amounts.length); 192 | const before = await getTokenBalances(erc20, recipients); 193 | await erc20.approve(multiplexer.address, 2); 194 | let thrown = false; 195 | try { 196 | await multiplexer.sendErc20(erc20.address, recipients, amounts); 197 | } catch (e) { 198 | thrown = true; 199 | } 200 | assert.equal(thrown, true); 201 | const after = await getBalances(recipients); 202 | amounts.forEach((a, i) => assert.deepEqual(after[i], before[i])); 203 | assert.deepEqual(await getTokenBalance(erc20, from), bn(initialTokens)); 204 | }); 205 | }); 206 | }); 207 | --------------------------------------------------------------------------------