├── pethreon-logo.png ├── pethreon-slides.pptx ├── README.md ├── LICENSE └── pethreon.sol /pethreon-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-tikhomirov/pethreon/HEAD/pethreon-logo.png -------------------------------------------------------------------------------- /pethreon-slides.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-tikhomirov/pethreon/HEAD/pethreon-slides.pptx -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pethreon: recurring payments on Ethereum 2 | 3 | Support your favourite creator (an artist, a musician, a programmer) in a regular fasion. 4 | 5 | ![Logo](pethreon-logo.png) 6 | 7 | Compared to centralized alternatives, Pethreon: 8 | 9 | * **requires no trust**: a smart contract can not run away with the money 10 | * **respects privacy**: neither supporters nor creators have to reveal their personal information 11 | * **offers lower fees**: apart from Ethereum transaction cost, there are no fees 12 | 13 | This is how it works. 14 | 15 | A *supporter*: 16 | 17 | * deposits ether into a smart contract 18 | * makes a pledge to support a creator by regular payments 19 | * can cancel the pledge anytime and get the remaining funds back 20 | 21 | A *creator*: 22 | 23 | * publishes an Ethereum address and waits for supporters to pledge 24 | * can withdraw the available amount of ether anytime 25 | 26 | Created at the [Merkle week hackathon](http://www.merkleweek.com/hackathon) together with [@robcat](https://github.com/robcat) and [@stferr](https://github.com/stferr). 27 | Thanks to [Patreon](https://www.patreon.com/) for inspiration. 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sergei Tikhomirov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pethreon.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.4.19; 2 | 3 | /* 4 | An Ethereum version of recurring payments. 5 | 6 | Creator: 7 | 1. publishes address (via website, etc) 8 | 2. can withdraw a certain amount once every PERIOD 9 | 10 | Supporter: 11 | 1. Deposits ether 12 | 2. Pledges to give N wei to Creator once a PERIOD 13 | 3. Can unsubscribe any time (pledges for earlier periods not refunded) 14 | */ 15 | 16 | contract Pethreon { 17 | 18 | /***** EVENTS *****/ 19 | event SupporterDeposited(uint period, address supporter, uint amount); 20 | event PledgeCreated(uint period, address creator, address supporter, uint weiPerPeriod, uint periods); 21 | event PledgeCancelled(uint period, address creator, address supporter); 22 | event SupporterWithdrew(uint period, address supporter, uint amount); 23 | event CreatorWithdrew(uint period, address creator, uint amount); 24 | 25 | /***** CONSTANTS *****/ 26 | // Time is processed in steps of 1 PERIOD 27 | // Period 0 is the Start of epoch -- i.e., contract creation 28 | uint period; 29 | uint startOfEpoch; 30 | 31 | /***** DATA STRUCTURES *****/ 32 | struct Pledge { 33 | address creator; 34 | uint weiPerPeriod; 35 | uint afterLastPeriod; // first period s.t. pledge makes no payment 36 | bool initialized; 37 | } 38 | 39 | mapping (address => uint) supporterBalances; 40 | mapping (address => uint) creatorBalances; 41 | 42 | // supporter => (creator => pledge) 43 | mapping(address => mapping(address => Pledge)) pledges; 44 | 45 | // creator => (periodNumber => payment) 46 | mapping (address => mapping(uint => uint)) expectedPayments; 47 | mapping (address => uint) afterLastWithdrawalPeriod; 48 | 49 | 50 | /***** HELPER FUNCTIONS *****/ 51 | function Pethreon(uint _period) { 52 | startOfEpoch = now; 53 | period = _period; 54 | } 55 | 56 | function currentPeriod() 57 | internal 58 | view 59 | returns (uint periodNumber) { 60 | return (now - startOfEpoch) / period; 61 | } 62 | /* 63 | // TODO: get expected payments in batch (can't return uint[]?) 64 | function getExpectedPayment(uint period) constant returns (uint expectedPayment) { 65 | return (period < afterLastWithdrawalPeriod[msg.sender]) ? 0 : 66 | expectedPayments[msg.sender][period]; 67 | } 68 | */ 69 | /***** DEPOSIT & WITHDRAW *****/ 70 | 71 | // Get your (yet unpledged) balance as a supporter 72 | function balanceAsSupporter() 73 | public 74 | view 75 | returns (uint) { 76 | return supporterBalances[msg.sender]; 77 | } 78 | 79 | function balanceAsCreator() 80 | public 81 | view 82 | returns (uint) { 83 | // sum up all expected payments from all pledges from all previous periods 84 | uint256 amount = 0; 85 | for (var period = afterLastWithdrawalPeriod[msg.sender]; period < currentPeriod(); period++) { 86 | amount += expectedPayments[msg.sender][period]; 87 | } 88 | return amount; 89 | } 90 | 91 | // deposit ether to be used in future pledges 92 | function deposit() 93 | public 94 | payable 95 | returns (uint newBalance) { 96 | supporterBalances[msg.sender] += msg.value; 97 | SupporterDeposited(currentPeriod(), msg.sender, msg.value); 98 | return supporterBalances[msg.sender]; 99 | } 100 | 101 | // withdraw ether (generic function) 102 | function withdraw(bool isSupporter, uint amount) 103 | internal 104 | returns (uint newBalance) { 105 | var balances = isSupporter ? supporterBalances : creatorBalances; 106 | uint oldBalance = balances[msg.sender]; 107 | if (balances[msg.sender] < amount) return oldBalance; 108 | balances[msg.sender] -= amount; 109 | if (!msg.sender.send(amount)) { 110 | balances[msg.sender] += amount; 111 | return oldBalance; 112 | } 113 | return balances[msg.sender]; 114 | } 115 | 116 | // Supporter can choose how much to withdraw 117 | function withdrawAsSupporter(uint amount) 118 | public { 119 | withdraw(true, amount); 120 | SupporterWithdrew(currentPeriod(), msg.sender, amount); 121 | } 122 | 123 | // Creator can only withdraw the full amount available (keeping it simple!) 124 | function withdrawAsCreator() 125 | public { 126 | var amount = balanceAsCreator(); 127 | afterLastWithdrawalPeriod[msg.sender] = currentPeriod(); 128 | withdraw(false, amount); 129 | CreatorWithdrew(currentPeriod(), msg.sender, amount); 130 | } 131 | 132 | 133 | /***** PLEDGES *****/ 134 | 135 | function canPledge(uint _weiPerPeriod, uint _periods) 136 | internal 137 | view 138 | returns (bool enoughFunds) { 139 | return (supporterBalances[msg.sender] >= _weiPerPeriod * _periods); 140 | } 141 | 142 | function createPledge(address _creator, uint _weiPerPeriod, uint _periods) 143 | public { 144 | 145 | // must have enough funds 146 | require(canPledge(_weiPerPeriod, _periods)); 147 | 148 | // can't pledge twice for same creator (for simplicity) 149 | // to change pledge parameters, cancel it and create a new one 150 | require(!pledges[msg.sender][_creator].initialized); 151 | 152 | // update creator's mapping of future payments 153 | for (uint period = currentPeriod(); period < _periods; period++) { 154 | expectedPayments[_creator][period] += _weiPerPeriod; 155 | } 156 | 157 | // store the data structure so that supporter can cancel pledge 158 | var pledge = Pledge({ 159 | creator: _creator, 160 | weiPerPeriod: _weiPerPeriod, 161 | afterLastPeriod: currentPeriod() + _periods, 162 | initialized: true 163 | }); 164 | 165 | pledges[msg.sender][_creator] = pledge; 166 | supporterBalances[msg.sender] -= _weiPerPeriod * _periods; 167 | PledgeCreated(currentPeriod(), _creator, msg.sender, _weiPerPeriod, _periods); 168 | } 169 | 170 | function cancelPledge(address _creator) 171 | public { 172 | var pledge = pledges[msg.sender][_creator]; 173 | require(pledge.initialized); 174 | supporterBalances[msg.sender] += pledge.weiPerPeriod * (pledge.afterLastPeriod - currentPeriod()); 175 | for (uint period = currentPeriod(); period < pledge.afterLastPeriod; period++) { 176 | expectedPayments[_creator][period] -= pledge.weiPerPeriod; 177 | } 178 | delete pledges[msg.sender][_creator]; 179 | PledgeCancelled(currentPeriod(), _creator, msg.sender); 180 | } 181 | 182 | function myPledgeTo(address _creator) 183 | public 184 | view 185 | returns (uint weiPerPeriod, uint afterLastPeriod) { 186 | var pledge = pledges[msg.sender][_creator]; 187 | return (pledge.weiPerPeriod, pledge.afterLastPeriod); 188 | } 189 | 190 | } 191 | 192 | --------------------------------------------------------------------------------