This repository has been archived by the owner on Jan 1, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Payroll.sol
286 lines (241 loc) · 9.58 KB
/
Payroll.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
pragma solidity ^0.4.18;
import "./zeppelin/ownership/Ownable.sol";
import "./zeppelin/math/SafeMath.sol";
import "./zeppelin/token/ERC20Basic.sol";
import "./PayrollInterface.sol";
contract Payroll is Ownable, PayrollInterface {
using SafeMath for uint256;
//// CONTRACT VARIABLES ////
// miscellaneous contract variables initialized in the constructor
address oracleAddress;
enum states { Locked, Unlocked }
states contractState;
// token logic
struct Token {
address tokenAddress;
uint256 EURExchangeRate;
uint256 rateLastUpdated;
}
mapping(address => Token) tokenRegister;
address[] tokenWhitelist;
// employee logic
struct Employee {
uint256 employeeId;
address employeeAddress;
address[] allowedTokens;
uint256[] tokenDistribution;
uint256 yearlyEURSalary;
uint256 lastPayday;
uint256 lastAllocation;
}
mapping(address => uint256) employeeIds;
mapping(uint256 => Employee) employeeRegister;
uint256 nextEmployeeId;
uint256 totalYearlyEURSalary;
uint256 employeeCount;
//// MODIFIERS ////
modifier onlyOracle() {
require(msg.sender == oracleAddress);
_;
}
modifier unlocked() {
require(contractState == states.Unlocked);
_;
}
modifier isEmployee(address challenge, bool desired) {
require((employeeIds[challenge] > 0) == desired);
_;
}
//// CONSTRUCTOR ////
function Payroll(address _oracleAddress, address _EURAddress) public {
// employee ids begin at 1 so that the isEmployee modifier works as written
nextEmployeeId = 1;
// hardcode the oracle address
oracleAddress = _oracleAddress;
// EUR Token Support by default
addTokenToWhiteList(_EURAddress);
// and we're off to the races
contractState = states.Unlocked;
}
//// FALLBACK ////
function () public { revert(); }
//// EXTERNAL - ORACLE ONLY ////
function setExchangeRate(address token, uint256 EURExchangeRate) external onlyOracle {
require(supportsToken(token));
Token storage _token = tokenRegister[token];
_token.EURExchangeRate = EURExchangeRate;
_token.rateLastUpdated = now;
}
//// PUBLIC - OWNER ONLY ////
function addEmployee(
address accountAddress,
address[] allowedTokens,
uint256[] tokenDistribution,
uint256 initialYearlyEURSalary
)
public
onlyOwner
isEmployee(accountAddress, false)
returns (uint256 employeeId)
{
employeeCount++;
totalYearlyEURSalary = totalYearlyEURSalary.add(initialYearlyEURSalary);
employeeId = nextEmployeeId++;
employeeIds[accountAddress] = employeeId;
employeeRegister[employeeId] = Employee(
employeeId,
accountAddress,
allowedTokens,
tokenDistribution,
initialYearlyEURSalary,
now,
0
);
_determineAllocation(employeeId, allowedTokens, tokenDistribution);
return employeeId;
}
function setEmployeeSalary(uint256 employeeId, uint256 yearlyEURSalary)
public
onlyOwner
isEmployee(employeeRegister[employeeId].employeeAddress, true)
{
Employee storage employee = employeeRegister[employeeId];
totalYearlyEURSalary =
totalYearlyEURSalary.sub(employee.yearlyEURSalary).add(yearlyEURSalary);
employee.yearlyEURSalary = yearlyEURSalary;
}
function removeEmployee(uint256 employeeId)
public
onlyOwner
isEmployee(employeeRegister[employeeId].employeeAddress, true)
{
// since we're deleteing this entry, it's clearer to work with a temporary copy in memory
Employee memory employee = employeeRegister[employeeId];
employeeCount--;
totalYearlyEURSalary = totalYearlyEURSalary.sub(employee.yearlyEURSalary);
delete employeeIds[employee.employeeAddress];
delete employeeRegister[employeeId];
}
function addTokenToWhiteList(address token) public onlyOwner {
require(!supportsToken(token));
tokenWhitelist.push(token);
tokenRegister[token] = Token(token, 0, 0);
}
function lock() public onlyOwner {
contractState = states.Locked;
// send all whitelisted tokens to the owner
for (uint256 i = 0; i < tokenWhitelist.length; i++) {
ERC20Basic token = ERC20Basic(tokenWhitelist[i]);
token.transfer(owner, token.balanceOf(this));
}
}
function unlock() public onlyOwner {
contractState = states.Unlocked;
}
//// PUBLIC - TOKEN INTERACTIONS ////
// deal with erc223-compliant tokens (reject if not in whitelist) - Dexaran implementation
function tokenFallback(address _from, uint, bytes) public {
require(supportsToken(_from));
}
// safety valve ensuring that no ERC-20 token can ever be "stuck" in the contract
function drainToken(address tokenAddress) public onlyOwner {
ERC20Basic token = ERC20Basic(tokenAddress);
token.transfer(owner, token.balanceOf(this));
}
// returns true if token is whitelisted
function supportsToken(address token) public view returns (bool) {
return(tokenRegister[token].tokenAddress != 0x0);
}
//// PUBLIC - USEFUL VIEW FUNCTIONS ////
function getEmployeeCount() public view returns (uint256) { return employeeCount; }
function getEmployee(uint256 employeeId)
public
view
returns (
address employeeAddress,
address[] allowedTokens,
uint256[] tokenDistribution,
uint256 yearlyEURSalary,
uint256 lastPayday,
uint256 lastAllocation
)
{
Employee storage employee = employeeRegister[employeeId];
return (
employee.employeeAddress,
employee.allowedTokens,
employee.tokenDistribution,
employee.yearlyEURSalary,
employee.lastPayday,
employee.lastAllocation
);
}
function getWhitelist() public view returns (address[] whitelist) { return tokenWhitelist; }
// projected total monthly token ouflows, denominated in EUR
function calculatePayrollBurnrate() public view returns (uint256) {
return totalYearlyEURSalary.div(12);
}
// days before the contract will be broke
// WARNING: calculations are based on EUR-converted balances...
// ...the contract could run out of any whitelisted token at any time
function calculatePayrollRunway() public view returns (uint256) {
uint256 EURBalance;
for (uint256 i = 0; i < tokenWhitelist.length; i++) {
Token storage token = tokenRegister[tokenWhitelist[i]];
uint256 tokenBalanceInEUR =
ERC20Basic(token.tokenAddress).balanceOf(this).div(token.EURExchangeRate);
EURBalance = EURBalance.add(tokenBalanceInEUR);
}
return EURBalance.div(totalYearlyEURSalary.div(365));
}
//// PUBLIC - EMPLOYEE ONLY ////
function determineAllocation(address[] tokens, uint256[] distribution)
public
unlocked
isEmployee(msg.sender, true)
{
_determineAllocation(employeeIds[msg.sender], tokens, distribution);
}
function payday() public unlocked isEmployee(msg.sender, true) {
Employee storage employee = employeeRegister[employeeIds[msg.sender]];
// ensure the employee hasn't called this function for at least 1 month
require(now > (employee.lastPayday + (1 years / 12)));
// calculate the amount of EUR the employee is owed based on seconds since last payday
uint256 EUROwed = (now - employee.lastPayday).mul(employee.yearlyEURSalary).div(1 years);
// for each token the employee accepts, send the appropriate share
for (uint256 i = 0; i < employee.allowedTokens.length; i++) {
Token storage token = tokenRegister[employee.allowedTokens[i]];
// compute the amount of the current token owed to the employee
uint256 tokenOwed = EUROwed.mul(employee.tokenDistribution[i]).div(100).mul(token.EURExchangeRate);
require(ERC20Basic(token.tokenAddress).transfer(msg.sender, tokenOwed));
}
employee.lastPayday = now;
}
//// INTERNAL ////
// interal allocation determination, allows an employee-only version and version in addEmployee
function _determineAllocation(uint256 employeeId, address[] tokens, uint256[] distribution)
internal
{
Employee storage employee = employeeRegister[employeeId];
// ensure the employee hasn't called this function for at least 6 months
require(now > (employee.lastAllocation + (1 years / 2)));
// ensure the distribution is valid
require(tokens.length == distribution.length);
checkDistribution(distribution);
for (uint256 i = 0; i < tokens.length; i++) {
require(supportsToken(tokens[i]));
}
// update the distribution and reset the cooldown
employee.allowedTokens = tokens;
employee.tokenDistribution = distribution;
employee.lastAllocation = now;
}
// ensures that the sum of the passed uint array is 100 i.e. a valid distribution
function checkDistribution(uint256[] distribution) internal pure {
uint256 distributionSum;
for (uint256 i = 0; i < distribution.length; i++) {
distributionSum = distributionSum.add(distribution[i]);
}
require(distributionSum == 100);
}
}