An implementation of the Aragon coding challenge.
A quick file guide: Payroll logic is found in contracts/Payroll.sol, the interface is in contracts/PayrollInterface.sol. I implemented a dummy EUR Token and an oracle for testing purposes. Tests are in test/payroll.js.
While implementing this Payroll interface, my intent was to write clear and functional code. To clarify any remaining points of confusion, I've documented several key design features below.
- Employees are eligible to receive their prorated salary allotment starting 1 month from when they are added to the Payroll contract, and 1 month after all subsequent
payday
calls. - Employees must choose a distribution over ERC20 tokens which they wish to receive their allotment in. The list of eligible ERC20 tokens is determined by the contract owner and can by viewed by calling
getWhitelist
. The contract only pays out ERC20 tokens from this whitelist. Once added to the whitelist, tokens cannot be removed, nor can their addresses be changed. EUR is the default token (i.e. it's always in the whitelist). - Employees may not elect to receive their salary in ether (I made this restriction for the sake of simplicity). In the event that non-whitelisted tokens are sent to the contract (making them unrecoverable in normal circumstances), the owner may call
drainToken
to rescue the token balances. If anyone tries to transfer a non-whitelisted but ERC223-compliant token to the contract,tokenFallback
ensures that the transfer will fail. - At first glance,
calculatePayrollRunway
seems like it might return the number of days before the contract will be unable to fulfill a possiblepayday
request. This is not the case - currently,calculatePayrollRunway
sums all whitelisted token balances in units of EUR without accounting for employee-elected distributions over tokens. So, while the number of days given bycalculatePayrollRunway
is accurate if the contract's balances are appropriately distributed across tokens, they are not a time frame during which allpayday
requests are guaranteed to succeed. Again, this decision was made to cut down on code complexity. - When sending tokens, I use
transfer
rather thanapprove
. This is because implementingcalculatePayrollRunway
would have been harder if I had to keep track of potentially withdrawable employee allowances. - The oracle's address is hard-coded address and immutable, like the tokens' (again, for simplicity). It is assumed that the Oracle updates all whitelisted token balances via
setExchangeRate
at acceptably fine-grained intervals, to ensure that EUR token exchange rates are kept current. In a real-world setting, one way to make this more practical/efficient would be for functions that require accurate exchange rates to fail if the time elapsed sincerateLastUpdated
for a given token was too high and emit an Event that would prompt the oracle to send the appropriate update(s). - A note on the
EURExchangeRate
s sent by the oracle: Imagine two tokens, EUR and USD. Say the USD/EUR exchange rate is 3/1 (i.e. 3 USD trade for 1 EUR), but that EUR has 2 decimals while USD has 4. This means that 1 EUR is represented as 100 in the EUR contract, and 1 USD is represented as 10,000 in the USD contract. My code currently assumes that the oracle'sEURExchangeRate
value for USD/EUR is 3*10,000/100 = 300. This is a fairly fragile assumption, because we could imagine that the decimals were flipped, in which case theEURExchangeRate
would be 3/100, which we would be forced to store as 0 or 1 since our rate variable is auint
. In general, the closer this exchange rate is to 1, the worse the Payroll performs, because of potential rounding issues in theuint
operations performed incalculatePayrollRunway
andpayday
. Similiar issues arise if yearly EUR salaries are small, e.g. on the order of 1e3. In a real-world setting, perhaps the easiest way around this fragility would be to arrange that a decimals-like precision multiplier (say, 1e18) would be used by the rate oracle or some of theuint
calculations, and adjust the contract logic accordingly. For the sake of this exercise though, I have elected not to do so.