├── .babelrc ├── .eslintrc.json ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .jshintrc ├── README.md ├── contracts ├── PaymentPool.sol └── Token.sol ├── index.js ├── lib ├── cumulative-payment-tree.js └── merkle-tree.js ├── package.json ├── test ├── helpers │ └── utils.js └── payment-pool-test.js ├── truffle-config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2017, 4 | "sourceType": "module" 5 | }, 6 | "extends": "eslint:recommended", 7 | "env": { 8 | "node": true, 9 | "mocha": true 10 | }, 11 | "globals": { 12 | "web3": true, 13 | "contract": true, 14 | "artifacts": true, 15 | "assert": true 16 | }, 17 | "rules": { 18 | "no-console": "warn", 19 | "semi": "error" 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Merkle Tree Payment Pool Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: volta-cli/action@v1 16 | - uses: actions/cache@v2 17 | with: 18 | path: ~/.cache/yarn 19 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 20 | - name: Install packages 21 | run: yarn install 22 | - name: Run truffle test 23 | run: yarn test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /build 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # misc 12 | npm-debug.log 13 | testem.log 14 | .DS_Store 15 | .node-* 16 | accounts.txt 17 | *.csv 18 | *.log 19 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 7 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Merkle-Tree Payment Pool [![Build Status](https://travis-ci.org/cardstack/merkle-tree-payment-pool.svg?branch=master)](https://travis-ci.org/cardstack/merkle-tree-payment-pool) 2 | 3 | This is an implementation of a Merkle Tree based payment pool in Solidity for ERC-20 tokens. This project was inspired by this Ethereum research post: https://ethresear.ch/t/pooled-payments-scaling-solution-for-one-to-many-transactions/590. A longer description around the motivations behind this project is available here: https://medium.com/cardstack/scalable-payment-pools-in-solidity-d97e45fc7c5c. This project includes a payment pool smart contract that leverages Merkle Trees. Also included is a JS lib to create Merkle Trees, derive Merkle roots, and Merkle proofs that have metadata attached to the proofs that aid this smart contract in managing the payment pool. 4 | 5 | The key feature behind this payment pool, is that by using a Merkle tree to represent the list of payees and their payment amounts, we can specify arbitrarily large amounts of payees and their payment amounts simply by specifying a 32 byte Merkle root of a Merkle tree that represents the payee list. Payees can then withdraw their payments by providing the payment pool with the Merkle proof associated with the payee. This solution does rely on an off-chain mechanism to derive the Merkle tree for each payment cycle, as well as to publish the Merkle proofs for the payees in manner that payees can easily discover their proofs (e.g. IPFS). 6 | 7 | ## Prerequisites 8 | * Node 7.6 or greater 9 | * Yarn 10 | 11 | ## Setting up 12 | 1. run `yarn install` within the project 13 | 2. run `npm test` to run the tests 14 | 15 | ## How It Works 16 | The way this payment pool works is that for each *payment cycle* the contract owner derives a Merkle tree for a list of payees that recieve payment during the payment cycle. Each payment cycle is numbered, with the first payment cycle start at `1` when the contract is deployed. To look up the current payment cycle use the contract function `paymentPool.numPaymentCycles()`. This project includes a javascript abstraction for a payee-list based Merkle tree, `CumulativePaymentTree`, that you can use to manage the Merkle tree for the list of payees for each payment cycle. 17 | 18 | Link and deploy the `PaymentPool` contract specying whatever ERC-20 token will be governed by the payment pool. From the tests this looks like the following (a truffle migration script would follow a similar approach): 19 | ```js 20 | const PaymentPool = artifacts.require('./PaymentPool.sol'); 21 | const MerkleProofLib = artifacts.require('MerkleProof.sol'); // From open zeppelin 22 | const Token = artifacts.require('./Token.sol'); // This is just a sample ERC-20 token 23 | 24 | let merkleProofLib = await MerkleProofLib.new(); 25 | let token = await Token.new(); 26 | PaymentPool.link('MerkleProof', merkleProofLib.address); 27 | 28 | await PaymentPool.new(token.address); 29 | ``` 30 | 31 | Assemble the list of payees and their cumulative payment amounts. The payment amounts need to be cumulative across all the payment cycles in order for the payment pool to calculate the current amount available to a payee for their provided proof. The cumulative amounts should never decrease in subsequent payment cycles. The amounts represent the amounts in the ERC-20 token that is specified when the `PaymentPool` contract is deployed. 32 | 33 | ```js 34 | let paymentList = [{ 35 | payee: "0x627306090abab3a6e1400e9345bc60c78a8bef57", 36 | amount: 20 37 | },{ 38 | payee: "0xf17f52151ebef6c7334fad080c5704d77216b732", 39 | amount: 12 40 | },{ 41 | payee: "0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef", 42 | amount: 15 43 | }]; 44 | ``` 45 | 46 | 47 | Instantiate an instance of the `CumulativePaymentTree` class with the payee list to build the Merkle tree: 48 | ```js 49 | import CumulativePaymentTree from '../lib/cumulative-payment-tree.js'; 50 | 51 | let paymentTree = new CumulativePaymentTree(paymentList); 52 | ``` 53 | 54 | 55 | Note the current payment cycle number by querying the `PaymentPool` contract: 56 | ```js 57 | let paymentCycleNumber = await paymentPool.numPaymentCycles(); 58 | ``` 59 | 60 | 61 | Retreive the root of the payment list's Merkle tree and submit to the `PaymentPool` contract. Note that submitting the Merkle root triggers the end of the current payment cycle, and a new cycle is started: 62 | ```js 63 | let root = paymentTree.getHexRoot(); 64 | await paymentPool.submitPayeeMerkleRoot(root); 65 | ``` 66 | 67 | 68 | Retreive the Merkle proof for each payee in the payment list while providing the payment cycle number of the payment cycle that just ended. Then publish the Merkle proof off-chain for each payee in a place that is easily accessible, like IPFS. 69 | 70 | It is probably a good idea to organize the published proofs by payment cycle number for each payee, as the payee will generally want to use the latest proof (to retrieve the most tokens). But an older proof can be used too for retrieving tokens, provided all the tokens haven't already been withdrawn using an older proof. For extra credit, perhaps add a link to a dApp that can display the available balance for each payee's proof. 71 | 72 | ```js 73 | // `paymentCycleNumber` is set to the paymentCycle that ended when the root was submitted 74 | paymentList.forEach(({ payee }) => { 75 | let proof = paymentTree.hexProofForPayee(payee, paymentCycle); 76 | console.log(`Payee ${payee} proof is: ${proof}`); 77 | }); 78 | ``` 79 | 80 | 81 | A payee can view the balance the is available to be withdrawn from the payment pool using their proof by calling the `PaymentPool` contract: 82 | ```js 83 | let balance = await paymentPool.balanceForProofWithAddress(payeeAddress, proof); 84 | ``` 85 | 86 | 87 | A payee can then withdraw tokens from the payment pool using their proof by calling the `PaymentPool` contract. A payee is allowed to withdraw any amount up to the amount allowed by the proof. The payees' withdrawals are tracked by the payment pool, such that a payee cannot withdraw more tokens than they are allotted from the payment pool: 88 | ```js 89 | await paymentPool.withdraw(15, proof); // withdraw 15 tokens from the payment pool 90 | ``` 91 | 92 | 93 | Feel free to checkout the tests for more examples. 94 | -------------------------------------------------------------------------------- /contracts/PaymentPool.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.17; 2 | 3 | import '@openzeppelin/contracts/math/SafeMath.sol'; 4 | import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; 5 | import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; 6 | import '@openzeppelin/contracts/cryptography/MerkleProof.sol'; 7 | import '@openzeppelin/contracts/ownership/Ownable.sol'; 8 | 9 | contract PaymentPool is Ownable { 10 | 11 | using SafeMath for uint256; 12 | using SafeERC20 for ERC20; 13 | using MerkleProof for bytes32[]; 14 | 15 | ERC20 public token; 16 | uint256 public numPaymentCycles = 1; 17 | mapping(address => uint256) public withdrawals; 18 | 19 | mapping(uint256 => bytes32) payeeRoots; 20 | uint256 currentPaymentCycleStartBlock; 21 | 22 | event PaymentCycleEnded(uint256 paymentCycle, uint256 startBlock, uint256 endBlock); 23 | event PayeeWithdraw(address indexed payee, uint256 amount); 24 | 25 | constructor (ERC20 _token) public { 26 | token = _token; 27 | currentPaymentCycleStartBlock = block.number; 28 | } 29 | 30 | function startNewPaymentCycle() internal onlyOwner returns(bool) { 31 | require(block.number > currentPaymentCycleStartBlock); 32 | 33 | emit PaymentCycleEnded(numPaymentCycles, currentPaymentCycleStartBlock, block.number); 34 | 35 | numPaymentCycles = numPaymentCycles.add(1); 36 | currentPaymentCycleStartBlock = block.number.add(1); 37 | 38 | return true; 39 | } 40 | 41 | function submitPayeeMerkleRoot(bytes32 payeeRoot) public onlyOwner returns(bool) { 42 | payeeRoots[numPaymentCycles] = payeeRoot; 43 | 44 | startNewPaymentCycle(); 45 | 46 | return true; 47 | } 48 | 49 | function balanceForProofWithAddress(address _address, bytes memory proof) public view returns(uint256) { 50 | bytes32[] memory meta; 51 | bytes32[] memory _proof; 52 | 53 | (meta, _proof) = splitIntoBytes32(proof, 2); 54 | if (meta.length != 2) { return 0; } 55 | 56 | uint256 paymentCycleNumber = uint256(meta[0]); 57 | uint256 cumulativeAmount = uint256(meta[1]); 58 | if (payeeRoots[paymentCycleNumber] == 0x0) { return 0; } 59 | 60 | bytes32 leaf = keccak256( 61 | abi.encodePacked( 62 | _address, 63 | cumulativeAmount 64 | ) 65 | ); 66 | if (withdrawals[_address] < cumulativeAmount && 67 | _proof.verify(payeeRoots[paymentCycleNumber], leaf)) { 68 | return cumulativeAmount.sub(withdrawals[_address]); 69 | } else { 70 | return 0; 71 | } 72 | } 73 | 74 | 75 | function balanceForProof(bytes memory proof) public view returns(uint256) { 76 | return balanceForProofWithAddress(msg.sender, proof); 77 | } 78 | 79 | function withdraw(uint256 amount, bytes memory proof) public returns(bool) { 80 | require(amount > 0); 81 | require(token.balanceOf(address(this)) >= amount); 82 | 83 | uint256 balance = balanceForProof(proof); 84 | require(balance >= amount); 85 | 86 | withdrawals[msg.sender] = withdrawals[msg.sender].add(amount); 87 | token.safeTransfer(msg.sender, amount); 88 | 89 | emit PayeeWithdraw(msg.sender, amount); 90 | } 91 | 92 | 93 | function splitIntoBytes32(bytes memory byteArray, uint256 numBytes32) internal pure returns (bytes32[] memory bytes32Array, 94 | bytes32[] memory remainder) { 95 | if ( byteArray.length % 32 != 0 || 96 | byteArray.length < numBytes32.mul(32) || 97 | byteArray.length.div(32) > 50) { // Arbitrarily limiting this function to an array of 50 bytes32's to conserve gas 98 | 99 | bytes32Array = new bytes32[](0); 100 | remainder = new bytes32[](0); 101 | return (bytes32Array, remainder); 102 | } 103 | 104 | bytes32Array = new bytes32[](numBytes32); 105 | remainder = new bytes32[](byteArray.length.sub(64).div(32)); 106 | bytes32 _bytes32; 107 | for (uint256 k = 32; k <= byteArray.length; k = k.add(32)) { 108 | assembly { 109 | _bytes32 := mload(add(byteArray, k)) 110 | } 111 | if(k <= numBytes32*32){ 112 | bytes32Array[k.sub(32).div(32)] = _bytes32; 113 | } else { 114 | remainder[k.sub(96).div(32)] = _bytes32; 115 | } 116 | } 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /contracts/Token.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.17; 2 | 3 | import '@openzeppelin/contracts/token/ERC20/ERC20Mintable.sol'; 4 | 5 | contract Token is ERC20Mintable { } 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('@babel/register'); 2 | require("core-js/stable"); 3 | require("regenerator-runtime/runtime"); 4 | 5 | module.exports = { 6 | MerkleTree: require('./lib/merkle-tree').default, 7 | CumulativePaymentTree: require('./lib/cumulative-payment-tree').default 8 | }; 9 | 10 | -------------------------------------------------------------------------------- /lib/cumulative-payment-tree.js: -------------------------------------------------------------------------------- 1 | import MerkleTree from './merkle-tree'; 2 | import { bufferToHex, zeros } from 'ethereumjs-util'; 3 | import _ from 'lodash/lodash'; 4 | 5 | /* 6 | * `paymentList` is an array of objects that have a property `payee` to hold the 7 | * payee's Ethereum address and `amount` to hold the cumulative amount of tokens 8 | * paid to the payee across all payment cycles: 9 | * 10 | * [{ 11 | * payee: "0x627306090abab3a6e1400e9345bc60c78a8bef57", 12 | * amount: 20 13 | * },{ 14 | * payee: "0xf17f52151ebef6c7334fad080c5704d77216b732", 15 | * amount: 12 16 | * },{ 17 | * payee: "0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef", 18 | * amount: 15 19 | * }] 20 | * 21 | */ 22 | 23 | export default class CumulativePaymentTree extends MerkleTree { 24 | constructor(paymentList) { 25 | let filteredPaymentList = paymentList.filter(payment => payment.payee && payment.amount); 26 | let groupedPayees = _.groupBy(filteredPaymentList, payment => payment.payee); 27 | let reducedPaymentList = Object.keys(groupedPayees).map(payee => { 28 | let payments = groupedPayees[payee]; 29 | let amount = _.reduce(payments, (sum, payment) => sum + payment.amount, 0); 30 | return { payee, amount }; 31 | }); 32 | super(reducedPaymentList); 33 | this.paymentNodes = reducedPaymentList 34 | } 35 | 36 | amountForPayee(payee) { 37 | let payment = _.find(this.paymentNodes, { payee }); 38 | if (!payment) { return 0; } 39 | 40 | return payment.amount; 41 | } 42 | 43 | hexProofForPayee(payee, paymentCycle) { 44 | let leaf = _.find(this.paymentNodes, {payee}) 45 | if (!leaf) { return bufferToHex(zeros(32)); } 46 | return this.getHexProof(leaf, [ paymentCycle, this.amountForPayee(payee) ]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/merkle-tree.js: -------------------------------------------------------------------------------- 1 | import { bufferToHex, toBuffer, setLengthLeft, keccak256 } from 'ethereumjs-util'; 2 | import { soliditySha3, hexToBytes} from 'web3-utils' 3 | 4 | 5 | export default class MerkleTree { 6 | constructor (elements) { 7 | // Filter empty strings and hash elements 8 | this.elements = elements.filter(el => el).map(el => this.sha3(el)); 9 | 10 | // Deduplicate elements 11 | this.elements = this.bufDedup(this.elements); 12 | // Sort elements 13 | this.elements.sort(Buffer.compare); 14 | 15 | // Create layers 16 | this.layers = this.getLayers(this.elements); 17 | } 18 | 19 | getLayers (elements) { 20 | if (elements.length === 0) { 21 | return [['']]; 22 | } 23 | 24 | const layers = []; 25 | layers.push(elements); 26 | 27 | // Get next layer until we reach the root 28 | while (layers[layers.length - 1].length > 1) { 29 | layers.push(this.getNextLayer(layers[layers.length - 1])); 30 | } 31 | 32 | return layers; 33 | } 34 | getNextLayer (elements) { 35 | return elements.reduce((layer, el, idx, arr) => { 36 | if (idx % 2 === 0) { 37 | // Hash the current element with its pair element 38 | layer.push(this.combinedHash(el, arr[idx + 1])); 39 | } 40 | 41 | return layer; 42 | }, []); 43 | } 44 | 45 | combinedHash(first,second ) { 46 | if (!first) { return second; } 47 | if (!second) { return first; } 48 | return keccak256(this.sortAndConcat(first,second)) // Identical to: Buffer.from(hexToBytes(soliditySha3({t: 'bytes', v: this.sortAndConcat(first,second).toString("hex")}))) 49 | } 50 | 51 | getRoot () { 52 | return this.layers[this.layers.length - 1][0]; 53 | } 54 | 55 | getHexRoot () { 56 | return bufferToHex(this.getRoot()); 57 | } 58 | 59 | getProof (el, prefix) { 60 | let idx = this.bufIndexOf(el, this.elements); 61 | 62 | if (idx === -1) { 63 | throw new Error('Element does not exist in Merkle tree'); 64 | } 65 | 66 | let proof = this.layers.reduce((proof, layer) => { 67 | const pairElement = this.getPairElement(idx, layer); 68 | 69 | if (pairElement) { 70 | proof.push(pairElement); 71 | } 72 | 73 | idx = Math.floor(idx / 2); 74 | 75 | return proof; 76 | }, []); 77 | 78 | if (prefix) { 79 | if (!Array.isArray(prefix)) { 80 | prefix = [ prefix ]; 81 | } 82 | prefix = prefix.map(item => setLengthLeft(toBuffer(item), 32)); 83 | proof = prefix.concat(proof); 84 | } 85 | 86 | return proof; 87 | } 88 | 89 | getHexProof (el, prefix) { 90 | const proof = this.getProof(el, prefix); 91 | 92 | return this.bufArrToHex(proof); 93 | } 94 | 95 | getPairElement (idx, layer) { 96 | const pairIdx = idx % 2 === 0 ? idx + 1 : idx - 1; 97 | 98 | if (pairIdx < layer.length) { 99 | return layer[pairIdx]; 100 | } else { 101 | return null; 102 | } 103 | } 104 | 105 | bufIndexOf (el, arr) { 106 | let hash; 107 | 108 | // Convert element to 32 byte hash if it is not one already 109 | if (el.length !== 32 || !Buffer.isBuffer(el)) { 110 | hash = this.sha3(el); 111 | } else { 112 | hash = el; 113 | } 114 | 115 | for (let i = 0; i < arr.length; i++) { 116 | if (hash.equals(arr[i])) { 117 | return i; 118 | } 119 | } 120 | 121 | return -1; 122 | } 123 | 124 | bufDedup (elements) { 125 | return elements.filter((el, idx) => { 126 | return this.bufIndexOf(el, elements) === idx; 127 | }); 128 | } 129 | 130 | bufArrToHex (arr) { 131 | if (arr.some(el => !Buffer.isBuffer(el))) { 132 | throw new Error('Array is not an array of buffers'); 133 | } 134 | 135 | return '0x' + arr.map(el => el.toString('hex')).join(''); 136 | } 137 | 138 | sortAndConcat (...args) { 139 | return Buffer.concat([...args].sort(Buffer.compare)); 140 | } 141 | 142 | sha3 (node) { 143 | return Buffer.from(hexToBytes(soliditySha3({t: 'address', v: node["payee"]}, {t: "uint256", v: node["amount"] }))) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "merkle-tree-payment-pool", 3 | "description": "This is an implementation of a Merkle Tree based payment pool in Solidity for ERC-20 tokens. This project was inspired by this Ethereum research post: https://ethresear.ch/t/pooled-payments-scaling-solution-for-one-to-many-transactions/590. A longer description around the motivations behind this project is available here: https://medium.com/cardstack/scalable-payment-pools-in-solidity-d97e45fc7c5c. This project includes a payment pool smart contract that leverages Merkle Trees. Also included is a JS lib to create Merkle Trees, derive Merkle roots, and Merkle proofs that have metadata attached to the proofs that aid this smart contract in managing the payment pool.", 4 | "version": "1.0.1", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "./node_modules/.bin/truffle test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/cardstack/merkle-tree-payment-pool.git" 15 | }, 16 | "keywords": [ 17 | "ethereum", 18 | "solidity", 19 | "merkle-tree", 20 | "payment-pool" 21 | ], 22 | "author": "Hassan Abdel-Rahman", 23 | "homepage": "https://github.com/cardstack/merkle-tree-payment-pool#readme", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/cardstack/merkle-tree-payment-pool/issues" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.14.3", 30 | "@babel/preset-env": "^7.14.2", 31 | "@babel/register": "^7.13.16", 32 | "@openzeppelin/contracts": "2.5.0", 33 | "core-js": "^3.12.1", 34 | "ethereumjs-util": "^7.0.10", 35 | "lodash": "^4.17.4", 36 | "regenerator-runtime": "^0.13.7", 37 | "truffle": "^5.3.6", 38 | "web3-utils": "^1.3.6" 39 | }, 40 | "volta": { 41 | "node": "14.16.1", 42 | "yarn": "1.22.10" 43 | }, 44 | "dependencies": {} 45 | } 46 | -------------------------------------------------------------------------------- /test/helpers/utils.js: -------------------------------------------------------------------------------- 1 | export const assertRevert = async function (block, msg) { 2 | let err; 3 | try { 4 | await block(); 5 | } catch (e) { 6 | err = e; 7 | } 8 | 9 | if (!err) { return assert.isOk(err, "Revert should have been fired, instead no error fired"); } 10 | 11 | if (msg) { 12 | return assert.isOk(err.message.search(msg) > -1, 13 | msg + " should have been fired, instead:" + err.message); 14 | } else { 15 | return assert.isOk(err.message.search("revert") > -1, 16 | "revert should have been fired, instead:" + err.message); 17 | } 18 | }; 19 | 20 | 21 | export const advanceBlock = (web3) => { //passes local ganache web3 22 | return new Promise((resolve, reject) => { 23 | web3.currentProvider.send({ 24 | jsonrpc: '2.0', 25 | method: 'evm_mine', 26 | id: new Date().getTime() 27 | }, (err, result) => { 28 | if (err) { return reject(err) } 29 | const newBlockHash = web3.eth.getBlock('latest').hash 30 | 31 | return resolve(newBlockHash) 32 | }) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /test/payment-pool-test.js: -------------------------------------------------------------------------------- 1 | import CumulativePaymentTree from '../lib/cumulative-payment-tree.js'; 2 | import { assertRevert, advanceBlock } from './helpers/utils'; 3 | import {toHex, soliditySha3} from "web3-utils" 4 | 5 | const PaymentPool = artifacts.require('./PaymentPool.sol'); 6 | const Token = artifacts.require('./Token.sol'); 7 | const MerkleProofLib = artifacts.require('MerkleProof.sol'); 8 | 9 | contract('PaymentPool', function(accounts) { 10 | describe("payment pool", function() { 11 | let paymentPool; 12 | let token; 13 | let payments = [{ 14 | payee: accounts[2], 15 | amount: 10 16 | },{ 17 | payee: accounts[3], 18 | amount: 12 19 | },{ 20 | payee: accounts[4], 21 | amount: 2, 22 | },{ 23 | payee: accounts[5], 24 | amount: 1 25 | },{ 26 | payee: accounts[6], 27 | amount: 32 28 | },{ 29 | payee: accounts[7], 30 | amount: 10 31 | },{ 32 | payee: accounts[8], 33 | amount: 9 34 | },{ 35 | payee: accounts[9], 36 | amount: 101 // this amount is used to test logic when the payment pool doesn't have sufficient funds 37 | }]; 38 | let initialBlockNumber; 39 | beforeEach(async function() { 40 | let merkleProofLib = await MerkleProofLib.new(); 41 | token = await Token.new(); 42 | PaymentPool.link('MerkleProof', merkleProofLib.address); 43 | paymentPool = await PaymentPool.new(token.address); 44 | initialBlockNumber = await web3.eth.getBlockNumber(); 45 | 46 | }); 47 | 48 | afterEach(async function() { 49 | payments[0].amount = 10; // one of the tests is bleeding state... 50 | }); 51 | 52 | describe("submitPayeeMerkleRoot", function() { 53 | it("starts a new payment cycle after the payee merkle root is submitted", async function() { 54 | let merkleTree = new CumulativePaymentTree(payments); 55 | let root = merkleTree.getHexRoot(); 56 | let paymentCycleNumber = await paymentPool.numPaymentCycles(); 57 | assert.equal(paymentCycleNumber.toNumber(), 1, 'the payment cycle number is correct'); 58 | 59 | let txn = await paymentPool.submitPayeeMerkleRoot(root); 60 | let currentBlockNumber = await web3.eth.getBlockNumber(); 61 | paymentCycleNumber = await paymentPool.numPaymentCycles(); 62 | 63 | assert.equal(paymentCycleNumber.toNumber(), 2, "the payment cycle number is correct"); 64 | assert.equal(txn.logs.length, 1, "the correct number of events were fired"); 65 | 66 | let event = txn.logs[0]; 67 | assert.equal(event.event, "PaymentCycleEnded", "the event type is correct"); 68 | assert.equal(event.args.paymentCycle, 1, "the payment cycle number is correct"); 69 | assert.equal(event.args.startBlock, initialBlockNumber, "the payment cycle start block is correct"); 70 | assert.equal(event.args.endBlock, currentBlockNumber, "the payment cycle end block is correct"); 71 | }); 72 | 73 | it("allows a new merkle root to be submitted in a block after the previous payment cycle has ended", async function() { 74 | let merkleTree = new CumulativePaymentTree(payments); 75 | let root = merkleTree.getHexRoot(); 76 | await paymentPool.submitPayeeMerkleRoot(root); 77 | 78 | let updatedPayments = payments.slice(); 79 | updatedPayments[0].amount += 10; 80 | let updatedMerkleTree = new CumulativePaymentTree(updatedPayments); 81 | let updatedRoot = updatedMerkleTree.getHexRoot(); 82 | 83 | await advanceBlock(web3) 84 | 85 | await paymentPool.submitPayeeMerkleRoot(updatedRoot); 86 | 87 | let paymentCycleNumber = await paymentPool.numPaymentCycles(); 88 | 89 | assert.equal(paymentCycleNumber.toNumber(), 3, "the payment cycle number is correct"); 90 | }); 91 | 92 | it("does not allow 2 merkle roots to be submitted in the same block after the previous payment cycle has ended", async function() { 93 | let merkleTree = new CumulativePaymentTree(payments); 94 | let root = merkleTree.getHexRoot(); 95 | await paymentPool.submitPayeeMerkleRoot(root); 96 | 97 | let updatedPayments = payments.slice(); 98 | updatedPayments[0].amount += 10; 99 | let updatedMerkleTree = new CumulativePaymentTree(updatedPayments); 100 | let updatedRoot = updatedMerkleTree.getHexRoot(); 101 | 102 | await assertRevert(async () => await paymentPool.submitPayeeMerkleRoot(updatedRoot)); 103 | 104 | let paymentCycleNumber = await paymentPool.numPaymentCycles(); 105 | 106 | assert.equal(paymentCycleNumber.toNumber(), 2, "the payment cycle number is correct"); 107 | }); 108 | 109 | it("does not allow non-owner to submit merkle root", async function() { 110 | let merkleTree = new CumulativePaymentTree(payments); 111 | let root = merkleTree.getHexRoot(); 112 | 113 | await assertRevert(async () => paymentPool.submitPayeeMerkleRoot(root, { from: accounts[2] })); 114 | let paymentCycleNumber = await paymentPool.numPaymentCycles(); 115 | 116 | assert.equal(paymentCycleNumber.toNumber(), 1, "the payment cycle number is correct"); 117 | }); 118 | }); 119 | 120 | describe("balanceForProof", function() { 121 | let paymentPoolBalance; 122 | let paymentCycle; 123 | let proof; 124 | let payeeIndex = 0; 125 | let payee = payments[payeeIndex].payee; 126 | let paymentAmount = payments[payeeIndex].amount; 127 | let merkleTree = new CumulativePaymentTree(payments); 128 | let root = merkleTree.getHexRoot(); 129 | 130 | beforeEach(async function() { 131 | paymentPoolBalance = 100; 132 | await token.mint(paymentPool.address, paymentPoolBalance); 133 | paymentCycle = await paymentPool.numPaymentCycles(); 134 | paymentCycle = paymentCycle.toNumber(); 135 | proof = merkleTree.hexProofForPayee(payee, paymentCycle); 136 | await paymentPool.submitPayeeMerkleRoot(root); 137 | }); 138 | 139 | it("payee can get their available balance in the payment pool from their proof", async function() { 140 | let balance = await paymentPool.balanceForProof(proof, { from: payee }); 141 | assert.equal(balance.toNumber(), paymentAmount, "the balance is correct"); 142 | }); 143 | 144 | it("non-payee can get the available balance in the payment pool for an address and proof", async function() { 145 | let balance = await paymentPool.balanceForProofWithAddress(payee, proof); 146 | assert.equal(balance.toNumber(), paymentAmount, "the balance is correct"); 147 | }); 148 | 149 | it("an invalid proof/address pair returns a balance of 0 in the payment pool", async function() { 150 | let differentPayee = payments[4].payee; 151 | let differentUsersProof = merkleTree.hexProofForPayee(differentPayee, paymentCycle); 152 | let balance = await paymentPool.balanceForProofWithAddress(payee, differentUsersProof); 153 | assert.equal(balance.toNumber(), 0, "the balance is correct"); 154 | }); 155 | 156 | it("garbage proof data returns a balance of 0 in payment pool", async function() { 157 | const randomProof = web3.utils.randomHex(32*5) 158 | let balance = await paymentPool.balanceForProofWithAddress(payee, randomProof); 159 | assert.equal(balance.toNumber(), 0, "the balance is correct"); 160 | }); 161 | 162 | it("can handle balance for proofs from different payment cycles", async function() { 163 | let updatedPayments = payments.slice(); 164 | let updatedPaymentAmount = 20; 165 | updatedPayments[payeeIndex].amount = updatedPaymentAmount; 166 | let updatedMerkleTree = new CumulativePaymentTree(updatedPayments); 167 | let updatedRoot = updatedMerkleTree.getHexRoot(); 168 | 169 | await advanceBlock(web3) 170 | 171 | let paymentCycle = await paymentPool.numPaymentCycles(); 172 | paymentCycle = paymentCycle.toNumber(); 173 | let updatedProof = updatedMerkleTree.hexProofForPayee(payee, paymentCycle); 174 | await paymentPool.submitPayeeMerkleRoot(updatedRoot); 175 | 176 | let balance = await paymentPool.balanceForProof(updatedProof, { from: payee }); 177 | assert.equal(balance.toNumber(), updatedPaymentAmount, "the balance is correct for the updated proof"); 178 | 179 | balance = await paymentPool.balanceForProof(proof, { from: payee }); 180 | assert.equal(balance.toNumber(), paymentAmount, "the balance is correct for the original proof"); 181 | }); 182 | 183 | it("balance of payee that has 0 tokens in payment list returns 0 balance in payment pool", async function() { 184 | let aPayee = accounts[1]; 185 | let updatedPayments = payments.slice(); 186 | updatedPayments.push({ payee: aPayee, amount: 0 }); 187 | let updatedMerkleTree = new CumulativePaymentTree(updatedPayments); 188 | let updatedRoot = updatedMerkleTree.getHexRoot(); 189 | 190 | await advanceBlock(web3) 191 | 192 | let paymentCycle = await paymentPool.numPaymentCycles(); 193 | paymentCycle = paymentCycle.toNumber(); 194 | let updatedProof = updatedMerkleTree.hexProofForPayee(aPayee, paymentCycle); 195 | await paymentPool.submitPayeeMerkleRoot(updatedRoot); 196 | 197 | let balance = await paymentPool.balanceForProof(updatedProof, { from: aPayee }); 198 | assert.equal(balance.toNumber(), 0, "the balance is correct for the updated proof"); 199 | }); 200 | 201 | it("balance of proof for payee that has mulitple entries in the payment list returns the sum of all their amounts in the payment pool", async function() { 202 | let updatedPayments = payments.slice(); 203 | updatedPayments.push({ 204 | payee, 205 | amount: 8 206 | }); 207 | let updatedMerkleTree = new CumulativePaymentTree(updatedPayments); 208 | let updatedRoot = updatedMerkleTree.getHexRoot(); 209 | 210 | await advanceBlock(web3) 211 | 212 | let paymentCycle = await paymentPool.numPaymentCycles(); 213 | paymentCycle = paymentCycle.toNumber(); 214 | let updatedProof = updatedMerkleTree.hexProofForPayee(payee, paymentCycle); 215 | await paymentPool.submitPayeeMerkleRoot(updatedRoot); 216 | 217 | let balance = await paymentPool.balanceForProof(updatedProof, { from: payee }); 218 | assert.equal(balance.toNumber(), 18, "the balance is correct for the updated proof"); 219 | }); 220 | }); 221 | 222 | describe("withdraw", function() { 223 | let paymentPoolBalance; 224 | let paymentCycle; 225 | let proof; 226 | let payeeIndex = 0; 227 | let payee = payments[payeeIndex].payee; 228 | let paymentAmount = payments[payeeIndex].amount; 229 | let merkleTree = new CumulativePaymentTree(payments); 230 | let root = merkleTree.getHexRoot(); 231 | 232 | beforeEach(async function() { 233 | paymentPoolBalance = 100; 234 | await token.mint(paymentPool.address, paymentPoolBalance); 235 | paymentCycle = await paymentPool.numPaymentCycles(); 236 | paymentCycle = paymentCycle.toNumber(); 237 | proof = merkleTree.hexProofForPayee(payee, paymentCycle); 238 | await paymentPool.submitPayeeMerkleRoot(root); 239 | }); 240 | 241 | it("payee can withdraw up to their allotted amount from pool", async function() { 242 | let txn = await paymentPool.withdraw(paymentAmount, proof, { from: payee }); 243 | 244 | let withdrawEvent = txn.logs.find(log => log.event === 'PayeeWithdraw'); 245 | assert.equal(withdrawEvent.args.payee, payee, 'event payee is correct'); 246 | assert.equal(withdrawEvent.args.amount.toNumber(), paymentAmount, 'event amount is correct'); 247 | 248 | let payeeBalance = await token.balanceOf(payee); 249 | let poolBalance = await token.balanceOf(paymentPool.address); 250 | let withdrawals = await paymentPool.withdrawals(payee); 251 | let proofBalance = await paymentPool.balanceForProof(proof, { from: payee }); 252 | 253 | assert.equal(payeeBalance.toNumber(), paymentAmount, 'the payee balance is correct'); 254 | assert.equal(poolBalance.toNumber(), paymentPoolBalance - paymentAmount, 'the pool balance is correct'); 255 | assert.equal(withdrawals.toNumber(), paymentAmount, 'the withdrawals amount is correct'); 256 | assert.equal(proofBalance.toNumber(), 0, 'the proof balance is correct'); 257 | }); 258 | 259 | it("payee can make a withdrawal less than their allotted amount from the pool", async function() { 260 | let withdrawalAmount = 8; 261 | let txn = await paymentPool.withdraw(withdrawalAmount, proof, { from: payee }); 262 | 263 | let withdrawEvent = txn.logs.find(log => log.event === 'PayeeWithdraw'); 264 | assert.equal(withdrawEvent.args.payee, payee, 'event payee is correct'); 265 | assert.equal(withdrawEvent.args.amount.toNumber(), withdrawalAmount, 'event amount is correct'); 266 | 267 | let payeeBalance = await token.balanceOf(payee); 268 | let poolBalance = await token.balanceOf(paymentPool.address); 269 | let withdrawals = await paymentPool.withdrawals(payee); 270 | let proofBalance = await paymentPool.balanceForProof(proof, { from: payee }); 271 | 272 | assert.equal(payeeBalance.toNumber(), withdrawalAmount, 'the payee balance is correct'); 273 | assert.equal(poolBalance.toNumber(), paymentPoolBalance - withdrawalAmount, 'the pool balance is correct'); 274 | assert.equal(withdrawals.toNumber(), withdrawalAmount, 'the withdrawals amount is correct'); 275 | assert.equal(proofBalance.toNumber(), paymentAmount - withdrawalAmount, 'the proof balance is correct'); 276 | }); 277 | 278 | it("payee can make mulitple withdrawls within their allotted amount from the pool", async function() { 279 | let withdrawalAmount = 4 + 6; 280 | await paymentPool.withdraw(4, proof, { from: payee }); 281 | await paymentPool.withdraw(6, proof, { from: payee }); 282 | 283 | let payeeBalance = await token.balanceOf(payee); 284 | let poolBalance = await token.balanceOf(paymentPool.address); 285 | let withdrawals = await paymentPool.withdrawals(payee); 286 | let proofBalance = await paymentPool.balanceForProof(proof, { from: payee }); 287 | 288 | assert.equal(payeeBalance.toNumber(), withdrawalAmount, 'the payee balance is correct'); 289 | assert.equal(poolBalance.toNumber(), paymentPoolBalance - withdrawalAmount, 'the pool balance is correct'); 290 | assert.equal(withdrawals.toNumber(), withdrawalAmount, 'the withdrawals amount is correct'); 291 | assert.equal(proofBalance.toNumber(), paymentAmount - withdrawalAmount, 'the proof balance is correct'); 292 | }); 293 | 294 | it("payee cannot withdraw more than their allotted amount from the pool", async function() { 295 | let withdrawalAmount = 11; 296 | await assertRevert(async () => await paymentPool.withdraw(withdrawalAmount, proof, { from: payee })); 297 | 298 | let payeeBalance = await token.balanceOf(payee); 299 | let poolBalance = await token.balanceOf(paymentPool.address); 300 | let withdrawals = await paymentPool.withdrawals(payee); 301 | let proofBalance = await paymentPool.balanceForProof(proof, { from: payee }); 302 | 303 | assert.equal(payeeBalance.toNumber(), 0, 'the payee balance is correct'); 304 | assert.equal(poolBalance.toNumber(), paymentPoolBalance, 'the pool balance is correct'); 305 | assert.equal(withdrawals.toNumber(), 0, 'the withdrawals amount is correct'); 306 | assert.equal(proofBalance.toNumber(), paymentAmount, 'the proof balance is correct'); 307 | }); 308 | 309 | it("payee cannot withdraw using a proof whose metadata has been tampered with", async function() { 310 | let withdrawalAmount = 11; 311 | // the cumulative amount in in the proof's meta has been increased artifically to 12 tokens: note the "c" in the 127th position of the proof, here ---v 312 | let tamperedProof = "0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000c2e46ed0464b1e11097030a04086c9f068606b4c9808ccdac0343863c5e4f8244749e106fa8d91408f2578e5d93447f727f59279be85ce491faf212a7201d3b836b94214bff74426647e9cf0b5c5c3cbc9cef25b7e08759ca2b85357ec22c9b40"; 313 | 314 | await assertRevert(async () => await paymentPool.withdraw(withdrawalAmount, tamperedProof, { from: payee })); 315 | 316 | let payeeBalance = await token.balanceOf(payee); 317 | let poolBalance = await token.balanceOf(paymentPool.address); 318 | let withdrawals = await paymentPool.withdrawals(payee); 319 | let proofBalance = await paymentPool.balanceForProof(proof, { from: payee }); 320 | let tamperedProofBalance = await paymentPool.balanceForProof(tamperedProof, { from: payee }); 321 | 322 | assert.equal(payeeBalance.toNumber(), 0, 'the payee balance is correct'); 323 | assert.equal(poolBalance.toNumber(), paymentPoolBalance, 'the pool balance is correct'); 324 | assert.equal(withdrawals.toNumber(), 0, 'the withdrawals amount is correct'); 325 | assert.equal(proofBalance.toNumber(), paymentAmount, 'the proof balance is correct'); 326 | assert.equal(tamperedProofBalance.toNumber(), 0, 'the tampered proof balance is 0 tokens'); 327 | }); 328 | 329 | it("payee cannot make mulitple withdrawls that total to more than their allotted amount from the pool", async function() { 330 | let withdrawalAmount = 4; 331 | await paymentPool.withdraw(4, proof, { from: payee }); 332 | await assertRevert(async () => await paymentPool.withdraw(7, proof, { from: payee })); 333 | 334 | let payeeBalance = await token.balanceOf(payee); 335 | let poolBalance = await token.balanceOf(paymentPool.address); 336 | let withdrawals = await paymentPool.withdrawals(payee); 337 | let proofBalance = await paymentPool.balanceForProof(proof, { from: payee }); 338 | 339 | assert.equal(payeeBalance.toNumber(), withdrawalAmount, 'the payee balance is correct'); 340 | assert.equal(poolBalance.toNumber(), paymentPoolBalance - withdrawalAmount, 'the pool balance is correct'); 341 | assert.equal(withdrawals.toNumber(), withdrawalAmount, 'the withdrawals amount is correct'); 342 | assert.equal(proofBalance.toNumber(), paymentAmount - withdrawalAmount, 'the proof balance is correct'); 343 | }); 344 | 345 | it("payee cannot withdraw 0 tokens from payment pool", async function() { 346 | let withdrawalAmount = 0; 347 | await assertRevert(async () => await paymentPool.withdraw(withdrawalAmount, proof, { from: payee })); 348 | 349 | let payeeBalance = await token.balanceOf(payee); 350 | let poolBalance = await token.balanceOf(paymentPool.address); 351 | let withdrawals = await paymentPool.withdrawals(payee); 352 | let proofBalance = await paymentPool.balanceForProof(proof, { from: payee }); 353 | 354 | assert.equal(payeeBalance.toNumber(), 0, 'the payee balance is correct'); 355 | assert.equal(poolBalance.toNumber(), paymentPoolBalance, 'the pool balance is correct'); 356 | assert.equal(withdrawals.toNumber(), 0, 'the withdrawals amount is correct'); 357 | assert.equal(proofBalance.toNumber(), paymentAmount, 'the proof balance is correct'); 358 | }); 359 | 360 | it("non-payee cannot withdraw from pool", async function() { 361 | let withdrawalAmount = 10; 362 | await assertRevert(async () => await paymentPool.withdraw(withdrawalAmount, proof, { from: accounts[0] })); 363 | 364 | let payeeBalance = await token.balanceOf(payee); 365 | let poolBalance = await token.balanceOf(paymentPool.address); 366 | let withdrawals = await paymentPool.withdrawals(payee); 367 | let proofBalance = await paymentPool.balanceForProof(proof, { from: payee }); 368 | 369 | assert.equal(payeeBalance.toNumber(), 0, 'the payee balance is correct'); 370 | assert.equal(poolBalance.toNumber(), paymentPoolBalance, 'the pool balance is correct'); 371 | assert.equal(withdrawals.toNumber(), 0, 'the withdrawals amount is correct'); 372 | assert.equal(proofBalance.toNumber(), paymentAmount, 'the proof balance is correct'); 373 | }); 374 | 375 | it("payee cannot withdraw their allotted tokens from the pool when the pool does not have enough tokens", async function() { 376 | let insufficientFundsPayeeIndex = 7; 377 | let insufficientFundsPayee = payments[insufficientFundsPayeeIndex].payee; 378 | let insufficientFundsPaymentAmount = payments[insufficientFundsPayeeIndex].amount; 379 | let insufficientFundsProof = merkleTree.hexProofForPayee(insufficientFundsPayee, paymentCycle); 380 | 381 | await assertRevert(async () => await paymentPool.withdraw(insufficientFundsPaymentAmount, insufficientFundsProof, { from: insufficientFundsPayee })); 382 | 383 | let payeeBalance = await token.balanceOf(insufficientFundsPayee); 384 | let poolBalance = await token.balanceOf(paymentPool.address); 385 | let withdrawals = await paymentPool.withdrawals(insufficientFundsPayee); 386 | let proofBalance = await paymentPool.balanceForProof(insufficientFundsProof, { from: insufficientFundsPayee }); 387 | 388 | assert.equal(payeeBalance.toNumber(), 0, 'the payee balance is correct'); 389 | assert.equal(poolBalance.toNumber(), paymentPoolBalance, 'the pool balance is correct'); 390 | assert.equal(withdrawals.toNumber(), 0, 'the withdrawals amount is correct'); 391 | assert.equal(proofBalance.toNumber(), insufficientFundsPaymentAmount, 'the proof balance is correct'); 392 | }); 393 | 394 | it("payee withdraws their allotted amount from an older proof", async function() { 395 | let updatedPayments = payments.slice(); 396 | updatedPayments[payeeIndex].amount += 2; 397 | let updatedPaymentAmount = updatedPayments[payeeIndex].amount; 398 | let updatedMerkleTree = new CumulativePaymentTree(updatedPayments); 399 | let updatedRoot = updatedMerkleTree.getHexRoot(); 400 | 401 | await advanceBlock(web3) 402 | 403 | let paymentCycle = await paymentPool.numPaymentCycles(); 404 | paymentCycle = paymentCycle.toNumber(); 405 | let updatedProof = updatedMerkleTree.hexProofForPayee(payee, paymentCycle); 406 | await paymentPool.submitPayeeMerkleRoot(updatedRoot); 407 | 408 | let withdrawalAmount = 8; 409 | await paymentPool.withdraw(withdrawalAmount, proof, { from: payee }); 410 | 411 | let payeeBalance = await token.balanceOf(payee); 412 | let poolBalance = await token.balanceOf(paymentPool.address); 413 | let withdrawals = await paymentPool.withdrawals(payee); 414 | let proofBalance = await paymentPool.balanceForProof(proof, { from: payee }); 415 | let udpatedProofBalance = await paymentPool.balanceForProof(updatedProof, { from: payee }); 416 | 417 | assert.equal(payeeBalance.toNumber(), withdrawalAmount, 'the payee balance is correct'); 418 | assert.equal(poolBalance.toNumber(), paymentPoolBalance - withdrawalAmount, 'the pool balance is correct'); 419 | assert.equal(withdrawals.toNumber(), withdrawalAmount, 'the withdrawals amount is correct'); 420 | assert.equal(proofBalance.toNumber(), paymentAmount - withdrawalAmount, 'the proof balance is correct'); 421 | assert.equal(udpatedProofBalance.toNumber(), updatedPaymentAmount - withdrawalAmount, 'the updated proof balance is correct'); 422 | }); 423 | 424 | it("payee withdraws their allotted amount from a newer proof", async function() { 425 | let updatedPayments = payments.slice(); 426 | updatedPayments[payeeIndex].amount += 2; 427 | let updatedPaymentAmount = updatedPayments[payeeIndex].amount; 428 | let updatedMerkleTree = new CumulativePaymentTree(updatedPayments); 429 | let updatedRoot = updatedMerkleTree.getHexRoot(); 430 | 431 | await advanceBlock(web3) 432 | 433 | let paymentCycle = await paymentPool.numPaymentCycles(); 434 | paymentCycle = paymentCycle.toNumber(); 435 | let updatedProof = updatedMerkleTree.hexProofForPayee(payee, paymentCycle); 436 | await paymentPool.submitPayeeMerkleRoot(updatedRoot); 437 | 438 | let withdrawalAmount = 8; 439 | await paymentPool.withdraw(withdrawalAmount, updatedProof, { from: payee }); 440 | 441 | let payeeBalance = await token.balanceOf(payee); 442 | let poolBalance = await token.balanceOf(paymentPool.address); 443 | let withdrawals = await paymentPool.withdrawals(payee); 444 | let proofBalance = await paymentPool.balanceForProof(proof, { from: payee }); 445 | let udpatedProofBalance = await paymentPool.balanceForProof(updatedProof, { from: payee }); 446 | 447 | assert.equal(payeeBalance.toNumber(), withdrawalAmount, 'the payee balance is correct'); 448 | assert.equal(poolBalance.toNumber(), paymentPoolBalance - withdrawalAmount, 'the pool balance is correct'); 449 | assert.equal(withdrawals.toNumber(), withdrawalAmount, 'the withdrawals amount is correct'); 450 | assert.equal(proofBalance.toNumber(), paymentAmount - withdrawalAmount, 'the proof balance is correct'); 451 | assert.equal(udpatedProofBalance.toNumber(), updatedPaymentAmount - withdrawalAmount, 'the updated proof balance is correct'); 452 | }); 453 | 454 | it("payee withdraws their allotted amount from both an older and new proof", async function() { 455 | let updatedPayments = payments.slice(); 456 | updatedPayments[payeeIndex].amount += 2; 457 | let updatedPaymentAmount = updatedPayments[payeeIndex].amount; 458 | let updatedMerkleTree = new CumulativePaymentTree(updatedPayments); 459 | let updatedRoot = updatedMerkleTree.getHexRoot(); 460 | 461 | await advanceBlock(web3) 462 | 463 | let paymentCycle = await paymentPool.numPaymentCycles(); 464 | paymentCycle = paymentCycle.toNumber(); 465 | let updatedProof = updatedMerkleTree.hexProofForPayee(payee, paymentCycle); 466 | await paymentPool.submitPayeeMerkleRoot(updatedRoot); 467 | 468 | let withdrawalAmount = 8 + 4; 469 | await paymentPool.withdraw(8, proof, { from: payee }); 470 | await paymentPool.withdraw(4, updatedProof, { from: payee }); 471 | 472 | let payeeBalance = await token.balanceOf(payee); 473 | let poolBalance = await token.balanceOf(paymentPool.address); 474 | let withdrawals = await paymentPool.withdrawals(payee); 475 | let proofBalance = await paymentPool.balanceForProof(proof, { from: payee }); 476 | let udpatedProofBalance = await paymentPool.balanceForProof(updatedProof, { from: payee }); 477 | 478 | assert.equal(payeeBalance.toNumber(), withdrawalAmount, 'the payee balance is correct'); 479 | assert.equal(poolBalance.toNumber(), paymentPoolBalance - withdrawalAmount, 'the pool balance is correct'); 480 | assert.equal(withdrawals.toNumber(), withdrawalAmount, 'the withdrawals amount is correct'); 481 | assert.equal(proofBalance.toNumber(), 0, 'the proof balance is correct'); 482 | assert.equal(udpatedProofBalance.toNumber(), updatedPaymentAmount - withdrawalAmount, 'the updated proof balance is correct'); 483 | }); 484 | 485 | it("does not allow a payee to exceed their provided proof's allotted amount when withdrawing from an older proof and a newer proof", async function() { 486 | let updatedPayments = payments.slice(); 487 | updatedPayments[payeeIndex].amount += 2; 488 | let updatedPaymentAmount = updatedPayments[payeeIndex].amount; 489 | let updatedMerkleTree = new CumulativePaymentTree(updatedPayments); 490 | let updatedRoot = updatedMerkleTree.getHexRoot(); 491 | 492 | await advanceBlock(web3) 493 | 494 | let paymentCycle = await paymentPool.numPaymentCycles(); 495 | paymentCycle = paymentCycle.toNumber(); 496 | let updatedProof = updatedMerkleTree.hexProofForPayee(payee, paymentCycle); 497 | await paymentPool.submitPayeeMerkleRoot(updatedRoot); 498 | 499 | let withdrawalAmount = 8; 500 | await paymentPool.withdraw(8, updatedProof, { from: payee }); 501 | await assertRevert(async () => paymentPool.withdraw(4, proof, { from: payee })); // this proof only permits 10 - 8 tokens to be withdrawn, even though the newer proof permits 12 - 8 tokens to be withdrawn 502 | 503 | let payeeBalance = await token.balanceOf(payee); 504 | let poolBalance = await token.balanceOf(paymentPool.address); 505 | let withdrawals = await paymentPool.withdrawals(payee); 506 | let proofBalance = await paymentPool.balanceForProof(proof, { from: payee }); 507 | let udpatedProofBalance = await paymentPool.balanceForProof(updatedProof, { from: payee }); 508 | 509 | assert.equal(payeeBalance.toNumber(), withdrawalAmount, 'the payee balance is correct'); 510 | assert.equal(poolBalance.toNumber(), paymentPoolBalance - withdrawalAmount, 'the pool balance is correct'); 511 | assert.equal(withdrawals.toNumber(), withdrawalAmount, 'the withdrawals amount is correct'); 512 | assert.equal(proofBalance.toNumber(), paymentAmount - withdrawalAmount, 'the proof balance is correct'); 513 | assert.equal(udpatedProofBalance.toNumber(), updatedPaymentAmount - withdrawalAmount, 'the updated proof balance is correct'); 514 | }); 515 | }); 516 | 517 | 518 | describe("hash functions are accurate", function() { 519 | let node; 520 | beforeEach(function() { 521 | node = payments[0] 522 | }); 523 | it("checksum/non-checksum addresses output same hash", function (){ 524 | assert.equal(soliditySha3({t: 'address', v: node["payee"]}, {t: "uint256", v: node["amount"] }), "0xdc1a3188990e6f49560e7f513c95ce1ef99669f20d04bf16e2d1f3e76480d8ef" ) 525 | assert.equal(soliditySha3({t: 'address', v: toHex(node["payee"])}, {t: "uint256", v: node["amount"] }), "0xdc1a3188990e6f49560e7f513c95ce1ef99669f20d04bf16e2d1f3e76480d8ef" ) 526 | assert.equal(soliditySha3({t: 'address', v: node["payee"].replace("0x","")}, {t: "uint256", v: node["amount"] }), "0xdc1a3188990e6f49560e7f513c95ce1ef99669f20d04bf16e2d1f3e76480d8ef" ) 527 | assert.equal(soliditySha3({t: 'address', v: toHex(node["payee"]).replace("0x","")}, {t: "uint256", v: node["amount"] }), "0xdc1a3188990e6f49560e7f513c95ce1ef99669f20d04bf16e2d1f3e76480d8ef" ) 528 | }) 529 | }); 530 | }); 531 | }); 532 | 533 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | require('@babel/register'); 2 | require("core-js/stable"); 3 | require("regenerator-runtime/runtime"); 4 | 5 | module.exports = { 6 | // See 7 | // to customize your Truffle configuration! 8 | compilers: { 9 | solc: { 10 | version: "0.5.17", //default truffle (v5.3.6) solidity compiler 11 | }, 12 | }, 13 | }; 14 | --------------------------------------------------------------------------------