├── app ├── images │ └── .gittouch ├── stylesheets │ └── app.css ├── javascripts │ └── app.js └── index.html ├── environments ├── test │ └── config.json ├── development │ └── config.json ├── production │ └── config.json └── staging │ └── config.json ├── .gitignore ├── contracts ├── Example.sol └── GroupIncome.sol ├── test ├── example.js └── group_income.es6 ├── README.md ├── truffle.json └── docs └── Design.md /app/images/.gittouch: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/stylesheets/app.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /environments/test/config.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /environments/development/config.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /environments/production/config.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /environments/staging/config.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | environments/*/build 2 | environments/*/contracts 3 | -------------------------------------------------------------------------------- /app/javascripts/app.js: -------------------------------------------------------------------------------- 1 | var welcome_string = "Hello from Truffle!"; 2 | console.log(welcome_string); 3 | -------------------------------------------------------------------------------- /contracts/Example.sol: -------------------------------------------------------------------------------- 1 | contract Example { 2 | function Example() { 3 | // constructor 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/example.js: -------------------------------------------------------------------------------- 1 | contract('Example', function(accounts) { 2 | it("should assert true", function(done) { 3 | var example = Example.at(Example.deployed_address); 4 | assert.isTrue(true); 5 | done(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Our current focus is on **Group Income (Simple Edition)** 2 | 3 | - :point_right: **https://github.com/okTurtles/group-income-simple** 4 | 5 | That's where all the work is being done right now. 6 | 7 | That work will either replace this project or be incorporated into it. 8 | -------------------------------------------------------------------------------- /truffle.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | // Copy ./app/index.html (right hand side) to ./build/index.html. 4 | "index.html": "index.html", 5 | "app.js": [ 6 | // Paths relative to "app" directory that should be 7 | // concatenated and processed during build. 8 | "javascripts/app.js" 9 | ], 10 | "app.css": [ 11 | // Paths relative to "app" directory that should be 12 | // concatenated and processed during build. 13 | "stylesheets/app.css" 14 | ], 15 | // Note: You can also include directories. 16 | // This will copy a static images directory to the build directory. 17 | "images/": "images/" 18 | }, 19 | "deploy": [ 20 | // Names of contracts that should be deployed to the network. 21 | "Example" 22 | ], 23 | "rpc": { 24 | // Default RPC configuration. 25 | "host": "localhost", 26 | "port": 8545 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |12 | build => Build development version of app; creates ./build directory 13 | compile => Compile contracts 14 | create:contract => Create a basic contract 15 | create:test => Create a basic test 16 | deploy => Deploy contracts to the network 17 | dist => Create distributable version of app (minified); creates ./dist directory 18 | init => Initialize new Ethereum project, including example contracts and tests 19 | init:config => Initialize default project configuration 20 | init:contracts => Initialize default contracts directory 21 | init:tests => Initialize tests directory structure and helpers 22 | list => List all available tasks 23 | test => Run tests 24 | watch => Watch project for changes and rebuild app automatically 25 |26 | 27 | 28 | -------------------------------------------------------------------------------- /test/group_income.es6: -------------------------------------------------------------------------------- 1 | contract('GroupIncome', function(accounts) { 2 | it("Initial groupIncome settings should match", async function() { 3 | var groupIncome = await GroupIncome.new(1000); 4 | var creator = await groupIncome.creator(); 5 | assert.equal(creator, accounts[0], "Creator should be msg.sender."); 6 | 7 | var threshold = await groupIncome.basicIncomeThreshold(); 8 | assert.equal(threshold, 1000, "basicIncomeThreshold should be what we set it as."); 9 | 10 | var member = await groupIncome.members(accounts[0]); 11 | var memberAddr = member[0]; 12 | var memberAmountReceived = member[2]; 13 | assert.equal(memberAddr, accounts[0], "Creator should be in members."); 14 | assert.equal(memberAmountReceived, 0, "You start with $0"); 15 | 16 | var memberAddress = await groupIncome.memberAddresses(0); 17 | assert.equal(memberAddress, accounts[0], "Creator should be in memberAddresses."); 18 | }); 19 | 20 | it("addMember should work", async function() { 21 | var groupIncome = await GroupIncome.new(1000); 22 | 23 | var addr = '0x82a978b3f5962a5b0957d9ee9eef472ee55b42f1'; 24 | var name = 'Tibet'; 25 | await groupIncome.addMember(addr, name); 26 | 27 | var memberAddress = await groupIncome.memberAddresses(0); 28 | assert.equal(memberAddress, addr, "memberAddresses should have given address."); 29 | 30 | var member = await groupIncome.members(addr); 31 | assert.equal(member[0], addr, "New user should be in members"); 32 | assert.equal(member[1], name, "New user should be in members."); 33 | assert.equal(member[2], 0, "You start with $0"); 34 | }); 35 | 36 | it("payMember should increase amountReceivedThisPeriod", async function() { 37 | var groupIncome = await GroupIncome.new(1000); 38 | await groupIncome.payMember(accounts[0], 50); 39 | var member = await groupIncome.members(accounts[0]); 40 | 41 | assert.equal(member[2], 50, "Should be paid 50"); 42 | }); 43 | 44 | it("income should distribute correctly", async function() { 45 | var groupIncome = await GroupIncome.new(1000); 46 | var memberAddrs = [ 47 | '0x82a978b3f5912a5b0957d9ee9eef472ee55b42f1', 48 | '0x82a978b3f5922a5b0957d9ee9eef472ee55b42f1', 49 | '0x82a978b3f5932a5b0957d9ee9eef472ee55b42f1' 50 | ]; 51 | for (var i = 0; i < memberAddrs.length; i++) { 52 | var addr = memberAddrs[i]; 53 | // Create members. Their names are addresses cuz whatever. 54 | await groupIncome.addMember(addr, addr); 55 | } 56 | 57 | await groupIncome.receiveIncome( { from: accounts[0], value: 3000 } ); 58 | 59 | var incomeGenerator = await groupIncome.members(accounts[0]); 60 | assert.equal(incomeGenerator[2], 1000, "Should fill iG's bucket"); 61 | 62 | for (var i = 0; i < memberAddrs.length; i++) { 63 | var addr = memberAddrs[i]; 64 | var member = await groupIncome.members(addr); 65 | assert.equal(member[2], 1000, "Should be paid 1000"); 66 | } 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /docs/Design.md: -------------------------------------------------------------------------------- 1 | # Design of Group Income 2 | 3 | ## Contribution Limits 4 | 5 | Members can set limits to the amount that they are willing to shoulder the burden (in addition to the fairness check between monetizers). 6 | 7 | **The contributor limit does not apply to members making less than the bucket amount.** 8 | 9 | How the contribution limit relates to bucket sizes and the fairness check still remains to be sketched out. 10 | 11 | TBD: If someone is making a lot and *could* help fill a lot of buckets but chooses not to, then perhaps *their own* bucket should be smaller in size (i.e. they're not helping the group much, so the group shouldn't be helping them much, especially since they don't need the help as much as some of the other members). 12 | 13 | ## Fairness Check Between Monetizers 14 | 15 | The Fairness Check is a necessary measure to ensure that Group Income works and that people are willing to participate in the system. It exists because unfairness would otherwise naturally be created by the system simply based on when people receive paychecks. In otherwords, without the fairness check, two monetizers that make equal amounts per month would be sharing the monetary burden of the group unequally if they received payment at different times. 16 | 17 | The algorithm automatically keeps track of sets of monetizers by tracking historical income. 18 | 19 | In a group of 5 members with 2 monetizers and $1k BI, the two monetizers are weighted based on how much they make. 20 | 21 | So if they make equal amounts then the fairness check makes sure that they are contributing to the group equally (for the moment, we're imagining a situation where contributions-limits are not enabled). In a 10-month year, two monetizers making $100k/year each, will need to fill 3 buckets (other than theirs) and split that amount between themselves. 22 | 23 | If A makes $200k/year and B $100k/year, then by default the fairness check makes it so that A pays a great proportion of the (3*12 = $36k) than B, since A is more capable. It's easier to see why this is fair if you compare two monetizers, one making $200k/year, and the other $20k/year. The $20k/year monetizer should not be expected to split the burden equally. 24 | 25 | In the analogy of the oxen drawn carriage, the $20k/year monetizer is a smaller oxe than the $200k/year, and should not be burdened with the "weight" that is expected of the larger oxen. It would be like expecting a small child to life 100 pounds (several times their weight), simply unreasonable. 26 | 27 | #### Monetizers Sets, Or What To Do When A Non-Monetizer Becomes A Monetizer 28 | 29 | The idea is that, on average, everyone is providing a bucket's worth of value to the group (probably more), regardless of whether or not they are able to monetize that value. In other words, a housekeeper or a stay-at-home mother is providing real value that "the market" simply does not choose to monetize. They are, in other words, "paying back" their bucket's worth of value every month, regardless of how much they actually receive in tokens from the market. 30 | 31 | But what happens when the market decides to send them tokens? In other words, what happens when the former non-monetizer starts to monetize their value? 32 | 33 | The fairness check only operates between monetizers at the time they are monetizers. The non-monetizer *was* providing their bucket's worth of value to the group (or society) and so does not share the burden that previous monetizers were sharing between themselves. However, once they enter the monetizing set, then from *that point on* the fairness check starts to operate between them and the other monetizers. 34 | 35 | ## "Windfall Problem" 36 | 37 | When a member receives a large amount all of a sudden (based on historical data), then the app can notify them and let them decide what to do, while showing the impact that their decision would have on the group. 38 | 39 | To-be-fleshed out in more detail. -------------------------------------------------------------------------------- /contracts/GroupIncome.sol: -------------------------------------------------------------------------------- 1 | contract GroupIncome { 2 | uint public basicIncomeThreshold; 3 | address public creator; 4 | address[] public memberAddresses; 5 | mapping (address => Member) public members; 6 | 7 | struct Member { 8 | address addr; 9 | string name; 10 | uint amountReceivedThisPeriod; 11 | } 12 | 13 | function GroupIncome(uint _basicIncomeThreshold) { 14 | basicIncomeThreshold = _basicIncomeThreshold; 15 | creator = msg.sender; 16 | addMember(creator, 'memberName'); 17 | } 18 | 19 | // function to add members 20 | function addMember(address memberAddress, string memberName) { 21 | Member memory member = Member(memberAddress, memberName, 0); 22 | 23 | // Keeping track of memberAddresses so that we can loop over members 24 | memberAddresses.push(member.addr); 25 | members[member.addr] = member; 26 | } 27 | 28 | // Money comes into the system through msg: 29 | // - msg.sender is `incomeGenerator`, the Member that is giving the money to the Group 30 | // - msg.value is `income`, the amount of money being given to the Group 31 | // 32 | // On receipt of money: 33 | // 1. Try to fill up the sender's bucket first. 34 | // If there is still money left, then: 35 | // 2. The software figures out how to best distribute the funds between all 36 | // Members that have not yet reached their Threshold. 37 | // - Find Member with the lowest amount and give them money until they have 38 | // as much as the second lowest amount. Now bring both of those members up 39 | // until they have as much as the third largest amount and so on. Of 40 | // course, at any time in this if you run out of money you stop. 41 | // 3. Give extra money back to sender. 42 | function receiveIncome() public returns (bool success) { 43 | Member memory incomeGenerator = members[msg.sender]; 44 | 45 | // First fill the incomeGenerator's bucket. 46 | uint income = fillBucket(msg.sender, msg.value); 47 | 48 | // Pay everyone else the leftover income / number of Members 49 | while (income > 1) { 50 | income = distributeIncome(income); 51 | } 52 | } 53 | 54 | 55 | function distributeIncome(uint income) returns (uint remainingIncome) { 56 | 57 | // Figure out who needs money and how much is the maximum we can give 58 | // everyone without putting anyone over their threshold. 59 | uint minimumOwed = basicIncomeThreshold; 60 | uint numberMembersToPay = 0; 61 | for (uint i = 0; i < memberAddresses.length; i++) { 62 | var member = members[memberAddresses[i]]; 63 | uint amountReceived = member.amountReceivedThisPeriod; 64 | uint amountOwed = basicIncomeThreshold - amountReceived; 65 | 66 | if (amountOwed > 0) { 67 | numberMembersToPay += 1; 68 | if (amountOwed < minimumOwed) { 69 | minimumOwed = amountOwed; 70 | } 71 | } 72 | } 73 | 74 | // Send extra money back to incomeGenerator 75 | if (minimumOwed == 0) { 76 | payMember(msg.sender, income); 77 | } 78 | 79 | // If we were to give every group member that needs money the same amount, 80 | // we'd give them each: 81 | uint amountToPay = income / numberMembersToPay; 82 | 83 | // If amountToPay would bring a member above the threshold, we'll instead 84 | // give everyone what that person needs and then do this process again. 85 | if (minimumOwed < amountToPay) { 86 | amountToPay = minimumOwed; 87 | } 88 | 89 | // Make it rain. 90 | for (i = 0; i < memberAddresses.length; i++) { 91 | var memberAddr = memberAddresses[i]; 92 | payMember(memberAddr, amountToPay); 93 | income -= amountToPay; 94 | } 95 | return income; 96 | } 97 | 98 | function fillBucket(address memberAddress, uint income) returns (uint leftOverIncome) { 99 | Member member = members[memberAddress]; 100 | uint amountNeeded = basicIncomeThreshold - member.amountReceivedThisPeriod; 101 | if (amountNeeded > 0) { 102 | uint leftOvers = income - amountNeeded; 103 | if (leftOvers > 0) { 104 | payMember(member.addr, amountNeeded); 105 | return leftOvers; 106 | } else { 107 | payMember(member.addr, income); 108 | return 0; 109 | } 110 | } 111 | } 112 | 113 | function payMember(address memberAddress, uint amount) { 114 | Member member = members[memberAddress]; 115 | member.amountReceivedThisPeriod += amount; 116 | } 117 | } 118 | --------------------------------------------------------------------------------