From 327abd641211325a25e6f7748149371b2c763d54 Mon Sep 17 00:00:00 2001 From: Facu Spagnuolo Date: Sat, 27 Apr 2019 02:55:04 -0300 Subject: [PATCH] Payroll: clarify how rates are handled and add unit tests (#823) --- future-apps/payroll/contracts/Payroll.sol | 24 +- .../contracts/Payroll_add_employee.test.js | 27 +- .../contracts/Payroll_allowed_tokens.test.js | 61 ++- .../test/contracts/Payroll_bonuses.test.js | 147 +++--- .../test/contracts/Payroll_forwarding.test.js | 34 +- .../test/contracts/Payroll_gas_costs.test.js | 65 +-- .../contracts/Payroll_get_employee.test.js | 44 +- .../test/contracts/Payroll_initialize.test.js | 28 +- .../contracts/Payroll_modify_employee.test.js | 52 +- .../test/contracts/Payroll_payday.test.js | 291 ++++++----- .../test/contracts/Payroll_rates.test.js | 468 ++++++++++++++++++ .../test/contracts/Payroll_reentrancy.test.js | 12 +- .../contracts/Payroll_reimbursements.test.js | 165 +++--- .../test/contracts/Payroll_settings.test.js | 21 +- .../Payroll_terminate_employee.test.js | 52 +- .../Payroll_token_allocations.test.js | 52 +- future-apps/payroll/test/helpers/deploy.js | 3 +- future-apps/payroll/test/helpers/numbers.js | 17 +- .../payroll/test/helpers/set_token_rates.js | 23 - future-apps/payroll/test/helpers/time.js | 18 + future-apps/payroll/test/helpers/tokens.js | 70 +++ 21 files changed, 1070 insertions(+), 604 deletions(-) create mode 100644 future-apps/payroll/test/contracts/Payroll_rates.test.js delete mode 100644 future-apps/payroll/test/helpers/set_token_rates.js create mode 100644 future-apps/payroll/test/helpers/time.js create mode 100644 future-apps/payroll/test/helpers/tokens.js diff --git a/future-apps/payroll/contracts/Payroll.sol b/future-apps/payroll/contracts/Payroll.sol index bd20d595cd..20f202e7ee 100644 --- a/future-apps/payroll/contracts/Payroll.sol +++ b/future-apps/payroll/contracts/Payroll.sol @@ -655,17 +655,23 @@ contract Payroll is EtherTokenConstant, IForwarder, IsContract, AragonApp { } /** - * @dev Get token exchange rate for a token based on the denomination token - * @param _token Token + * @dev Get token exchange rate for a token based on the denomination token. + * If the denomination token was USD and ETH's price was 100USD, + * this would return 0.01 for ETH. + * @param _token Token to get price of in denomination tokens * @return ONE if _token is denominationToken or 0 if the exchange rate isn't recent enough */ - function _getExchangeRate(address _token) internal view returns (uint128) { + function _getExchangeRateInDenominationToken(address _token) internal view returns (uint128) { // Denomination token has always exchange rate of 1 if (_token == denominationToken) { return ONE; } - (uint128 xrt, uint64 when) = feed.get(_token, denominationToken); + // xrt is the number of `_token` that can be exchanged for one `denominationToken` + (uint128 xrt, uint64 when) = feed.get( + denominationToken, // Base (e.g. USD) + _token // Quote (e.g. ETH) + ); // Check the price feed is recent enough if (getTimestamp64().sub(when) >= rateExpiryTime) { @@ -692,11 +698,15 @@ contract Payroll is EtherTokenConstant, IForwarder, IsContract, AragonApp { address token = allowedTokensArray[i]; uint256 tokenAllocation = employee.allocation[token]; if (tokenAllocation != uint256(0)) { - uint128 exchangeRate = _getExchangeRate(token); + // Get the exchange rate for the token in denomination token, + // as we do accounting in denomination tokens + uint128 exchangeRate = _getExchangeRateInDenominationToken(token); require(exchangeRate > 0, ERROR_EXCHANGE_RATE_ZERO); - // Salary converted to token and applied allocation percentage + + // Salary (in denomination tokens) converted to payout token + // and applied allocation percentage uint256 tokenAmount = _totalAmount.mul(exchangeRate).mul(tokenAllocation); - // Divide by 100 for the allocation and by ONE for the exchange rate + // Divide by 100 for the allocation and by ONE for the exchange rate precision tokenAmount = tokenAmount / (100 * ONE); finance.newImmediatePayment(token, employeeAddress, tokenAmount, paymentReference); diff --git a/future-apps/payroll/test/contracts/Payroll_add_employee.test.js b/future-apps/payroll/test/contracts/Payroll_add_employee.test.js index f0f4460723..de8801bcdd 100644 --- a/future-apps/payroll/test/contracts/Payroll_add_employee.test.js +++ b/future-apps/payroll/test/contracts/Payroll_add_employee.test.js @@ -1,23 +1,18 @@ const { assertRevert } = require('@aragon/test-helpers/assertThrow') const { getEvents, getEventArgument } = require('../helpers/events') -const { maxUint64, annualSalaryPerSecond } = require('../helpers/numbers')(web3) -const { deployErc20TokenAndDeposit, deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy.js')(artifacts, web3) +const { USD, deployDAI } = require('../helpers/tokens')(artifacts, web3) +const { NOW, TWO_MONTHS, RATE_EXPIRATION_TIME } = require('../helpers/time') +const { MAX_UINT64, annualSalaryPerSecond } = require('../helpers/numbers')(web3) +const { deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy')(artifacts, web3) const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' contract('Payroll employees addition', ([owner, employee, anotherEmployee, anyone]) => { - let dao, payroll, payrollBase, finance, vault, priceFeed, denominationToken - - const NOW = 1553703809 // random fixed timestamp in seconds - const ONE_MONTH = 60 * 60 * 24 * 31 - const TWO_MONTHS = ONE_MONTH * 2 - const RATE_EXPIRATION_TIME = TWO_MONTHS - - const TOKEN_DECIMALS = 18 + let dao, payroll, payrollBase, finance, vault, priceFeed, DAI before('deploy base apps and tokens', async () => { ({ dao, finance, vault, payrollBase } = await deployContracts(owner)) - denominationToken = await deployErc20TokenAndDeposit(owner, finance, 'Denomination Token', TOKEN_DECIMALS) + DAI = await deployDAI(owner, finance) }) beforeEach('create payroll and price feed instance', async () => { @@ -26,11 +21,11 @@ contract('Payroll employees addition', ([owner, employee, anotherEmployee, anyon describe('addEmployee', () => { const role = 'Boss' - const salary = annualSalaryPerSecond(100000, TOKEN_DECIMALS) + const salary = annualSalaryPerSecond(100000) context('when it has already been initialized', function () { - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) context('when the sender has permissions to add employees', () => { @@ -69,7 +64,7 @@ contract('Payroll employees addition', ([owner, employee, anotherEmployee, anyon it('can add another employee', async () => { const anotherRole = 'Manager' - const anotherSalary = annualSalaryPerSecond(120000, TOKEN_DECIMALS) + const anotherSalary = annualSalaryPerSecond(120000) const receipt = await payroll.addEmployee(anotherEmployee, anotherSalary, anotherRole, startDate) const anotherEmployeeId = getEventArgument(receipt, 'AddEmployee', 'employeeId') @@ -91,7 +86,7 @@ contract('Payroll employees addition', ([owner, employee, anotherEmployee, anyon assert.equal(accruedSalary, 0, 'employee accrued salary does not match') assert.equal(employeeSalary.toString(), anotherSalary.toString(), 'employee salary does not match') assert.equal(lastPayroll.toString(), startDate.toString(), 'employee last payroll does not match') - assert.equal(endDate.toString(), maxUint64(), 'employee end date does not match') + assert.equal(endDate.toString(), MAX_UINT64, 'employee end date does not match') }) }) diff --git a/future-apps/payroll/test/contracts/Payroll_allowed_tokens.test.js b/future-apps/payroll/test/contracts/Payroll_allowed_tokens.test.js index 17c5f412cb..d18a1f5d6a 100644 --- a/future-apps/payroll/test/contracts/Payroll_allowed_tokens.test.js +++ b/future-apps/payroll/test/contracts/Payroll_allowed_tokens.test.js @@ -1,25 +1,20 @@ const PAYMENT_TYPES = require('../helpers/payment_types') -const setTokenRates = require('../helpers/set_token_rates')(web3) const { getEvent } = require('../helpers/events') const { assertRevert } = require('@aragon/test-helpers/assertThrow') -const { deployErc20TokenAndDeposit, deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy.js')(artifacts, web3) +const { annualSalaryPerSecond } = require('../helpers/numbers')(web3) +const { NOW, ONE_MONTH, RATE_EXPIRATION_TIME } = require('../helpers/time') +const { deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy')(artifacts, web3) +const { USD, deployDAI, deployTokenAndDeposit, setTokenRates, formatRate } = require('../helpers/tokens')(artifacts, web3) const MAX_GAS_USED = 6.5e6 const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' contract('Payroll allowed tokens,', ([owner, employee, anyone]) => { - let dao, payroll, payrollBase, finance, vault, priceFeed, denominationToken - - const NOW = 1553703809 // random fixed timestamp in seconds - const ONE_MONTH = 60 * 60 * 24 * 31 - const TWO_MONTHS = ONE_MONTH * 2 - const RATE_EXPIRATION_TIME = TWO_MONTHS - - const TOKEN_DECIMALS = 18 + let dao, payroll, payrollBase, finance, vault, priceFeed, DAI before('deploy base apps and tokens', async () => { ({ dao, finance, vault, payrollBase } = await deployContracts(owner)) - denominationToken = await deployErc20TokenAndDeposit(owner, finance, 'Denomination Token', TOKEN_DECIMALS) + DAI = await deployDAI(owner, finance) }) beforeEach('create payroll and price feed instance', async () => { @@ -28,8 +23,8 @@ contract('Payroll allowed tokens,', ([owner, employee, anyone]) => { describe('addAllowedToken', () => { context('when it has already been initialized', function () { - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) context('when the sender has permissions', () => { @@ -37,13 +32,13 @@ contract('Payroll allowed tokens,', ([owner, employee, anyone]) => { context('when it does not reach the maximum amount allowed', () => { it('can allow a token', async () => { - const receipt = await payroll.addAllowedToken(denominationToken.address, { from }) + const receipt = await payroll.addAllowedToken(DAI.address, { from }) const event = getEvent(receipt, 'AddAllowedToken') - assert.equal(event.token, denominationToken.address, 'denomination token address should match') + assert.equal(event.token, DAI.address, 'denomination token address should match') assert.equal(await payroll.getAllowedTokensArrayLength(), 1, 'allowed tokens length does not match') - assert(await payroll.isTokenAllowed(denominationToken.address), 'denomination token should be allowed') + assert(await payroll.isTokenAllowed(DAI.address), 'denomination token should be allowed') }) it('can allow a the zero address', async () => { @@ -57,15 +52,15 @@ contract('Payroll allowed tokens,', ([owner, employee, anyone]) => { }) it('can allow multiple tokens', async () => { - const erc20Token1 = await deployErc20TokenAndDeposit(owner, finance, 'Token 1', 18) - const erc20Token2 = await deployErc20TokenAndDeposit(owner, finance, 'Token 2', 16) + const erc20Token1 = await deployTokenAndDeposit(owner, finance, 'Token 1', 18) + const erc20Token2 = await deployTokenAndDeposit(owner, finance, 'Token 2', 16) - await payroll.addAllowedToken(denominationToken.address, { from }) + await payroll.addAllowedToken(DAI.address, { from }) await payroll.addAllowedToken(erc20Token1.address, { from }) await payroll.addAllowedToken(erc20Token2.address, { from }) assert.equal(await payroll.getAllowedTokensArrayLength(), 3, 'allowed tokens length does not match') - assert(await payroll.isTokenAllowed(denominationToken.address), 'denomination token should be allowed') + assert(await payroll.isTokenAllowed(DAI.address), 'denomination token should be allowed') assert(await payroll.isTokenAllowed(erc20Token1.address), 'ERC20 token 1 should be allowed') assert(await payroll.isTokenAllowed(erc20Token2.address), 'ERC20 token 2 should be allowed') }) @@ -77,7 +72,7 @@ contract('Payroll allowed tokens,', ([owner, employee, anyone]) => { before('deploy multiple tokens and set rates', async () => { MAX_ALLOWED_TOKENS = (await payrollBase.getMaxAllowedTokens()).valueOf() for (let i = 0; i < MAX_ALLOWED_TOKENS; i++) { - const token = await deployErc20TokenAndDeposit(owner, finance, `Token ${i}`, 18); + const token = await deployTokenAndDeposit(owner, finance, `Token ${i}`, 18); tokenAddresses.push(token.address) } }) @@ -86,14 +81,14 @@ contract('Payroll allowed tokens,', ([owner, employee, anyone]) => { await Promise.all(tokenAddresses.map(address => payroll.addAllowedToken(address, { from: owner }))) assert.equal(await payroll.getAllowedTokensArrayLength(), MAX_ALLOWED_TOKENS, 'amount of allowed tokens does not match') - const rates = tokenAddresses.map(() => 5) - await setTokenRates(priceFeed, denominationToken, tokenAddresses, rates) + const rates = tokenAddresses.map(() => formatRate(5)) + await setTokenRates(priceFeed, USD, tokenAddresses, rates) - await payroll.addEmployee(employee, 100000, 'Boss', NOW - ONE_MONTH, { from: owner }) + await payroll.addEmployee(employee, annualSalaryPerSecond(100000), 'Boss', NOW - ONE_MONTH, { from: owner }) }) it('can not add one more token', async () => { - const erc20Token = await deployErc20TokenAndDeposit(owner, finance, 'Extra token', 18) + const erc20Token = await deployTokenAndDeposit(owner, finance, 'Extra token', 18) await assertRevert(payroll.addAllowedToken(erc20Token.address), 'PAYROLL_MAX_ALLOWED_TOKENS') }) @@ -114,38 +109,38 @@ contract('Payroll allowed tokens,', ([owner, employee, anyone]) => { const from = anyone it('reverts', async () => { - await assertRevert(payroll.addAllowedToken(denominationToken.address, { from }), 'APP_AUTH_FAILED') + await assertRevert(payroll.addAllowedToken(DAI.address, { from }), 'APP_AUTH_FAILED') }) }) }) context('when it has not been initialized yet', function () { it('reverts', async () => { - await assertRevert(payroll.addAllowedToken(denominationToken.address, { from: owner }), 'APP_AUTH_FAILED') + await assertRevert(payroll.addAllowedToken(DAI.address, { from: owner }), 'APP_AUTH_FAILED') }) }) }) describe('isTokenAllowed', () => { context('when it has already been initialized', function () { - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) context('when the given token is not the zero address', () => { context('when the requested token was allowed', () => { beforeEach('allow denomination token', async () => { - await payroll.addAllowedToken(denominationToken.address, { from: owner }) + await payroll.addAllowedToken(DAI.address, { from: owner }) }) it('returns true', async () => { - assert(await payroll.isTokenAllowed(denominationToken.address), 'token should be allowed') + assert(await payroll.isTokenAllowed(DAI.address), 'token should be allowed') }) }) context('when the requested token was not allowed yet', () => { it('returns false', async () => { - assert.isFalse(await payroll.isTokenAllowed(denominationToken.address), 'token should not be allowed') + assert.isFalse(await payroll.isTokenAllowed(DAI.address), 'token should not be allowed') }) }) }) @@ -159,7 +154,7 @@ contract('Payroll allowed tokens,', ([owner, employee, anyone]) => { context('when it has not been initialized yet', function () { it('reverts', async () => { - await assertRevert(payroll.isTokenAllowed(denominationToken.address), 'INIT_NOT_INITIALIZED') + await assertRevert(payroll.isTokenAllowed(DAI.address), 'INIT_NOT_INITIALIZED') }) }) }) diff --git a/future-apps/payroll/test/contracts/Payroll_bonuses.test.js b/future-apps/payroll/test/contracts/Payroll_bonuses.test.js index c2a42728a8..fc40dc1ee5 100644 --- a/future-apps/payroll/test/contracts/Payroll_bonuses.test.js +++ b/future-apps/payroll/test/contracts/Payroll_bonuses.test.js @@ -1,19 +1,13 @@ const PAYMENT_TYPES = require('../helpers/payment_types') -const setTokenRates = require('../helpers/set_token_rates')(web3) const { assertRevert } = require('@aragon/test-helpers/assertThrow') -const { bn, maxUint256 } = require('../helpers/numbers')(web3) const { getEvents, getEventArgument } = require('../helpers/events') -const { deployErc20TokenAndDeposit, deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy.js')(artifacts, web3) +const { NOW, ONE_MONTH, RATE_EXPIRATION_TIME } = require('../helpers/time') +const { bn, bigExp, annualSalaryPerSecond, ONE, MAX_UINT256 } = require('../helpers/numbers')(web3) +const { deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy')(artifacts, web3) +const { USD, DAI_RATE, ANT_RATE, exchangedAmount, deployDAI, deployANT, setTokenRates } = require('../helpers/tokens')(artifacts, web3) contract('Payroll bonuses', ([owner, employee, anyone]) => { - let dao, payroll, payrollBase, finance, vault, priceFeed, denominationToken, anotherToken, anotherTokenRate - - const NOW = 1553703809 // random fixed timestamp in seconds - const ONE_MONTH = 60 * 60 * 24 * 31 - const TWO_MONTHS = ONE_MONTH * 2 - const RATE_EXPIRATION_TIME = TWO_MONTHS - - const TOKEN_DECIMALS = 18 + let dao, payroll, payrollBase, finance, vault, priceFeed, DAI, ANT const increaseTime = async seconds => { await payroll.mockIncreaseTime(seconds) @@ -22,8 +16,8 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { before('deploy base apps and tokens', async () => { ({ dao, finance, vault, payrollBase } = await deployContracts(owner)) - anotherToken = await deployErc20TokenAndDeposit(owner, finance, 'Another token', TOKEN_DECIMALS) - denominationToken = await deployErc20TokenAndDeposit(owner, finance, 'Denomination Token', TOKEN_DECIMALS) + ANT = await deployANT(owner, finance) + DAI = await deployDAI(owner, finance) }) beforeEach('create payroll and price feed instance', async () => { @@ -32,8 +26,8 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { describe('addBonus', () => { context('when it has already been initialized', function () { - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) context('when the sender has permissions', () => { @@ -43,7 +37,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { let employeeId beforeEach('add employee', async () => { - const receipt = await payroll.addEmployee(employee, 1000, 'Boss', await payroll.getTimestampPublic()) + const receipt = await payroll.addEmployee(employee, annualSalaryPerSecond(100000), 'Boss', await payroll.getTimestampPublic()) employeeId = getEventArgument(receipt, 'AddEmployee', 'employeeId') }) @@ -69,7 +63,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { } context('when the given bonus greater than zero', () => { - const amount = 1000 + const amount = bigExp(1000, 18) context('when there was no previous bonus', () => { itAddsBonusesSuccessfully(amount) @@ -85,13 +79,13 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { }) context('when the given bonus is zero', () => { - const amount = 0 + const amount = bn(0) itAddsBonusesSuccessfully(amount) }) context('when the given bonus way greater than zero', () => { - const amount = maxUint256() + const amount = MAX_UINT256 it('reverts', async () => { await payroll.addBonus(employeeId, 1, { from }) @@ -108,7 +102,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { }) it('reverts', async () => { - await assertRevert(payroll.addBonus(employeeId, 1000, { from }), 'PAYROLL_NON_ACTIVE_EMPLOYEE') + await assertRevert(payroll.addBonus(employeeId, bigExp(1000, 18), { from }), 'PAYROLL_NON_ACTIVE_EMPLOYEE') }) }) }) @@ -117,14 +111,14 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { const employeeId = 0 it('reverts', async () => { - await assertRevert(payroll.addBonus(employeeId, 1000, { from }), 'PAYROLL_NON_ACTIVE_EMPLOYEE') + await assertRevert(payroll.addBonus(employeeId, bigExp(1000, 18), { from }), 'PAYROLL_NON_ACTIVE_EMPLOYEE') }) }) }) context('when the sender does not have permissions', () => { const from = anyone - const amount = 1000 + const amount = bigExp(1000, 18) const employeeId = 0 it('reverts', async () => { @@ -134,7 +128,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { }) context('when it has not been initialized yet', function () { - const amount = 10000 + const amount = bigExp(1000, 18) const employeeId = 0 it('reverts', async () => { @@ -145,18 +139,17 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { describe('bonus payday', () => { context('when it has already been initialized', function () { - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) beforeEach('set token rates', async () => { - anotherTokenRate = bn(5) - await setTokenRates(priceFeed, denominationToken, [anotherToken], [anotherTokenRate]) + await setTokenRates(priceFeed, USD, [DAI, ANT], [DAI_RATE, ANT_RATE]) }) context('when the sender is an employee', () => { const from = employee - let employeeId, salary = 1000 + let employeeId, salary = annualSalaryPerSecond(100000) beforeEach('add employee and accumulate some salary', async () => { const receipt = await payroll.addEmployee(employee, salary, 'Boss', await payroll.getTimestampPublic()) @@ -166,40 +159,40 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { }) context('when the employee has already set some token allocations', () => { - const denominationTokenAllocation = 80 - const anotherTokenAllocation = 20 + const allocationDAI = 80 + const allocationANT = 20 beforeEach('set tokens allocation', async () => { - await payroll.addAllowedToken(anotherToken.address, { from: owner }) - await payroll.addAllowedToken(denominationToken.address, { from: owner }) - await payroll.determineAllocation([denominationToken.address, anotherToken.address], [denominationTokenAllocation, anotherTokenAllocation], { from }) + await payroll.addAllowedToken(ANT.address, { from: owner }) + await payroll.addAllowedToken(DAI.address, { from: owner }) + await payroll.determineAllocation([DAI.address, ANT.address], [allocationDAI, allocationANT], { from }) }) context('when the employee has a pending bonus', () => { - const bonusAmount = 100 + const bonusAmount = bigExp(100, 18) beforeEach('add bonus', async () => { - await payroll.addBonus(employeeId, bonusAmount / 2, { from: owner }) - await payroll.addBonus(employeeId, bonusAmount / 2, { from: owner }) + await payroll.addBonus(employeeId, bonusAmount.div(2), { from: owner }) + await payroll.addBonus(employeeId, bonusAmount.div(2), { from: owner }) }) const assertTransferredAmounts = (requestedAmount, expectedRequestedAmount = requestedAmount) => { - const requestedDenominationTokenAmount = parseInt(expectedRequestedAmount * denominationTokenAllocation / 100) - const requestedAnotherTokenAmount = expectedRequestedAmount * anotherTokenAllocation / 100 + const requestedDAI = exchangedAmount(expectedRequestedAmount, DAI_RATE, allocationDAI) + const requestedANT = exchangedAmount(expectedRequestedAmount, ANT_RATE, allocationANT) it('transfers all the pending bonus', async () => { - const previousDenominationTokenBalance = await denominationToken.balanceOf(employee) - const previousAnotherTokenBalance = await anotherToken.balanceOf(employee) + const previousDAI = await DAI.balanceOf(employee) + const previousANT = await ANT.balanceOf(employee) await payroll.payday(PAYMENT_TYPES.BONUS, requestedAmount, { from }) - const currentDenominationTokenBalance = await denominationToken.balanceOf(employee) - const expectedDenominationTokenBalance = previousDenominationTokenBalance.plus(requestedDenominationTokenAmount); - assert.equal(currentDenominationTokenBalance.toString(), expectedDenominationTokenBalance.toString(), 'current denomination token balance does not match') + const currentDAI = await DAI.balanceOf(employee) + const expectedDAI = previousDAI.plus(requestedDAI) + assert.equal(currentDAI.toString(), expectedDAI.toString(), 'current DAI balance does not match') - const currentAnotherTokenBalance = await anotherToken.balanceOf(employee) - const expectedAnotherTokenBalance = anotherTokenRate.mul(requestedAnotherTokenAmount).plus(previousAnotherTokenBalance).trunc() - assert.equal(currentAnotherTokenBalance.toString(), expectedAnotherTokenBalance.toString(), 'current token balance does not match') + const currentANT = await ANT.balanceOf(employee) + const expectedANT = previousANT.plus(requestedANT) + assert.equal(currentANT.toString(), expectedANT.toString(), 'current ANT balance does not match') }) it('emits one event per allocated token', async () => { @@ -208,17 +201,17 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { const events = receipt.logs.filter(l => l.event === 'SendPayment') assert.equal(events.length, 2, 'should have emitted two events') - const denominationTokenEvent = events.find(e => e.args.token === denominationToken.address).args - assert.equal(denominationTokenEvent.employee, employee, 'employee address does not match') - assert.equal(denominationTokenEvent.token, denominationToken.address, 'denomination token address does not match') - assert.equal(denominationTokenEvent.amount.toString(), requestedDenominationTokenAmount, 'payment amount does not match') - assert.equal(denominationTokenEvent.paymentReference, 'Bonus', 'payment reference does not match') - - const anotherTokenEvent = events.find(e => e.args.token === anotherToken.address).args - assert.equal(anotherTokenEvent.employee, employee, 'employee address does not match') - assert.equal(anotherTokenEvent.token, anotherToken.address, 'token address does not match') - assert.equal(anotherTokenEvent.amount.div(anotherTokenRate).trunc().toString(), parseInt(requestedAnotherTokenAmount), 'payment amount does not match') - assert.equal(anotherTokenEvent.paymentReference, 'Bonus', 'payment reference does not match') + const eventDAI = events.find(e => e.args.token === DAI.address).args + assert.equal(eventDAI.employee, employee, 'employee address does not match') + assert.equal(eventDAI.token, DAI.address, 'DAI address does not match') + assert.equal(eventDAI.amount.toString(), requestedDAI, 'payment amount does not match') + assert.equal(eventDAI.paymentReference, 'Bonus', 'payment reference does not match') + + const eventANT = events.find(e => e.args.token === ANT.address).args + assert.equal(eventANT.employee, employee, 'employee address does not match') + assert.equal(eventANT.token, ANT.address, 'token address does not match') + assert.equal(eventANT.amount.toString(), requestedANT, 'payment amount does not match') + assert.equal(eventANT.paymentReference, 'Bonus', 'payment reference does not match') }) } @@ -230,7 +223,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { const [address, employeeSalary, bonus] = await payroll.getEmployee(employeeId) assert.equal(address, employee, 'employee address does not match') - assert.equal(employeeSalary, salary, 'employee salary does not match') + assert.equal(employeeSalary.toString(), salary.toString(), 'employee salary does not match') assert.equal(previousBonus.minus(expectedRequestedAmount).toString(), bonus.toString(), 'employee bonus does not match') }) } @@ -244,7 +237,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { context('when exchange rates are expired', () => { beforeEach('expire exchange rates', async () => { const expiredTimestamp = (await payroll.getTimestampPublic()).sub(RATE_EXPIRATION_TIME + 1) - await setTokenRates(priceFeed, denominationToken, [anotherToken], [anotherTokenRate], expiredTimestamp) + await setTokenRates(priceFeed, USD, [DAI, ANT], [DAI_RATE, ANT_RATE], expiredTimestamp) }) it('reverts', async () => { @@ -254,7 +247,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { } context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) context('when the employee has some pending salary', () => { context('when the employee is not terminated', () => { @@ -297,7 +290,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { context('when exchange rates are expired', () => { beforeEach('expire exchange rates', async () => { const expiredTimestamp = (await payroll.getTimestampPublic()).sub(RATE_EXPIRATION_TIME + 1) - await setTokenRates(priceFeed, denominationToken, [anotherToken], [anotherTokenRate], expiredTimestamp) + await setTokenRates(priceFeed, USD, [DAI, ANT], [DAI_RATE, ANT_RATE], expiredTimestamp) }) it('reverts', async () => { @@ -309,7 +302,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { }) context('when the requested amount is less than the total bonus amount', () => { - const requestedAmount = bonusAmount - 1 + const requestedAmount = bonusAmount.div(2) context('when the employee has some pending salary', () => { context('when the employee is not terminated', () => { @@ -388,7 +381,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { context('when exchange rates are expired', () => { beforeEach('expire exchange rates', async () => { const expiredTimestamp = (await payroll.getTimestampPublic()).sub(RATE_EXPIRATION_TIME + 1) - await setTokenRates(priceFeed, denominationToken, [anotherToken], [anotherTokenRate], expiredTimestamp) + await setTokenRates(priceFeed, USD, [DAI, ANT], [DAI_RATE, ANT_RATE], expiredTimestamp) }) it('reverts', async () => { @@ -400,7 +393,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { }) context('when the requested amount is greater than the total bonus amount', () => { - const requestedAmount = bonusAmount + 1 + const requestedAmount = bonusAmount.plus(1) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.BONUS, requestedAmount, { from }), 'PAYROLL_INVALID_REQUESTED_AMT') @@ -410,7 +403,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { context('when the employee does not have pending reimbursements', () => { context('when the requested amount is greater than zero', () => { - const requestedAmount = 100 + const requestedAmount = bigExp(100, 18) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.BONUS, requestedAmount, { from }), 'PAYROLL_NOTHING_PAID') @@ -418,7 +411,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.BONUS, requestedAmount, { from }), 'PAYROLL_NOTHING_PAID') @@ -429,15 +422,15 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { context('when the employee did not set any token allocations yet', () => { context('when the employee has a pending bonus', () => { - const bonusAmount = 100 + const bonusAmount = bigExp(100, 18) beforeEach('add bonus', async () => { - await payroll.addBonus(employeeId, bonusAmount / 2, { from: owner }) - await payroll.addBonus(employeeId, bonusAmount / 2, { from: owner }) + await payroll.addBonus(employeeId, bonusAmount.div(2), { from: owner }) + await payroll.addBonus(employeeId, bonusAmount.div(2), { from: owner }) }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.BONUS, requestedAmount, { from }), 'PAYROLL_NOTHING_PAID') @@ -445,7 +438,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { }) context('when the requested amount is less than the total bonus amount', () => { - const requestedAmount = bonusAmount - 1 + const requestedAmount = bonusAmount.minus(1) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.BONUS, requestedAmount, { from }), 'PAYROLL_NOTHING_PAID') @@ -461,7 +454,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { }) context('when the requested amount is greater than the total bonus amount', () => { - const requestedAmount = bonusAmount + 1 + const requestedAmount = bonusAmount.plus(1) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.BONUS, requestedAmount, { from }), 'PAYROLL_INVALID_REQUESTED_AMT') @@ -471,7 +464,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { context('when the employee does not have pending reimbursements', () => { context('when the requested amount is greater than zero', () => { - const requestedAmount = 100 + const requestedAmount = bigExp(100, 18) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.BONUS, requestedAmount, { from }), 'PAYROLL_NOTHING_PAID') @@ -479,7 +472,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.BONUS, requestedAmount, { from }), 'PAYROLL_NOTHING_PAID') @@ -493,7 +486,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { const from = anyone context('when the requested amount is greater than zero', () => { - const requestedAmount = 100 + const requestedAmount = bigExp(100, 18) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.BONUS, requestedAmount, { from }), 'PAYROLL_EMPLOYEE_DOES_NOT_MATCH') @@ -501,7 +494,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.BONUS, requestedAmount, { from }), 'PAYROLL_EMPLOYEE_DOES_NOT_MATCH') @@ -511,7 +504,7 @@ contract('Payroll bonuses', ([owner, employee, anyone]) => { }) context('when it has not been initialized yet', function () { - const requestedAmount = 0 + const requestedAmount = bn(0) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.BONUS, requestedAmount, { from: employee }), 'PAYROLL_EMPLOYEE_DOES_NOT_MATCH') diff --git a/future-apps/payroll/test/contracts/Payroll_forwarding.test.js b/future-apps/payroll/test/contracts/Payroll_forwarding.test.js index 8f185ad8f9..4deff90898 100644 --- a/future-apps/payroll/test/contracts/Payroll_forwarding.test.js +++ b/future-apps/payroll/test/contracts/Payroll_forwarding.test.js @@ -1,23 +1,19 @@ const { assertRevert } = require('@aragon/test-helpers/assertThrow') -const { encodeCallScript } = require('@aragon/test-helpers/evmScript') const { getEventArgument } = require('../helpers/events') -const { deployErc20TokenAndDeposit, deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy.js')(artifacts, web3) +const { encodeCallScript } = require('@aragon/test-helpers/evmScript') +const { annualSalaryPerSecond } = require('../helpers/numbers')(web3) +const { USD, deployDAI } = require('../helpers/tokens')(artifacts, web3) +const { NOW, ONE_MONTH, RATE_EXPIRATION_TIME } = require('../helpers/time') +const { deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy')(artifacts, web3) const ExecutionTarget = artifacts.require('ExecutionTarget') contract('Payroll forwarding,', ([owner, employee, anyone]) => { - let dao, payroll, payrollBase, finance, vault, priceFeed, denominationToken - - const NOW = 1553703809 // random fixed timestamp in seconds - const ONE_MONTH = 60 * 60 * 24 * 31 - const TWO_MONTHS = ONE_MONTH * 2 - const RATE_EXPIRATION_TIME = TWO_MONTHS - - const TOKEN_DECIMALS = 18 + let dao, payroll, payrollBase, finance, vault, priceFeed, DAI before('deploy base apps and tokens', async () => { ({ dao, finance, vault, payrollBase } = await deployContracts(owner)) - denominationToken = await deployErc20TokenAndDeposit(owner, finance, 'Denomination Token', TOKEN_DECIMALS) + DAI = await deployDAI(owner, finance) }) beforeEach('create payroll and price feed instance', async () => { @@ -26,8 +22,8 @@ contract('Payroll forwarding,', ([owner, employee, anyone]) => { describe('isForwarder', () => { context('when it has already been initialized', function () { - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) it('returns true', async () => { @@ -44,8 +40,8 @@ contract('Payroll forwarding,', ([owner, employee, anyone]) => { describe('canForward', () => { context('when it has already been initialized', function () { - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) context('when the sender is an employee', () => { @@ -53,7 +49,7 @@ contract('Payroll forwarding,', ([owner, employee, anyone]) => { const sender = employee beforeEach('add employee', async () => { - const receipt = await payroll.addEmployee(employee, 100000, 'Boss', await payroll.getTimestampPublic(), { from: owner }) + const receipt = await payroll.addEmployee(employee, annualSalaryPerSecond(100000), 'Boss', await payroll.getTimestampPublic(), { from: owner }) employeeId = getEventArgument(receipt, 'AddEmployee', 'employeeId').toString() }) @@ -101,8 +97,8 @@ contract('Payroll forwarding,', ([owner, employee, anyone]) => { }) context('when it has already been initialized', function () { - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) context('when the sender is an employee', () => { @@ -110,7 +106,7 @@ contract('Payroll forwarding,', ([owner, employee, anyone]) => { const from = employee beforeEach('add employee', async () => { - const receipt = await payroll.addEmployee(employee, 100000, 'Boss', await payroll.getTimestampPublic(), { from: owner }) + const receipt = await payroll.addEmployee(employee, annualSalaryPerSecond(100000), 'Boss', await payroll.getTimestampPublic(), { from: owner }) employeeId = getEventArgument(receipt, 'AddEmployee', 'employeeId').toString() }) diff --git a/future-apps/payroll/test/contracts/Payroll_gas_costs.test.js b/future-apps/payroll/test/contracts/Payroll_gas_costs.test.js index f1c64be636..6449aa94be 100644 --- a/future-apps/payroll/test/contracts/Payroll_gas_costs.test.js +++ b/future-apps/payroll/test/contracts/Payroll_gas_costs.test.js @@ -1,21 +1,16 @@ const PAYMENT_TYPES = require('../helpers/payment_types') -const setTokenRates = require('../helpers/set_token_rates')(web3) const { annualSalaryPerSecond } = require('../helpers/numbers')(web3) -const { deployErc20TokenAndDeposit, deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy.js')(artifacts, web3) +const { NOW, ONE_MONTH, RATE_EXPIRATION_TIME } = require('../helpers/time') +const { deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy')(artifacts, web3) +const { USD, deployDAI, deployANT, DAI_RATE, ANT_RATE, setTokenRates } = require('../helpers/tokens')(artifacts, web3) contract('Payroll gas costs', ([owner, employee, anotherEmployee]) => { - let dao, payroll, payrollBase, finance, vault, priceFeed, denominationToken - - const NOW = 1553703809 // random fixed timestamp in seconds - const ONE_MONTH = 60 * 60 * 24 * 31 - const TWO_MONTHS = ONE_MONTH * 2 - const RATE_EXPIRATION_TIME = TWO_MONTHS - - const TOKEN_DECIMALS = 18 + let dao, payroll, payrollBase, finance, vault, priceFeed, DAI, ANT before('deploy base apps and tokens', async () => { ({ dao, finance, vault, payrollBase } = await deployContracts(owner)) - denominationToken = await deployErc20TokenAndDeposit(owner, finance, 'Denomination Token', TOKEN_DECIMALS) + DAI = await deployDAI(owner, finance) + ANT = await deployANT(owner, finance) }) beforeEach('create payroll and price feed instance', async () => { @@ -23,52 +18,42 @@ contract('Payroll gas costs', ([owner, employee, anotherEmployee]) => { }) describe('gas costs', () => { - let erc20Token1, erc20Token2 - - before('deploy more tokens', async () => { - erc20Token1 = await deployErc20TokenAndDeposit(owner, finance, 'Token 1', 16) - erc20Token2 = await deployErc20TokenAndDeposit(owner, finance, 'Token 2', 18) - }) - - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) const startDate = NOW - ONE_MONTH - const salary = annualSalaryPerSecond(100000, TOKEN_DECIMALS) + const salary = annualSalaryPerSecond(100000) await payroll.addEmployee(employee, salary, 'Boss', startDate) await payroll.addEmployee(anotherEmployee, salary, 'Manager', startDate) }) - context('when there are not allowed tokens yet', function () { - it('expends ~330k gas for a single allowed token', async () => { - await payroll.addAllowedToken(denominationToken.address) - await payroll.determineAllocation([denominationToken.address], [100], { from: employee }) + beforeEach('allow tokens and set rates', async () => { + await payroll.addAllowedToken(DAI.address, { from: owner }) + await payroll.addAllowedToken(ANT.address, { from: owner }) - const { receipt: { cumulativeGasUsed } } = await payroll.payday(PAYMENT_TYPES.PAYROLL, 0, { from: employee }) - - assert.isBelow(cumulativeGasUsed, 330000, 'payout gas cost for a single allowed token should be ~314k') - }) + await setTokenRates(priceFeed, USD, [DAI, ANT], [DAI_RATE, ANT_RATE]) }) - context('when there are some allowed tokens', function () { - const erc20Token1Rate = 2, erc20Token2Rate = 5 + context('when there is only one allowed token', function () { + it('expends ~335k gas for a single allowed token', async () => { + await payroll.determineAllocation([DAI.address], [100], { from: employee }) - beforeEach('allow tokens and set rates', async () => { - await payroll.addAllowedToken(denominationToken.address, { from: owner }) - await payroll.addAllowedToken(erc20Token1.address, { from: owner }) - await payroll.addAllowedToken(erc20Token2.address, { from: owner }) - await setTokenRates(priceFeed, denominationToken, [erc20Token1, erc20Token2], [erc20Token1Rate, erc20Token2Rate]) + const { receipt: { cumulativeGasUsed } } = await payroll.payday(PAYMENT_TYPES.PAYROLL, 0, { from: employee }) + + assert.isBelow(cumulativeGasUsed, 335000, 'payout gas cost for a single allowed token should be ~335k') }) + }) - it('expends ~270k gas per allowed token', async () => { - await payroll.determineAllocation([denominationToken.address, erc20Token1.address], [60, 40], { from: employee }) + context('when there are two allowed token', function () { + it('expends ~295k gas per allowed token', async () => { + await payroll.determineAllocation([DAI.address], [100], { from: employee }) const { receipt: { cumulativeGasUsed: employeePayoutGasUsed } } = await payroll.payday(PAYMENT_TYPES.PAYROLL, 0, { from: employee }) - await payroll.determineAllocation([denominationToken.address, erc20Token1.address, erc20Token2.address], [65, 25, 10], { from: anotherEmployee }) + await payroll.determineAllocation([DAI.address, ANT.address], [60, 40], { from: anotherEmployee }) const { receipt: { cumulativeGasUsed: anotherEmployeePayoutGasUsed } } = await payroll.payday(PAYMENT_TYPES.PAYROLL, 0, { from: anotherEmployee }) const gasPerAllowedToken = anotherEmployeePayoutGasUsed - employeePayoutGasUsed - assert.isBelow(gasPerAllowedToken, 280000, 'payout gas cost increment per allowed token should be ~270k') + assert.isBelow(gasPerAllowedToken, 295000, 'payout gas cost increment per allowed token should be ~295k') }) }) }) diff --git a/future-apps/payroll/test/contracts/Payroll_get_employee.test.js b/future-apps/payroll/test/contracts/Payroll_get_employee.test.js index c344c0c287..fbbab737a4 100644 --- a/future-apps/payroll/test/contracts/Payroll_get_employee.test.js +++ b/future-apps/payroll/test/contracts/Payroll_get_employee.test.js @@ -1,23 +1,19 @@ -const { maxUint64 } = require('../helpers/numbers')(web3) +const { MAX_UINT64 } = require('../helpers/numbers')(web3) const { assertRevert } = require('@aragon/test-helpers/assertThrow') const { getEventArgument } = require('../helpers/events') -const { deployErc20TokenAndDeposit, deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy.js')(artifacts, web3) +const { NOW, RATE_EXPIRATION_TIME } = require('../helpers/time') +const { annualSalaryPerSecond } = require('../helpers/numbers')(web3) +const { USD, deployDAI } = require('../helpers/tokens')(artifacts, web3) +const { deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy')(artifacts, web3) contract('Payroll employee getters', ([owner, employee]) => { - let dao, payroll, payrollBase, finance, vault, priceFeed, denominationToken - - const NOW = 1553703809 // random fixed timestamp in seconds - const ONE_MONTH = 60 * 60 * 24 * 31 - const TWO_MONTHS = ONE_MONTH * 2 - const RATE_EXPIRATION_TIME = TWO_MONTHS - - const TOKEN_DECIMALS = 18 + let dao, payroll, payrollBase, finance, vault, priceFeed, DAI const currentTimestamp = async () => payroll.getTimestampPublic() before('deploy base apps and tokens', async () => { ({ dao, finance, vault, payrollBase } = await deployContracts(owner)) - denominationToken = await deployErc20TokenAndDeposit(owner, finance, 'Denomination Token', TOKEN_DECIMALS) + DAI = await deployDAI(owner, finance) }) beforeEach('create payroll and price feed instance', async () => { @@ -26,28 +22,29 @@ contract('Payroll employee getters', ([owner, employee]) => { describe('getEmployee', () => { context('when it has already been initialized', function () { - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) context('when the given id exists', () => { let employeeId + const salary = annualSalaryPerSecond(100000) beforeEach('add employee', async () => { - const receipt = await payroll.addEmployee(employee, 1000, 'Boss', await payroll.getTimestampPublic(), { from: owner }) + const receipt = await payroll.addEmployee(employee, salary, 'Boss', await payroll.getTimestampPublic(), { from: owner }) employeeId = getEventArgument(receipt, 'AddEmployee', 'employeeId').toString() }) it('adds a new employee', async () => { - const [address, salary, bonus, reimbursements, accruedSalary, lastPayroll, endDate] = await payroll.getEmployee(employeeId) + const [address, employeeSalary, bonus, reimbursements, accruedSalary, lastPayroll, endDate] = await payroll.getEmployee(employeeId) assert.equal(address, employee, 'employee address does not match') assert.equal(bonus.toString(), 0, 'employee bonus does not match') assert.equal(reimbursements, 0, 'employee reimbursements does not match') assert.equal(accruedSalary, 0, 'employee accrued salary does not match') - assert.equal(salary.toString(), 1000, 'employee salary does not match') + assert.equal(employeeSalary.toString(), salary.toString(), 'employee salary does not match') assert.equal(lastPayroll.toString(), (await currentTimestamp()).toString(), 'employee last payroll does not match') - assert.equal(endDate.toString(), maxUint64(), 'employee end date does not match') + assert.equal(endDate.toString(), MAX_UINT64, 'employee end date does not match') }) }) @@ -71,29 +68,30 @@ contract('Payroll employee getters', ([owner, employee]) => { describe('getEmployeeByAddress', () => { context('when it has already been initialized', function () { - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) context('when the given address exists', () => { let employeeId const address = employee + const salary = annualSalaryPerSecond(100000) beforeEach('add employee', async () => { - const receipt = await payroll.addEmployee(employee, 1000, 'Boss', await payroll.getTimestampPublic(), { from: owner }) + const receipt = await payroll.addEmployee(employee, salary, 'Boss', await payroll.getTimestampPublic(), { from: owner }) employeeId = getEventArgument(receipt, 'AddEmployee', 'employeeId') }) it('adds a new employee', async () => { - const [id, salary, bonus, reimbursements, accruedSalary, lastPayroll, endDate] = await payroll.getEmployeeByAddress(address) + const [id, employeeSalary, bonus, reimbursements, accruedSalary, lastPayroll, endDate] = await payroll.getEmployeeByAddress(address) assert.equal(id.toString(), employeeId.toString(), 'employee id does not match') - assert.equal(salary.toString(), 1000, 'employee salary does not match') + assert.equal(employeeSalary.toString(), salary.toString(), 'employee salary does not match') assert.equal(bonus.toString(), 0, 'employee bonus does not match') assert.equal(reimbursements.toString(), 0, 'employee reimbursements does not match') assert.equal(accruedSalary.toString(), 0, 'employee accrued salary does not match') assert.equal(lastPayroll.toString(), (await currentTimestamp()).toString(), 'employee last payroll does not match') - assert.equal(endDate.toString(), maxUint64(), 'employee end date does not match') + assert.equal(endDate.toString(), MAX_UINT64, 'employee end date does not match') }) }) diff --git a/future-apps/payroll/test/contracts/Payroll_initialize.test.js b/future-apps/payroll/test/contracts/Payroll_initialize.test.js index 95ab36b032..1162e32d94 100644 --- a/future-apps/payroll/test/contracts/Payroll_initialize.test.js +++ b/future-apps/payroll/test/contracts/Payroll_initialize.test.js @@ -1,21 +1,15 @@ +const { USD } = require('../helpers/tokens')(artifacts, web3) const { assertRevert } = require('@aragon/test-helpers/assertThrow') -const { deployErc20TokenAndDeposit, deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy.js')(artifacts, web3) +const { NOW, RATE_EXPIRATION_TIME } = require('../helpers/time') +const { deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy')(artifacts, web3) const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' contract('Payroll initialization', ([owner]) => { - let dao, payroll, payrollBase, finance, vault, priceFeed, denominationToken - - const NOW = 1553703809 // random fixed timestamp in seconds - const ONE_MONTH = 60 * 60 * 24 * 31 - const TWO_MONTHS = ONE_MONTH * 2 - const RATE_EXPIRATION_TIME = TWO_MONTHS - - const TOKEN_DECIMALS = 18 + let dao, payroll, payrollBase, finance, vault, priceFeed before('deploy base apps and tokens', async () => { ({ dao, finance, vault, payrollBase } = await deployContracts(owner)) - denominationToken = await deployErc20TokenAndDeposit(owner, finance, 'Denomination Token', TOKEN_DECIMALS) }) beforeEach('create payroll and price feed instance', async () => { @@ -27,7 +21,7 @@ contract('Payroll initialization', ([owner]) => { it('cannot initialize the base app', async () => { assert(await payrollBase.isPetrified(), 'base payroll app should be petrified') - await assertRevert(payrollBase.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from }), 'INIT_ALREADY_INITIALIZED') + await assertRevert(payrollBase.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from }), 'INIT_ALREADY_INITIALIZED') }) context('when it has not been initialized yet', function () { @@ -38,32 +32,32 @@ contract('Payroll initialization', ([owner]) => { it('reverts when passing an expiration time lower than or equal to a minute', async () => { const ONE_MINUTE = 60 - await assertRevert(payroll.initialize(finance.address, denominationToken.address, priceFeed.address, ONE_MINUTE, { from }), 'PAYROLL_EXPIRY_TIME_TOO_SHORT') + await assertRevert(payroll.initialize(finance.address, USD, priceFeed.address, ONE_MINUTE, { from }), 'PAYROLL_EXPIRY_TIME_TOO_SHORT') }) it('reverts when passing an invalid finance instance', async () => { - await assertRevert(payroll.initialize(ZERO_ADDRESS, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from }), 'PAYROLL_FINANCE_NOT_CONTRACT') + await assertRevert(payroll.initialize(ZERO_ADDRESS, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from }), 'PAYROLL_FINANCE_NOT_CONTRACT') }) it('reverts when passing an invalid feed instance', async () => { - await assertRevert(payroll.initialize(finance.address, denominationToken.address, ZERO_ADDRESS, RATE_EXPIRATION_TIME, { from }), 'PAYROLL_FEED_NOT_CONTRACT') + await assertRevert(payroll.initialize(finance.address, USD, ZERO_ADDRESS, RATE_EXPIRATION_TIME, { from }), 'PAYROLL_FEED_NOT_CONTRACT') }) }) context('when it has already been initialized', function () { beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from }) + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from }) }) it('cannot be initialized again', async () => { - await assertRevert(payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from }), 'INIT_ALREADY_INITIALIZED') + await assertRevert(payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from }), 'INIT_ALREADY_INITIALIZED') }) it('has a price feed instance, a finance instance, a denomination token and a rate expiration time', async () => { assert.equal(await payroll.feed(), priceFeed.address, 'feed address does not match') assert.equal(await payroll.finance(), finance.address, 'finance address should match') - assert.equal(await payroll.denominationToken(), denominationToken.address, 'denomination token does not match') assert.equal(await payroll.rateExpiryTime(), RATE_EXPIRATION_TIME, 'rate expiration time does not match') + assert.equal(web3.toChecksumAddress(await payroll.denominationToken()), USD, 'denomination token address does not match') }) }) }) diff --git a/future-apps/payroll/test/contracts/Payroll_modify_employee.test.js b/future-apps/payroll/test/contracts/Payroll_modify_employee.test.js index 37dceca2ba..ff825669ed 100644 --- a/future-apps/payroll/test/contracts/Payroll_modify_employee.test.js +++ b/future-apps/payroll/test/contracts/Payroll_modify_employee.test.js @@ -1,19 +1,14 @@ +const { USD } = require('../helpers/tokens')(artifacts, web3) const { assertRevert } = require('@aragon/test-helpers/assertThrow') const { getEvents, getEventArgument } = require('../helpers/events') -const { maxUint256, annualSalaryPerSecond } = require('../helpers/numbers')(web3) -const { deployErc20TokenAndDeposit, deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy.js')(artifacts, web3) +const { NOW, ONE_MONTH, RATE_EXPIRATION_TIME } = require('../helpers/time') +const { bn, MAX_UINT256, annualSalaryPerSecond } = require('../helpers/numbers')(web3) +const { deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy')(artifacts, web3) const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' contract('Payroll employees modification', ([owner, employee, anotherEmployee, anyone]) => { - let dao, payroll, payrollBase, finance, vault, priceFeed, denominationToken - - const NOW = 1553703809 // random fixed timestamp in seconds - const ONE_MONTH = 60 * 60 * 24 * 31 - const TWO_MONTHS = ONE_MONTH * 2 - const RATE_EXPIRATION_TIME = TWO_MONTHS - - const TOKEN_DECIMALS = 18 + let dao, payroll, payrollBase, finance, vault, priceFeed const increaseTime = async seconds => { await payroll.mockIncreaseTime(seconds) @@ -22,7 +17,6 @@ contract('Payroll employees modification', ([owner, employee, anotherEmployee, a before('deploy base apps and tokens', async () => { ({ dao, finance, vault, payrollBase } = await deployContracts(owner)) - denominationToken = await deployErc20TokenAndDeposit(owner, finance, 'Denomination Token', TOKEN_DECIMALS) }) beforeEach('create payroll and price feed instance', async () => { @@ -31,8 +25,8 @@ contract('Payroll employees modification', ([owner, employee, anotherEmployee, a describe('setEmployeeSalary', () => { context('when it has already been initialized', function () { - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) context('when the sender has permissions', () => { @@ -40,7 +34,7 @@ contract('Payroll employees modification', ([owner, employee, anotherEmployee, a context('when the given employee exists', () => { let employeeId - const previousSalary = annualSalaryPerSecond(100000, TOKEN_DECIMALS) + const previousSalary = annualSalaryPerSecond(100000) beforeEach('add employee', async () => { const receipt = await payroll.addEmployee(employee, previousSalary, 'Boss', await payroll.getTimestampPublic()) @@ -54,7 +48,7 @@ contract('Payroll employees modification', ([owner, employee, anotherEmployee, a await payroll.setEmployeeSalary(employeeId, newSalary, { from }) const salary = (await payroll.getEmployee(employeeId))[1] - assert.equal(salary.toString(), newSalary, 'accrued salary does not match') + assert.equal(salary.toString(), newSalary.toString(), 'accrued salary does not match') }) it('adds previous owed salary to the accrued salary', async () => { @@ -64,24 +58,24 @@ contract('Payroll employees modification', ([owner, employee, anotherEmployee, a await increaseTime(ONE_MONTH) const accruedSalary = (await payroll.getEmployee(employeeId))[4] - const expectedAccruedSalary = previousSalary * ONE_MONTH - assert.equal(accruedSalary.toString(), expectedAccruedSalary, 'accrued salary does not match') + const expectedAccruedSalary = previousSalary.mul(ONE_MONTH) + assert.equal(accruedSalary.toString(), expectedAccruedSalary.toString(), 'accrued salary does not match') const events = getEvents(receipt, 'AddEmployeeAccruedSalary') assert.equal(events.length, 1, 'number of AddEmployeeAccruedSalary emitted events does not match') assert.equal(events[0].args.employeeId.toString(), employeeId, 'employee id does not match') - assert.equal(events[0].args.amount.toString(), expectedAccruedSalary, 'accrued salary does not match') + assert.equal(events[0].args.amount.toString(), expectedAccruedSalary.toString(), 'accrued salary does not match') }) it('accrues all previous owed salary as accrued salary', async () => { await increaseTime(ONE_MONTH) await payroll.setEmployeeSalary(employeeId, newSalary, { from }) await increaseTime(ONE_MONTH) - await payroll.setEmployeeSalary(employeeId, newSalary * 2, { from }) + await payroll.setEmployeeSalary(employeeId, newSalary.mul(2), { from }) const accruedSalary = (await payroll.getEmployee(employeeId))[4] - const expectedAccruedSalary = previousSalary * ONE_MONTH + newSalary * ONE_MONTH - assert.equal(accruedSalary.toString(), expectedAccruedSalary, 'accrued salary does not match') + const expectedAccruedSalary = previousSalary.mul(ONE_MONTH).plus(newSalary.mul(ONE_MONTH)) + assert.equal(accruedSalary.toString(), expectedAccruedSalary.toString(), 'accrued salary does not match') }) it('emits a SetEmployeeSalary event', async () => { @@ -90,12 +84,12 @@ contract('Payroll employees modification', ([owner, employee, anotherEmployee, a const events = getEvents(receipt, 'SetEmployeeSalary') assert.equal(events.length, 1, 'number of SetEmployeeSalary emitted events does not match') assert.equal(events[0].args.employeeId.toString(), employeeId, 'employee id does not match') - assert.equal(events[0].args.denominationSalary.toString(), newSalary, 'salary does not match') + assert.equal(events[0].args.denominationSalary.toString(), newSalary.toString(), 'salary does not match') }) } context('when the given value greater than zero', () => { - const newSalary = 1000 + const newSalary = previousSalary.mul(2) context('when the employee is not owed a huge salary amount', () => { itSetsSalarySuccessfully(newSalary) @@ -103,7 +97,7 @@ contract('Payroll employees modification', ([owner, employee, anotherEmployee, a context('when the employee is owed a huge salary amount', () => { beforeEach('accrued a huge salary amount', async () => { - await payroll.setEmployeeSalary(employeeId, maxUint256(), { from }) + await payroll.setEmployeeSalary(employeeId, MAX_UINT256, { from }) await increaseTime(ONE_MONTH) }) @@ -114,7 +108,7 @@ contract('Payroll employees modification', ([owner, employee, anotherEmployee, a }) context('when the given value is zero', () => { - const newSalary = 0 + const newSalary = bn(0) itSetsSalarySuccessfully(newSalary) }) @@ -162,8 +156,8 @@ contract('Payroll employees modification', ([owner, employee, anotherEmployee, a describe('changeAddressByEmployee', () => { context('when it has already been initialized', function () { - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) context('when the sender is an employee', () => { @@ -171,7 +165,7 @@ contract('Payroll employees modification', ([owner, employee, anotherEmployee, a let employeeId beforeEach('add employee', async () => { - const receipt = await payroll.addEmployee(employee, 1000, 'Boss', await payroll.getTimestampPublic(), { from: owner }) + const receipt = await payroll.addEmployee(employee, annualSalaryPerSecond(100000), 'Boss', await payroll.getTimestampPublic(), { from: owner }) employeeId = getEventArgument(receipt, 'AddEmployee', 'employeeId') }) @@ -207,7 +201,7 @@ contract('Payroll employees modification', ([owner, employee, anotherEmployee, a context('when the given address belongs to another employee', () => { beforeEach('add another employee', async () => { - await payroll.addEmployee(anotherEmployee, 1000, 'Boss', await payroll.getTimestampPublic(), { from: owner }) + await payroll.addEmployee(anotherEmployee, annualSalaryPerSecond(100000), 'Boss', await payroll.getTimestampPublic(), { from: owner }) }) it('reverts', async () => { diff --git a/future-apps/payroll/test/contracts/Payroll_payday.test.js b/future-apps/payroll/test/contracts/Payroll_payday.test.js index 0b0b58f7b6..ab289d0f48 100644 --- a/future-apps/payroll/test/contracts/Payroll_payday.test.js +++ b/future-apps/payroll/test/contracts/Payroll_payday.test.js @@ -1,19 +1,13 @@ const PAYMENT_TYPES = require('../helpers/payment_types') -const setTokenRates = require('../helpers/set_token_rates')(web3) const { assertRevert } = require('@aragon/test-helpers/assertThrow') -const { bn, maxUint256 } = require('../helpers/numbers')(web3) const { getEventArgument } = require('../helpers/events') -const { deployErc20TokenAndDeposit, deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy.js')(artifacts, web3) +const { bn, ONE, MAX_UINT256, bigExp } = require('../helpers/numbers')(web3) +const { NOW, ONE_MONTH, TWO_MONTHS, RATE_EXPIRATION_TIME } = require('../helpers/time') +const { deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy')(artifacts, web3) +const { USD, DAI_RATE, ANT_RATE, exchangedAmount, deployDAI, deployANT, setTokenRates } = require('../helpers/tokens')(artifacts, web3) contract('Payroll payday', ([owner, employee, anyone]) => { - let dao, payroll, payrollBase, finance, vault, priceFeed, denominationToken, anotherToken, anotherTokenRate - - const NOW = 1553703809 // random fixed timestamp in seconds - const ONE_MONTH = 60 * 60 * 24 * 31 - const TWO_MONTHS = ONE_MONTH * 2 - const RATE_EXPIRATION_TIME = TWO_MONTHS - - const TOKEN_DECIMALS = 18 + let dao, payroll, payrollBase, finance, vault, priceFeed, DAI, ANT const increaseTime = async seconds => { await payroll.mockIncreaseTime(seconds) @@ -22,8 +16,8 @@ contract('Payroll payday', ([owner, employee, anyone]) => { before('deploy base apps and tokens', async () => { ({ dao, finance, vault, payrollBase } = await deployContracts(owner)) - anotherToken = await deployErc20TokenAndDeposit(owner, finance, 'Another token', TOKEN_DECIMALS) - denominationToken = await deployErc20TokenAndDeposit(owner, finance, 'Denomination Token', TOKEN_DECIMALS) + ANT = await deployANT(owner, finance) + DAI = await deployDAI(owner, finance) }) beforeEach('create payroll and price feed instance', async () => { @@ -32,13 +26,12 @@ contract('Payroll payday', ([owner, employee, anyone]) => { describe('payroll payday', () => { context('when it has already been initialized', function () { - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) beforeEach('set token rates', async () => { - anotherTokenRate = bn(5) - await setTokenRates(priceFeed, denominationToken, [anotherToken], [anotherTokenRate]) + await setTokenRates(priceFeed, USD, [DAI, ANT], [DAI_RATE, ANT_RATE]) }) context('when the sender is an employee', () => { @@ -46,7 +39,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { const from = employee context('when the employee has a reasonable salary', () => { - const salary = 100000 + const salary = bigExp(1, 18) // using 1 USD per second to simplify incomes in tests beforeEach('add employee', async () => { const receipt = await payroll.addEmployee(employee, salary, 'Boss', await payroll.getTimestampPublic()) @@ -54,32 +47,32 @@ contract('Payroll payday', ([owner, employee, anyone]) => { }) context('when the employee has already set some token allocations', () => { - const denominationTokenAllocation = 80 - const anotherTokenAllocation = 20 + const allocationDAI = 80 + const allocationANT = 20 beforeEach('set tokens allocation', async () => { - await payroll.addAllowedToken(anotherToken.address, { from: owner }) - await payroll.addAllowedToken(denominationToken.address, { from: owner }) - await payroll.determineAllocation([denominationToken.address, anotherToken.address], [denominationTokenAllocation, anotherTokenAllocation], { from }) + await payroll.addAllowedToken(ANT.address, { from: owner }) + await payroll.addAllowedToken(DAI.address, { from: owner }) + await payroll.determineAllocation([DAI.address, ANT.address], [allocationDAI, allocationANT], { from }) }) const assertTransferredAmounts = (requestedAmount, expectedRequestedAmount = requestedAmount) => { - const requestedDenominationTokenAmount = Math.round(expectedRequestedAmount * denominationTokenAllocation / 100) - const requestedAnotherTokenAmount = Math.round(expectedRequestedAmount * anotherTokenAllocation / 100) + const requestedDAI = exchangedAmount(expectedRequestedAmount, DAI_RATE, allocationDAI) + const requestedANT = exchangedAmount(expectedRequestedAmount, ANT_RATE, allocationANT) it('transfers the requested salary amount', async () => { - const previousDenominationTokenBalance = await denominationToken.balanceOf(employee) - const previousAnotherTokenBalance = await anotherToken.balanceOf(employee) + const previousDAI = await DAI.balanceOf(employee) + const previousANT = await ANT.balanceOf(employee) await payroll.payday(PAYMENT_TYPES.PAYROLL, requestedAmount, { from }) - const currentDenominationTokenBalance = await denominationToken.balanceOf(employee) - const expectedDenominationTokenBalance = previousDenominationTokenBalance.plus(requestedDenominationTokenAmount); - assert.equal(currentDenominationTokenBalance.toString(), expectedDenominationTokenBalance.toString(), 'current denomination token balance does not match') + const currentDAI = await DAI.balanceOf(employee) + const expectedDAI = previousDAI.plus(requestedDAI); + assert.equal(currentDAI.toString(), expectedDAI.toString(), 'current DAI balance does not match') - const currentAnotherTokenBalance = await anotherToken.balanceOf(employee) - const expectedAnotherTokenBalance = anotherTokenRate.mul(requestedAnotherTokenAmount).plus(previousAnotherTokenBalance).trunc() - assert.equal(currentAnotherTokenBalance.toString(), expectedAnotherTokenBalance.toString(), 'current token balance does not match') + const currentANT = await ANT.balanceOf(employee) + const expectedANT = previousANT.plus(requestedANT) + assert.equal(currentANT.toString(), expectedANT.toString(), 'current ANT balance does not match') }) it('emits one event per allocated token', async () => { @@ -88,43 +81,43 @@ contract('Payroll payday', ([owner, employee, anyone]) => { const events = receipt.logs.filter(l => l.event === 'SendPayment') assert.equal(events.length, 2, 'should have emitted two events') - const denominationTokenEvent = events.find(e => e.args.token === denominationToken.address).args - assert.equal(denominationTokenEvent.employee, employee, 'employee address does not match') - assert.equal(denominationTokenEvent.token, denominationToken.address, 'denomination token address does not match') - assert.equal(denominationTokenEvent.amount.toString(), requestedDenominationTokenAmount, 'payment amount does not match') - assert.equal(denominationTokenEvent.paymentReference, 'Payroll', 'payment reference does not match') + const eventDAI = events.find(e => e.args.token === DAI.address).args + assert.equal(eventDAI.employee, employee, 'employee address does not match') + assert.equal(eventDAI.token, DAI.address, 'DAI address does not match') + assert.equal(eventDAI.amount.toString(), requestedDAI.toString(), 'payment amount does not match') + assert.equal(eventDAI.paymentReference, 'Payroll', 'payment reference does not match') - const anotherTokenEvent = events.find(e => e.args.token === anotherToken.address).args - assert.equal(anotherTokenEvent.employee, employee, 'employee address does not match') - assert.equal(anotherTokenEvent.token, anotherToken.address, 'token address does not match') - assert.equal(anotherTokenEvent.amount.div(anotherTokenRate).trunc().toString(), requestedAnotherTokenAmount, 'payment amount does not match') - assert.equal(anotherTokenEvent.paymentReference, 'Payroll', 'payment reference does not match') + const eventANT = events.find(e => e.args.token === ANT.address).args + assert.equal(eventANT.employee, employee, 'employee address does not match') + assert.equal(eventANT.token, ANT.address, 'ANT address does not match') + assert.equal(eventANT.amount.toString(), requestedANT.toString(), 'payment amount does not match') + assert.equal(eventANT.paymentReference, 'Payroll', 'payment reference does not match') }) it('can be called multiple times between periods of time', async () => { // terminate employee in the future to ensure we can request payroll multiple times await payroll.terminateEmployee(employeeId, NOW + TWO_MONTHS + TWO_MONTHS, { from: owner }) - const previousDenominationTokenBalance = await denominationToken.balanceOf(employee) - const previousAnotherTokenBalance = await anotherToken.balanceOf(employee) + const previousDAI = await DAI.balanceOf(employee) + const previousANT = await ANT.balanceOf(employee) await payroll.payday(PAYMENT_TYPES.PAYROLL, requestedAmount, { from }) - const newOwedAmount = salary * ONE_MONTH - const newDenominationTokenOwedAmount = Math.round(newOwedAmount * denominationTokenAllocation / 100) - const newAnotherTokenOwedAmount = Math.round(newOwedAmount * anotherTokenAllocation / 100) + const newOwedAmount = salary.mul(ONE_MONTH) + const newDAIAmount = exchangedAmount(newOwedAmount, DAI_RATE, allocationDAI) + const newANTAmount = exchangedAmount(newOwedAmount, ANT_RATE, allocationANT) await increaseTime(ONE_MONTH) - await setTokenRates(priceFeed, denominationToken, [anotherToken], [anotherTokenRate]) + await setTokenRates(priceFeed, USD, [DAI, ANT], [DAI_RATE, ANT_RATE]) await payroll.payday(PAYMENT_TYPES.PAYROLL, newOwedAmount, { from }) - const currentDenominationTokenBalance = await denominationToken.balanceOf(employee) - const expectedDenominationTokenBalance = previousDenominationTokenBalance.plus(requestedDenominationTokenAmount + newDenominationTokenOwedAmount) - assert.equal(currentDenominationTokenBalance.toString(), expectedDenominationTokenBalance.toString(), 'current denomination token balance does not match') + const currentDAI = await DAI.balanceOf(employee) + const expectedDAI = previousDAI.plus(requestedDAI).plus(newDAIAmount) + assert.equal(currentDAI.toString(), expectedDAI.toString(), 'current DAI balance does not match') - const currentAnotherTokenBalance = await anotherToken.balanceOf(employee) - const expectedAnotherTokenBalance = anotherTokenRate.mul(requestedAnotherTokenAmount + newAnotherTokenOwedAmount).plus(previousAnotherTokenBalance) - assert.equal(currentAnotherTokenBalance.toString(), expectedAnotherTokenBalance.toString(), 'current token balance does not match') + const currentANT = await ANT.balanceOf(employee) + const expectedANT = previousANT.plus(requestedANT).plus(newANTAmount) + assert.equal(currentANT.toString(), expectedANT.toString(), 'current ANT balance does not match') }) } @@ -134,9 +127,9 @@ contract('Payroll payday', ([owner, employee, anyone]) => { const [previousAccruedSalary, previousPayrollDate] = (await payroll.getEmployee(employeeId)).slice(4, 6) if (expectedRequestedAmount >= previousAccruedSalary) { - expectedAccruedSalary = 0 - const remainder = expectedRequestedAmount - previousAccruedSalary - expectedLastPayrollDate = previousPayrollDate.plus(Math.ceil(remainder / salary)) + expectedAccruedSalary = bn(0) + const remainder = expectedRequestedAmount.minus(previousAccruedSalary) + expectedLastPayrollDate = previousPayrollDate.plus(remainder.div(salary).ceil()) } else { expectedAccruedSalary = previousAccruedSalary.minus(expectedRequestedAmount).toString() expectedLastPayrollDate = previousPayrollDate @@ -145,7 +138,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { await payroll.payday(PAYMENT_TYPES.PAYROLL, requestedAmount, { from }) const [accruedSalary, lastPayrollDate] = (await payroll.getEmployee(employeeId)).slice(4, 6) - assert.equal(accruedSalary.toString(), expectedAccruedSalary, 'accrued salary does not match') + assert.equal(accruedSalary.toString(), expectedAccruedSalary.toString(), 'accrued salary does not match') assert.equal(lastPayrollDate.toString(), expectedLastPayrollDate.toString(), 'last payroll date does not match') }) @@ -155,7 +148,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { const [address, employeeSalary] = await payroll.getEmployee(employeeId) assert.equal(address, employee, 'employee address does not match') - assert.equal(employeeSalary, salary, 'employee salary does not match') + assert.equal(employeeSalary.toString(), salary.toString(), 'employee salary does not match') }) } @@ -168,7 +161,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { context('when exchange rates are expired', () => { beforeEach('expire exchange rates', async () => { const expiredTimestamp = (await payroll.getTimestampPublic()).sub(RATE_EXPIRATION_TIME + 1) - await setTokenRates(priceFeed, denominationToken, [anotherToken], [anotherTokenRate], expiredTimestamp) + await setTokenRates(priceFeed, USD, [DAI, ANT], [DAI_RATE, ANT_RATE], expiredTimestamp) }) it('reverts', async () => { @@ -178,7 +171,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { } const itHandlesPayrollProperlyNeverthelessExtrasOwedAmounts = (requestedAmount, totalOwedAmount) => { - const expectedRequestedAmount = requestedAmount === 0 ? totalOwedAmount : requestedAmount + const expectedRequestedAmount = requestedAmount.eq(0) ? totalOwedAmount : requestedAmount context('when the employee has some pending reimbursements', () => { beforeEach('add reimbursement', async () => { @@ -247,7 +240,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { await payroll.terminateEmployee(employeeId, await payroll.getTimestampPublic(), { from: owner }) }) - if (requestedAmount === 0 || requestedAmount === totalOwedAmount) { + if (requestedAmount.eq(0) || requestedAmount === totalOwedAmount) { context('when exchange rates are not expired', () => { assertTransferredAmounts(requestedAmount, expectedRequestedAmount) @@ -261,7 +254,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { context('when exchange rates are expired', () => { beforeEach('expire exchange rates', async () => { const expiredTimestamp = (await payroll.getTimestampPublic()).sub(RATE_EXPIRATION_TIME + 1) - await setTokenRates(priceFeed, denominationToken, [anotherToken], [anotherTokenRate], expiredTimestamp) + await setTokenRates(priceFeed, USD, [DAI, ANT], [DAI_RATE, ANT_RATE], expiredTimestamp) }) it('reverts', async () => { @@ -277,21 +270,21 @@ contract('Payroll payday', ([owner, employee, anyone]) => { context('when the employee does not have accrued salary', () => { context('when the employee has some pending salary', () => { - const currentOwedSalary = salary * ONE_MONTH + const currentOwedSalary = salary.mul(ONE_MONTH) beforeEach('accumulate some pending salary', async () => { await increaseTime(ONE_MONTH) }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) itHandlesPayrollProperlyNeverthelessExtrasOwedAmounts(requestedAmount, currentOwedSalary) }) context('when the requested amount is lower than the total owed salary', () => { context('when the requested amount represents less than a second of the earnings', () => { - const requestedAmount = salary / 2 + const requestedAmount = salary.div(2) it('updates the last payroll date by one second', async () => { const previousLastPayrollDate = (await payroll.getEmployee(employeeId))[5] @@ -304,7 +297,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { }) context('when the requested amount represents more than a second of the earnings', () => { - const requestedAmount = currentOwedSalary / 2 + const requestedAmount = currentOwedSalary.div(2) itHandlesPayrollProperlyNeverthelessExtrasOwedAmounts(requestedAmount, currentOwedSalary) }) @@ -317,7 +310,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { }) context('when the requested amount is greater than the total owed salary', () => { - const requestedAmount = currentOwedSalary + 1 + const requestedAmount = currentOwedSalary.plus(1) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.PAYROLL, requestedAmount, { from }), 'PAYROLL_INVALID_REQUESTED_AMT') @@ -327,7 +320,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { context('when the employee does not have pending salary', () => { context('when the requested amount is greater than zero', () => { - const requestedAmount = 100 + const requestedAmount = bigExp(100, 18) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.PAYROLL, requestedAmount, { from }), 'PAYROLL_NOTHING_PAID') @@ -335,7 +328,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.PAYROLL, requestedAmount, { from }), 'PAYROLL_NOTHING_PAID') @@ -345,8 +338,8 @@ contract('Payroll payday', ([owner, employee, anyone]) => { }) context('when the employee has some accrued salary', () => { - const previousSalary = 10 - const previousOwedSalary = ONE_MONTH * previousSalary + const previousSalary = salary.mul(2) + const previousOwedSalary = previousSalary.mul(ONE_MONTH) beforeEach('accrue some salary', async () => { await payroll.setEmployeeSalary(employeeId, previousSalary, { from: owner }) @@ -355,28 +348,28 @@ contract('Payroll payday', ([owner, employee, anyone]) => { }) context('when the employee has some pending salary', () => { - const currentOwedSalary = salary * ONE_MONTH - const totalOwedSalary = previousOwedSalary + currentOwedSalary + const currentOwedSalary = salary.mul(ONE_MONTH) + const totalOwedSalary = previousOwedSalary.plus(currentOwedSalary) beforeEach('accumulate some pending salary and renew token rates', async () => { await increaseTime(ONE_MONTH) - await setTokenRates(priceFeed, denominationToken, [anotherToken], [anotherTokenRate]) + await setTokenRates(priceFeed, USD, [DAI, ANT], [DAI_RATE, ANT_RATE]) }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) itHandlesPayrollProperlyNeverthelessExtrasOwedAmounts(requestedAmount, totalOwedSalary) }) context('when the requested amount is lower than the previous owed salary', () => { - const requestedAmount = previousOwedSalary - 10 + const requestedAmount = previousOwedSalary.minus(10) itHandlesPayrollProperlyNeverthelessExtrasOwedAmounts(requestedAmount, totalOwedSalary) }) context('when the requested amount is greater than the previous owed salary but lower than the total owed', () => { - const requestedAmount = totalOwedSalary / 2 + const requestedAmount = totalOwedSalary.div(2) itHandlesPayrollProperlyNeverthelessExtrasOwedAmounts(requestedAmount, totalOwedSalary) }) @@ -388,7 +381,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { }) context('when the requested amount is greater than the total owed salary', () => { - const requestedAmount = totalOwedSalary + 1 + const requestedAmount = totalOwedSalary.plus(1) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.PAYROLL, requestedAmount, { from }), 'PAYROLL_INVALID_REQUESTED_AMT') @@ -398,13 +391,13 @@ contract('Payroll payday', ([owner, employee, anyone]) => { context('when the employee does not have pending salary', () => { context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) itHandlesPayrollProperlyNeverthelessExtrasOwedAmounts(requestedAmount, previousOwedSalary) }) context('when the requested amount is lower than the previous owed salary', () => { - const requestedAmount = previousOwedSalary - 10 + const requestedAmount = previousOwedSalary.div(2) itHandlesPayrollProperlyNeverthelessExtrasOwedAmounts(requestedAmount, previousOwedSalary) }) @@ -416,7 +409,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { }) context('when the requested amount is greater than the previous owed salary', () => { - const requestedAmount = previousOwedSalary + 1 + const requestedAmount = previousOwedSalary.plus(1) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.PAYROLL, requestedAmount, { from }), 'PAYROLL_INVALID_REQUESTED_AMT') @@ -428,14 +421,14 @@ contract('Payroll payday', ([owner, employee, anyone]) => { context('when the employee did not set any token allocations yet', () => { context('when the employee has some pending salary', () => { - const owedSalary = salary * ONE_MONTH + const owedSalary = salary.mul(ONE_MONTH) beforeEach('accumulate some pending salary', async () => { await increaseTime(ONE_MONTH) }) context('when the requested amount is lower than the total owed salary', () => { - const requestedAmount = owedSalary - 10 + const requestedAmount = owedSalary.div(2) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.PAYROLL, requestedAmount, { from }), 'PAYROLL_NOTHING_PAID') @@ -453,7 +446,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { context('when the employee does not have pending salary', () => { context('when the requested amount is greater than zero', () => { - const requestedAmount = 100 + const requestedAmount = bigExp(100, 18) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.PAYROLL, requestedAmount, { from }), 'PAYROLL_NOTHING_PAID') @@ -461,7 +454,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.PAYROLL, requestedAmount, { from }), 'PAYROLL_NOTHING_PAID') @@ -486,7 +479,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { context('when exchange rates are expired', () => { beforeEach('expire exchange rates', async () => { const expiredTimestamp = (await payroll.getTimestampPublic()).sub(RATE_EXPIRATION_TIME + 1) - await setTokenRates(priceFeed, denominationToken, [anotherToken], [anotherTokenRate], expiredTimestamp) + await setTokenRates(priceFeed, USD, [DAI, ANT], [DAI_RATE, ANT_RATE], expiredTimestamp) }) itReverts(requestedAmount, expiredRatesReason) @@ -528,7 +521,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { } context('when the employee has a zero salary', () => { - const salary = 0 + const salary = bn(0) beforeEach('add employee', async () => { const receipt = await payroll.addEmployee(employee, salary, 'Boss', await payroll.getTimestampPublic()) @@ -542,13 +535,13 @@ contract('Payroll payday', ([owner, employee, anyone]) => { }) context('when the requested amount is greater than zero', () => { - const requestedAmount = 10000 + const requestedAmount = bigExp(10000, 18) itRevertsToWithdrawPartialPayroll(requestedAmount, 'PAYROLL_NOTHING_PAID', 'PAYROLL_NOTHING_PAID') }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) itRevertsToWithdrawPartialPayroll(requestedAmount, 'PAYROLL_NOTHING_PAID', 'PAYROLL_NOTHING_PAID') }) @@ -556,13 +549,13 @@ contract('Payroll payday', ([owner, employee, anyone]) => { context('when the employee does not have pending salary', () => { context('when the requested amount is greater than zero', () => { - const requestedAmount = 1000 + const requestedAmount = bigExp(1000, 18) itRevertsToWithdrawPartialPayroll(requestedAmount, 'PAYROLL_NOTHING_PAID', 'PAYROLL_NOTHING_PAID') }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) itRevertsToWithdrawPartialPayroll(requestedAmount, 'PAYROLL_NOTHING_PAID', 'PAYROLL_NOTHING_PAID') }) @@ -570,13 +563,13 @@ contract('Payroll payday', ([owner, employee, anyone]) => { } context('when the employee has already set some token allocations', () => { - const denominationTokenAllocation = 80 - const anotherTokenAllocation = 20 + const allocationDAI = 80 + const allocationANT = 20 beforeEach('set tokens allocation', async () => { - await payroll.addAllowedToken(anotherToken.address, {from: owner}) - await payroll.addAllowedToken(denominationToken.address, {from: owner}) - await payroll.determineAllocation([denominationToken.address, anotherToken.address], [denominationTokenAllocation, anotherTokenAllocation], {from}) + await payroll.addAllowedToken(ANT.address, { from: owner }) + await payroll.addAllowedToken(DAI.address, { from: owner }) + await payroll.determineAllocation([DAI.address, ANT.address], [allocationDAI, allocationANT], { from }) }) itRevertsAnyAttemptToWithdrawPartialPayroll() @@ -588,7 +581,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { }) context('when the employee has a huge salary', () => { - const salary = maxUint256() + const salary = MAX_UINT256 beforeEach('add employee', async () => { const receipt = await payroll.addEmployee(employee, salary, 'Boss', await payroll.getTimestampPublic()) @@ -596,48 +589,48 @@ contract('Payroll payday', ([owner, employee, anyone]) => { }) context('when the employee has already set some token allocations', () => { - const denominationTokenAllocation = 80 - const anotherTokenAllocation = 20 + const allocationDAI = 80 + const allocationANT = 20 beforeEach('set tokens allocation', async () => { - await payroll.addAllowedToken(anotherToken.address, {from: owner}) - await payroll.addAllowedToken(denominationToken.address, {from: owner}) - await payroll.determineAllocation([denominationToken.address, anotherToken.address], [denominationTokenAllocation, anotherTokenAllocation], {from}) + await payroll.addAllowedToken(ANT.address, { from: owner }) + await payroll.addAllowedToken(DAI.address, { from: owner }) + await payroll.determineAllocation([DAI.address, ANT.address], [allocationDAI, allocationANT], { from }) }) context('when the employee has some pending salary', () => { - const owedSalary = maxUint256() + const owedSalary = MAX_UINT256 beforeEach('accumulate some pending salary', async () => { await increaseTime(ONE_MONTH) }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) itRevertsToWithdrawPartialPayroll(requestedAmount, 'MATH_MUL_OVERFLOW', 'PAYROLL_EXCHANGE_RATE_ZERO') }) context('when the requested amount is lower than the total owed salary', () => { - const requestedAmount = 10000 + const requestedAmount = bigExp(1000, 18) const assertTransferredAmounts = requestedAmount => { - const requestedDenominationTokenAmount = Math.round(requestedAmount * denominationTokenAllocation / 100) - const requestedAnotherTokenAmount = Math.round(requestedAmount * anotherTokenAllocation / 100) + const requestedDAI = exchangedAmount(requestedAmount, DAI_RATE, allocationDAI) + const requestedANT = exchangedAmount(requestedAmount, ANT_RATE, allocationANT) it('transfers the requested salary amount', async () => { - const previousDenominationTokenBalance = await denominationToken.balanceOf(employee) - const previousAnotherTokenBalance = await anotherToken.balanceOf(employee) + const previousDAI = await DAI.balanceOf(employee) + const previousANT = await ANT.balanceOf(employee) await payroll.payday(PAYMENT_TYPES.PAYROLL, requestedAmount, { from }) - const currentDenominationTokenBalance = await denominationToken.balanceOf(employee) - const expectedDenominationTokenBalance = previousDenominationTokenBalance.plus(requestedDenominationTokenAmount); - assert.equal(currentDenominationTokenBalance.toString(), expectedDenominationTokenBalance.toString(), 'current denomination token balance does not match') + const currentDAI = await DAI.balanceOf(employee) + const expectedDAI = previousDAI.plus(requestedDAI) + assert.equal(currentDAI.toString(), expectedDAI.toString(), 'current DAI balance does not match') - const currentAnotherTokenBalance = await anotherToken.balanceOf(employee) - const expectedAnotherTokenBalance = anotherTokenRate.mul(requestedAnotherTokenAmount).plus(previousAnotherTokenBalance).trunc() - assert.equal(currentAnotherTokenBalance.toString(), expectedAnotherTokenBalance.toString(), 'current token balance does not match') + const currentANT = await ANT.balanceOf(employee) + const expectedANT = previousANT.plus(requestedANT) + assert.equal(currentANT.toString(), expectedANT.toString(), 'current ANT balance does not match') }) it('emits one event per allocated token', async () => { @@ -646,39 +639,39 @@ contract('Payroll payday', ([owner, employee, anyone]) => { const events = receipt.logs.filter(l => l.event === 'SendPayment') assert.equal(events.length, 2, 'should have emitted two events') - const denominationTokenEvent = events.find(e => e.args.token === denominationToken.address).args - assert.equal(denominationTokenEvent.employee, employee, 'employee address does not match') - assert.equal(denominationTokenEvent.token, denominationToken.address, 'denomination token address does not match') - assert.equal(denominationTokenEvent.amount.toString(), requestedDenominationTokenAmount, 'payment amount does not match') - assert.equal(denominationTokenEvent.paymentReference, 'Payroll', 'payment reference does not match') - - const anotherTokenEvent = events.find(e => e.args.token === anotherToken.address).args - assert.equal(anotherTokenEvent.employee, employee, 'employee address does not match') - assert.equal(anotherTokenEvent.token, anotherToken.address, 'token address does not match') - assert.equal(anotherTokenEvent.amount.div(anotherTokenRate).trunc().toString(), requestedAnotherTokenAmount, 'payment amount does not match') - assert.equal(anotherTokenEvent.paymentReference, 'Payroll', 'payment reference does not match') + const eventDAI = events.find(e => e.args.token === DAI.address).args + assert.equal(eventDAI.employee, employee, 'employee address does not match') + assert.equal(eventDAI.token, DAI.address, 'DAI address does not match') + assert.equal(eventDAI.amount.toString(), requestedDAI, 'payment amount does not match') + assert.equal(eventDAI.paymentReference, 'Payroll', 'payment reference does not match') + + const eventANT = events.find(e => e.args.token === ANT.address).args + assert.equal(eventANT.employee, employee, 'employee address does not match') + assert.equal(eventANT.token, ANT.address, 'ANT address does not match') + assert.equal(eventANT.amount.toString(), requestedANT, 'payment amount does not match') + assert.equal(eventANT.paymentReference, 'Payroll', 'payment reference does not match') }) it('can be called multiple times between periods of time', async () => { // terminate employee in the future to ensure we can request payroll multiple times await payroll.terminateEmployee(employeeId, NOW + TWO_MONTHS + TWO_MONTHS, { from: owner }) - const previousDenominationTokenBalance = await denominationToken.balanceOf(employee) - const previousAnotherTokenBalance = await anotherToken.balanceOf(employee) + const previousDAI = await DAI.balanceOf(employee) + const previousANT = await ANT.balanceOf(employee) await payroll.payday(PAYMENT_TYPES.PAYROLL, requestedAmount, { from }) await increaseTime(ONE_MONTH) - await setTokenRates(priceFeed, denominationToken, [anotherToken], [anotherTokenRate]) + await setTokenRates(priceFeed, USD, [DAI, ANT], [DAI_RATE, ANT_RATE]) await payroll.payday(PAYMENT_TYPES.PAYROLL, requestedAmount, { from }) - const currentDenominationTokenBalance = await denominationToken.balanceOf(employee) - const expectedDenominationTokenBalance = previousDenominationTokenBalance.plus(requestedDenominationTokenAmount * 2) - assert.equal(currentDenominationTokenBalance.toString(), expectedDenominationTokenBalance.toString(), 'current denomination token balance does not match') + const currentDAI = await DAI.balanceOf(employee) + const expectedDAI = previousDAI.plus(requestedDAI.mul(2)) + assert.equal(currentDAI.toString(), expectedDAI.toString(), 'current DAI balance does not match') - const currentAnotherTokenBalance = await anotherToken.balanceOf(employee) - const expectedAnotherTokenBalance = anotherTokenRate.mul(requestedAnotherTokenAmount * 2).plus(previousAnotherTokenBalance) - assert.equal(currentAnotherTokenBalance.toString(), expectedAnotherTokenBalance.toString(), 'current token balance does not match') + const currentANT = await ANT.balanceOf(employee) + const expectedANT = previousANT.plus(requestedANT.mul(2)) + assert.equal(currentANT.toString(), expectedANT.toString(), 'current ANT balance does not match') }) } @@ -713,7 +706,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { context('when exchange rates are expired', () => { beforeEach('expire exchange rates', async () => { const expiredTimestamp = (await payroll.getTimestampPublic()).sub(RATE_EXPIRATION_TIME + 1) - await setTokenRates(priceFeed, denominationToken, [anotherToken], [anotherTokenRate], expiredTimestamp) + await setTokenRates(priceFeed, USD, [DAI, ANT], [DAI_RATE, ANT_RATE], expiredTimestamp) }) it('reverts', async () => { @@ -724,7 +717,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { context('when the employee has some pending reimbursements', () => { beforeEach('add reimbursement', async () => { - await payroll.addReimbursement(employeeId, 1000, { from: owner }) + await payroll.addReimbursement(employeeId, bigExp(1000, 18), { from: owner }) }) context('when the employee is not terminated', () => { @@ -764,13 +757,13 @@ contract('Payroll payday', ([owner, employee, anyone]) => { context('when the employee does not have pending salary', () => { context('when the requested amount is greater than zero', () => { - const requestedAmount = 100 + const requestedAmount = bigExp(100, 18) itRevertsToWithdrawPartialPayroll(requestedAmount, 'PAYROLL_NOTHING_PAID', 'PAYROLL_NOTHING_PAID') }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) itRevertsToWithdrawPartialPayroll(requestedAmount, 'PAYROLL_NOTHING_PAID', 'PAYROLL_NOTHING_PAID') }) @@ -779,20 +772,20 @@ contract('Payroll payday', ([owner, employee, anyone]) => { context('when the employee did not set any token allocations yet', () => { context('when the employee has some pending salary', () => { - const owedSalary = maxUint256() + const owedSalary = MAX_UINT256 beforeEach('accumulate some pending salary', async () => { await increaseTime(ONE_MONTH) }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) itRevertsToWithdrawPartialPayroll(requestedAmount, 'PAYROLL_NOTHING_PAID', 'PAYROLL_NOTHING_PAID') }) context('when the requested amount is lower than the total owed salary', () => { - const requestedAmount = 10000 + const requestedAmount = bigExp(1000, 18) itRevertsToWithdrawPartialPayroll(requestedAmount, 'PAYROLL_NOTHING_PAID', 'PAYROLL_NOTHING_PAID') }) @@ -806,13 +799,13 @@ contract('Payroll payday', ([owner, employee, anyone]) => { context('when the employee does not have pending salary', () => { context('when the requested amount is greater than zero', () => { - const requestedAmount = 100 + const requestedAmount = bigExp(1000, 18) itRevertsToWithdrawPartialPayroll(requestedAmount, 'PAYROLL_NOTHING_PAID', 'PAYROLL_NOTHING_PAID') }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) itRevertsToWithdrawPartialPayroll(requestedAmount, 'PAYROLL_NOTHING_PAID', 'PAYROLL_NOTHING_PAID') }) @@ -826,7 +819,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { const from = anyone context('when the requested amount is greater than zero', () => { - const requestedAmount = 100 + const requestedAmount = bigExp(1000, 18) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.PAYROLL, requestedAmount, { from }), 'PAYROLL_EMPLOYEE_DOES_NOT_MATCH') @@ -834,7 +827,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.PAYROLL, requestedAmount, { from }), 'PAYROLL_EMPLOYEE_DOES_NOT_MATCH') @@ -844,7 +837,7 @@ contract('Payroll payday', ([owner, employee, anyone]) => { }) context('when it has not been initialized yet', function () { - const requestedAmount = 0 + const requestedAmount = bn(0) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.PAYROLL, requestedAmount, { from: employee }), 'PAYROLL_EMPLOYEE_DOES_NOT_MATCH') diff --git a/future-apps/payroll/test/contracts/Payroll_rates.test.js b/future-apps/payroll/test/contracts/Payroll_rates.test.js new file mode 100644 index 0000000000..c5290556f1 --- /dev/null +++ b/future-apps/payroll/test/contracts/Payroll_rates.test.js @@ -0,0 +1,468 @@ +const PAYMENT_TYPES = require('../helpers/payment_types') +const { bigExp } = require('../helpers/numbers')(web3) +const { NOW, TWO_MINUTES, RATE_EXPIRATION_TIME } = require('../helpers/time') +const { deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy')(artifacts, web3) +const { USD, ETH, ETH_RATE, deployDAI, DAI_RATE, deployANT, ANT_RATE, formatRate, setTokenRate } = require('../helpers/tokens')(artifacts, web3) + +contract('Payroll rates handling,', ([owner, employee, anyone]) => { + let dao, payroll, payrollBase, finance, vault, priceFeed, DAI, ANT + + const increaseTime = async seconds => { + await payroll.mockIncreaseTime(seconds) + await priceFeed.mockIncreaseTime(seconds) + } + + before('deploy contracts and tokens', async () => { + ({ dao, finance, vault, payrollBase } = await deployContracts(owner)) + + DAI = await deployDAI(owner, finance) + ANT = await deployANT(owner, finance) + await finance.deposit(ETH, bigExp(50, 18), 'Initial ETH deposit', { from: anyone, value: bigExp(50, 18) }) + }) + + context('when the denomination token is USD', async () => { + beforeEach('initialize payroll app', async () => { + ({ payroll, priceFeed } = await createPayrollAndPriceFeed(dao, payrollBase, owner, NOW)) + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + }) + + beforeEach('set rates and allow tokens', async () => { + await setTokenRate(priceFeed, USD, ETH, ETH_RATE) + await setTokenRate(priceFeed, USD, DAI, DAI_RATE) + await setTokenRate(priceFeed, USD, ANT, ANT_RATE) + + await payroll.addAllowedToken(ETH, { from: owner }) + await payroll.addAllowedToken(DAI.address, { from: owner }) + await payroll.addAllowedToken(ANT.address, { from: owner }) + }) + + beforeEach('add employee with salary 1 USD per second', async () => { + const salary = bigExp(1, 18) + await payroll.addEmployee(employee, salary, 'Boss', await payroll.getTimestampPublic()) + }) + + beforeEach('accrue two minutes of salary', async () => { + await increaseTime(TWO_MINUTES) + }) + + context('when the employee requests only ETH', () => { + beforeEach('set token allocation', async () => { + await payroll.determineAllocation([ETH], [100], { from: employee }) + }) + + it('receives the expected amount of ETH', async () => { + const previousETH = await web3.eth.getBalance(employee) + const previousDAI = await DAI.balanceOf(employee) + const previousANT = await ANT.balanceOf(employee) + + const { tx, receipt } = await payroll.payday(PAYMENT_TYPES.PAYROLL, 0, { from: employee }) + const { gasPrice } = await web3.eth.getTransaction(tx) + const txCost = gasPrice.mul(receipt.gasUsed) + + // expected an income of 6 ETH since we accrued 2 minutes of salary at 1 USD per second, and the ETH rate is 20 USD + const currentETH = await web3.eth.getBalance(employee) + assert.equal(currentETH.minus(previousETH).plus(txCost).toString(), bigExp(6, 18).toString(), 'expected current ETH amount does not match') + + // no DAI income expected + const currentDAI = await DAI.balanceOf(employee) + assert.equal(currentDAI.toString(), previousDAI.toString(), 'expected current DAI amount does not match') + + // no ANT income expected + const currentANT = await ANT.balanceOf(employee) + assert.equal(currentANT.toString(), previousANT.toString(), 'expected current ANT amount does not match') + }) + }) + + context('when the employee requests only ANT', () => { + beforeEach('set token allocation', async () => { + await payroll.determineAllocation([ANT.address], [100], { from: employee }) + }) + + it('receives the expected amount of ANT', async () => { + const previousETH = await web3.eth.getBalance(employee) + const previousDAI = await DAI.balanceOf(employee) + const previousANT = await ANT.balanceOf(employee) + + const { tx, receipt } = await payroll.payday(PAYMENT_TYPES.PAYROLL, 0, { from: employee }) + const { gasPrice } = await web3.eth.getTransaction(tx) + const txCost = gasPrice.mul(receipt.gasUsed) + + // no ETH income expected + const currentETH = await web3.eth.getBalance(employee) + assert.equal(currentETH.plus(txCost).toString(), previousETH.toString(), 'expected current ETH amount does not match') + + // no DAI income expected + const currentDAI = await DAI.balanceOf(employee) + assert.equal(currentDAI.toString(), previousDAI.toString(), 'expected current DAI amount does not match') + + // expected an income of 240 ANT since we accrued 2 minutes of salary at 1 USD per second, and the ANT rate is 0.5 USD + const currentANT = await ANT.balanceOf(employee) + assert.equal(currentANT.minus(previousANT).toString(), bigExp(240, 18).toString(), 'expected current ANT amount does not match') + }) + }) + + context('when the employee requests multiple tokens', () => { + beforeEach('set token allocations', async () => { + await payroll.determineAllocation([ETH, DAI.address, ANT.address], [50, 25, 25], { from: employee }) + }) + + it('receives the expected amount of tokens', async () => { + const previousETH = await web3.eth.getBalance(employee) + const previousDAI = await DAI.balanceOf(employee) + const previousANT = await ANT.balanceOf(employee) + + const { tx, receipt } = await payroll.payday(PAYMENT_TYPES.PAYROLL, 0, { from: employee }) + const { gasPrice } = await web3.eth.getTransaction(tx) + const txCost = gasPrice.mul(receipt.gasUsed) + + // expected an income of 3 ETH having 50% allocated, since we accrued 2 minutes of salary at 1 USD per second, and the ETH rate is 20 USD + const currentETH = await web3.eth.getBalance(employee) + assert.equal(currentETH.minus(previousETH).plus(txCost).toString(), bigExp(3, 18).toString(), 'expected current ETH amount does not match') + + // expected an income of 30 DAI having 25% allocated, since we accrued 2 minutes of salary at 1 USD per second, and the DAI rate is 1 USD + const currentDAI = await DAI.balanceOf(employee) + assert.equal(currentDAI.minus(previousDAI).toString(), bigExp(30, 18).toString(), 'expected current DAI amount does not match') + + // expected an income of 60 ANT having 25% allocated, since we accrued 2 minutes of salary at 1 USD per second, and the ANT rate is 0.5 USD + const currentANT = await ANT.balanceOf(employee) + assert.equal(currentANT.minus(previousANT).toString(), bigExp(60, 18).toString(), 'expected current ANT amount does not match') + }) + }) + }) + + context('when the denomination token is ETH', async () => { + beforeEach('initialize payroll app', async () => { + ({ payroll, priceFeed } = await createPayrollAndPriceFeed(dao, payrollBase, owner, NOW)) + await payroll.initialize(finance.address, ETH, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + }) + + beforeEach('set rates and allow tokens', async () => { + const ETH_TO_DAI_RATE = formatRate(ETH_RATE.div(DAI_RATE)) // 20 DAI + const ETH_TO_ANT_RATE = formatRate(ETH_RATE.div(ANT_RATE)) // 40 ANT + + await setTokenRate(priceFeed, DAI, ETH, ETH_TO_DAI_RATE) + await setTokenRate(priceFeed, ANT, ETH, ETH_TO_ANT_RATE) + + await payroll.addAllowedToken(ETH, { from: owner }) + await payroll.addAllowedToken(DAI.address, { from: owner }) + await payroll.addAllowedToken(ANT.address, { from: owner }) + }) + + beforeEach('add employee with salary 0.1 ETH per second', async () => { + const salary = bigExp(0.1, 18) + await payroll.addEmployee(employee, salary, 'Boss', await payroll.getTimestampPublic()) + }) + + beforeEach('accrue two minutes of salary', async () => { + await increaseTime(TWO_MINUTES) + }) + + context('when the employee requests only ETH', () => { + beforeEach('set token allocation', async () => { + await payroll.determineAllocation([ETH], [100], { from: employee }) + }) + + it('receives the expected amount of ETH', async () => { + const previousETH = await web3.eth.getBalance(employee) + const previousDAI = await DAI.balanceOf(employee) + const previousANT = await ANT.balanceOf(employee) + + const { tx, receipt } = await payroll.payday(PAYMENT_TYPES.PAYROLL, 0, { from: employee }) + const { gasPrice } = await web3.eth.getTransaction(tx) + const txCost = gasPrice.mul(receipt.gasUsed) + + // expected an income of 12 ETH since we accrued 2 minutes of salary at 0.1 ETH per second, and the denomination token is ETH + const currentETH = await web3.eth.getBalance(employee) + assert.equal(currentETH.minus(previousETH).plus(txCost).toString(), bigExp(12, 18).toString(), 'expected current ETH amount does not match') + + // no DAI income expected + const currentDAI = await DAI.balanceOf(employee) + assert.equal(currentDAI.toString(), previousDAI.toString(), 'expected current DAI amount does not match') + + // no ANT income expected + const currentANT = await ANT.balanceOf(employee) + assert.equal(currentANT.toString(), previousANT.toString(), 'expected current ANT amount does not match') + }) + }) + + context('when the employee requests only ANT', () => { + beforeEach('set token allocation', async () => { + await payroll.determineAllocation([ANT.address], [100], { from: employee }) + }) + + it('receives the expected amount of ANT', async () => { + const previousETH = await web3.eth.getBalance(employee) + const previousDAI = await DAI.balanceOf(employee) + const previousANT = await ANT.balanceOf(employee) + + const { tx, receipt } = await payroll.payday(PAYMENT_TYPES.PAYROLL, 0, { from: employee }) + const { gasPrice } = await web3.eth.getTransaction(tx) + const txCost = gasPrice.mul(receipt.gasUsed) + + // no ETH income expected + const currentETH = await web3.eth.getBalance(employee) + assert.equal(currentETH.plus(txCost).toString(), previousETH.toString(), 'expected current ETH amount does not match') + + // no DAI income expected + const currentDAI = await DAI.balanceOf(employee) + assert.equal(currentDAI.toString(), previousDAI.toString(), 'expected current DAI amount does not match') + + // expected an income of 480 ANT since we accrued 2 minutes of salary at 0.1 ETH per second, and the ANT rate is 0.025 ETH + const currentANT = await ANT.balanceOf(employee) + assert.equal(currentANT.minus(previousANT).toString(), bigExp(480, 18).toString(), 'expected current ANT amount does not match') + }) + }) + + context('when the employee requests multiple tokens', () => { + beforeEach('set token allocations', async () => { + await payroll.determineAllocation([ETH, DAI.address, ANT.address], [50, 25, 25], { from: employee }) + }) + + it('receives the expected amount of tokens', async () => { + const previousETH = await web3.eth.getBalance(employee) + const previousDAI = await DAI.balanceOf(employee) + const previousANT = await ANT.balanceOf(employee) + + const { tx, receipt } = await payroll.payday(PAYMENT_TYPES.PAYROLL, 0, { from: employee }) + const { gasPrice } = await web3.eth.getTransaction(tx) + const txCost = gasPrice.mul(receipt.gasUsed) + + // expected an income of 6 ETH having 50% allocated, since we accrued 2 minutes of salary at 0.1 ETH per second, and the denomination token is ETH + const currentETH = await web3.eth.getBalance(employee) + assert.equal(currentETH.minus(previousETH).plus(txCost).toString(), bigExp(6, 18).toString(), 'expected current ETH amount does not match') + + // expected an income of 60 DAI having 25% allocated, since we accrued 2 minutes of salary at 0.1 ETH per second, and the DAI rate is 0.05 ETH + const currentDAI = await DAI.balanceOf(employee) + assert.equal(currentDAI.minus(previousDAI).toString(), bigExp(60, 18).toString(), 'expected current DAI amount does not match') + + // expected an income of 120 ANT having 25% allocated, since we accrued 2 minutes of salary at 0.1 ETH per second, and the ANT rate is 0.025 ETH + const currentANT = await ANT.balanceOf(employee) + assert.equal(currentANT.minus(previousANT).toString(), bigExp(120, 18).toString(), 'expected current ANT amount does not match') + }) + }) + }) + + context('when the denomination token is DAI', async () => { + beforeEach('initialize payroll app', async () => { + ({ payroll, priceFeed } = await createPayrollAndPriceFeed(dao, payrollBase, owner, NOW)) + await payroll.initialize(finance.address, DAI.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + }) + + beforeEach('set rates and allow tokens', async () => { + const ETH_TO_DAI_RATE = formatRate(ETH_RATE.div(DAI_RATE)) // 20 DAI + const ANT_TO_DAI_RATE = formatRate(ANT_RATE.div(DAI_RATE)) // 0.5 DAI + + await setTokenRate(priceFeed, DAI, ETH, ETH_TO_DAI_RATE) + await setTokenRate(priceFeed, DAI, ANT, ANT_TO_DAI_RATE) + + await payroll.addAllowedToken(ETH, { from: owner }) + await payroll.addAllowedToken(DAI.address, { from: owner }) + await payroll.addAllowedToken(ANT.address, { from: owner }) + }) + + beforeEach('add employee with salary 1 DAI per second', async () => { + const salary = bigExp(1, 18) + await payroll.addEmployee(employee, salary, 'Boss', await payroll.getTimestampPublic()) + }) + + beforeEach('accrue two minutes of salary', async () => { + await increaseTime(TWO_MINUTES) + }) + + context('when the employee requests only ETH', () => { + beforeEach('set token allocation', async () => { + await payroll.determineAllocation([ETH], [100], { from: employee }) + }) + + it('receives the expected amount of ETH', async () => { + const previousETH = await web3.eth.getBalance(employee) + const previousDAI = await DAI.balanceOf(employee) + const previousANT = await ANT.balanceOf(employee) + + const { tx, receipt } = await payroll.payday(PAYMENT_TYPES.PAYROLL, 0, { from: employee }) + const { gasPrice } = await web3.eth.getTransaction(tx) + const txCost = gasPrice.mul(receipt.gasUsed) + + // expected an income of 6 ETH since we accrued 2 minutes of salary at 1 DAI per second, and the ETH rate is 20 DAI + const currentETH = await web3.eth.getBalance(employee) + assert.equal(currentETH.minus(previousETH).plus(txCost).toString(), bigExp(6, 18).toString(), 'expected current ETH amount does not match') + + // no DAI income expected + const currentDAI = await DAI.balanceOf(employee) + assert.equal(currentDAI.toString(), previousDAI.toString(), 'expected current DAI amount does not match') + + // no ANT income expected + const currentANT = await ANT.balanceOf(employee) + assert.equal(currentANT.toString(), previousANT.toString(), 'expected current ANT amount does not match') + }) + }) + + context('when the employee requests only ANT', () => { + beforeEach('set token allocation', async () => { + await payroll.determineAllocation([ANT.address], [100], { from: employee }) + }) + + it('receives the expected amount of ANT', async () => { + const previousETH = await web3.eth.getBalance(employee) + const previousDAI = await DAI.balanceOf(employee) + const previousANT = await ANT.balanceOf(employee) + + const { tx, receipt } = await payroll.payday(PAYMENT_TYPES.PAYROLL, 0, { from: employee }) + const { gasPrice } = await web3.eth.getTransaction(tx) + const txCost = gasPrice.mul(receipt.gasUsed) + + // no ETH income expected + const currentETH = await web3.eth.getBalance(employee) + assert.equal(currentETH.plus(txCost).toString(), previousETH.toString(), 'expected current ETH amount does not match') + + // no DAI income expected + const currentDAI = await DAI.balanceOf(employee) + assert.equal(currentDAI.toString(), previousDAI.toString(), 'expected current DAI amount does not match') + + // expected an income of 240 ANT since we accrued 2 minutes of salary at 1 DAI per second, and the ANT rate is 0.5 DAI + const currentANT = await ANT.balanceOf(employee) + assert.equal(currentANT.minus(previousANT).toString(), bigExp(240, 18).toString(), 'expected current ANT amount does not match') + }) + }) + + context('when the employee requests multiple tokens', () => { + beforeEach('set token allocations', async () => { + await payroll.determineAllocation([ETH, DAI.address, ANT.address], [50, 25, 25], { from: employee }) + }) + + it('receives the expected amount of tokens', async () => { + const previousETH = await web3.eth.getBalance(employee) + const previousDAI = await DAI.balanceOf(employee) + const previousANT = await ANT.balanceOf(employee) + + const { tx, receipt } = await payroll.payday(PAYMENT_TYPES.PAYROLL, 0, { from: employee }) + const { gasPrice } = await web3.eth.getTransaction(tx) + const txCost = gasPrice.mul(receipt.gasUsed) + + // expected an income of 3 ETH having 50% allocated, since we accrued 2 minutes of salary at 1 DAI per second, and the ETH rate is 20 DAI + const currentETH = await web3.eth.getBalance(employee) + assert.equal(currentETH.minus(previousETH).plus(txCost).toString(), bigExp(3, 18).toString(), 'expected current ETH amount does not match') + + // expected an income of 30 DAI having 25% allocated, since we accrued 2 minutes of salary at 1 DAI per second, and the denomination token is DAI + const currentDAI = await DAI.balanceOf(employee) + assert.equal(currentDAI.minus(previousDAI).toString(), bigExp(30, 18).toString(), 'expected current DAI amount does not match') + + // expected an income of 60 ANT having 25% allocated, since we accrued 2 minutes of salary at 1 DAI per second, and the ANT rate is 0.5 DAI + const currentANT = await ANT.balanceOf(employee) + assert.equal(currentANT.minus(previousANT).toString(), bigExp(60, 18).toString(), 'expected current ANT amount does not match') + }) + }) + }) + + context('when the denomination token is ANT', async () => { + beforeEach('initialize payroll app', async () => { + ({ payroll, priceFeed } = await createPayrollAndPriceFeed(dao, payrollBase, owner, NOW)) + await payroll.initialize(finance.address, ANT.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + }) + + beforeEach('set rates and allow tokens', async () => { + const ETH_TO_ANT_RATE = formatRate(ETH_RATE.div(ANT_RATE)) // 40 ANT + const DAI_TO_ANT_RATE = formatRate(DAI_RATE.div(ANT_RATE)) // 2 ANT + + await setTokenRate(priceFeed, ANT, ETH, ETH_TO_ANT_RATE) + await setTokenRate(priceFeed, ANT, DAI, DAI_TO_ANT_RATE) + + await payroll.addAllowedToken(ETH, { from: owner }) + await payroll.addAllowedToken(DAI.address, { from: owner }) + await payroll.addAllowedToken(ANT.address, { from: owner }) + }) + + beforeEach('add employee with salary 1 ANT per second', async () => { + const salary = bigExp(1, 18) + await payroll.addEmployee(employee, salary, 'Boss', await payroll.getTimestampPublic()) + }) + + beforeEach('accrue two minutes of salary', async () => { + await increaseTime(TWO_MINUTES) + }) + + context('when the employee requests only ETH', () => { + beforeEach('set token allocation', async () => { + await payroll.determineAllocation([ETH], [100], { from: employee }) + }) + + it('receives the expected amount of ETH', async () => { + const previousETH = await web3.eth.getBalance(employee) + const previousDAI = await DAI.balanceOf(employee) + const previousANT = await ANT.balanceOf(employee) + + const { tx, receipt } = await payroll.payday(PAYMENT_TYPES.PAYROLL, 0, { from: employee }) + const { gasPrice } = await web3.eth.getTransaction(tx) + const txCost = gasPrice.mul(receipt.gasUsed) + + // expected an income of 3 ETH since we accrued 2 minutes of salary at 1 ANT per second, and the ETH rate is 40 ANT + const currentETH = await web3.eth.getBalance(employee) + assert.equal(currentETH.minus(previousETH).plus(txCost).toString(), bigExp(3, 18).toString(), 'expected current ETH amount does not match') + + // no DAI income expected + const currentDAI = await DAI.balanceOf(employee) + assert.equal(currentDAI.toString(), previousDAI.toString(), 'expected current DAI amount does not match') + + // no ANT income expected + const currentANT = await ANT.balanceOf(employee) + assert.equal(currentANT.toString(), previousANT.toString(), 'expected current ANT amount does not match') + }) + }) + + context('when the employee requests only ANT', () => { + beforeEach('set token allocation', async () => { + await payroll.determineAllocation([ANT.address], [100], { from: employee }) + }) + + it('receives the expected amount of ANT', async () => { + const previousETH = await web3.eth.getBalance(employee) + const previousDAI = await DAI.balanceOf(employee) + const previousANT = await ANT.balanceOf(employee) + + const { tx, receipt } = await payroll.payday(PAYMENT_TYPES.PAYROLL, 0, { from: employee }) + const { gasPrice } = await web3.eth.getTransaction(tx) + const txCost = gasPrice.mul(receipt.gasUsed) + + // no ETH income expected + const currentETH = await web3.eth.getBalance(employee) + assert.equal(currentETH.plus(txCost).toString(), previousETH.toString(), 'expected current ETH amount does not match') + + // no DAI income expected + const currentDAI = await DAI.balanceOf(employee) + assert.equal(currentDAI.toString(), previousDAI.toString(), 'expected current DAI amount does not match') + + // expected an income of 120 ANT since we accrued 2 minutes of salary at 1 ANT per second, and the denomination token is ANT + const currentANT = await ANT.balanceOf(employee) + assert.equal(currentANT.minus(previousANT).toString(), bigExp(120, 18).toString(), 'expected current ANT amount does not match') + }) + }) + + context('when the employee requests multiple tokens', () => { + beforeEach('set token allocations', async () => { + await payroll.determineAllocation([ETH, DAI.address, ANT.address], [50, 25, 25], { from: employee }) + }) + + it('receives the expected amount of tokens', async () => { + const previousETH = await web3.eth.getBalance(employee) + const previousDAI = await DAI.balanceOf(employee) + const previousANT = await ANT.balanceOf(employee) + + const { tx, receipt } = await payroll.payday(PAYMENT_TYPES.PAYROLL, 0, { from: employee }) + const { gasPrice } = await web3.eth.getTransaction(tx) + const txCost = gasPrice.mul(receipt.gasUsed) + + // expected an income of 1.5 ETH having 50% allocated, since we accrued 2 minutes of salary at 1 ANT per second, and the ETH rate is 40 ANT + const currentETH = await web3.eth.getBalance(employee) + assert.equal(currentETH.minus(previousETH).plus(txCost).toString(), bigExp(1.5, 18).toString(), 'expected current ETH amount does not match') + + // expected an income of 15 DAI having 25% allocated, since we accrued 2 minutes of salary at 1 ANT per second, and the DAI rate is 2 ANT + const currentDAI = await DAI.balanceOf(employee) + assert.equal(currentDAI.minus(previousDAI).toString(), bigExp(15, 18).toString(), 'expected current DAI amount does not match') + + // expected an income of 30 ANT having 25% allocated, since we accrued 2 minutes of salary at 1 ANT per second, and the denomination token is ANT + const currentANT = await ANT.balanceOf(employee) + assert.equal(currentANT.minus(previousANT).toString(), bigExp(30, 18).toString(), 'expected current ANT amount does not match') + }) + }) + }) +}) diff --git a/future-apps/payroll/test/contracts/Payroll_reentrancy.test.js b/future-apps/payroll/test/contracts/Payroll_reentrancy.test.js index b210c5dd50..dbf6036a2b 100644 --- a/future-apps/payroll/test/contracts/Payroll_reentrancy.test.js +++ b/future-apps/payroll/test/contracts/Payroll_reentrancy.test.js @@ -1,6 +1,7 @@ -const { bigExp } = require('../helpers/numbers')(web3) const { assertRevert } = require('@aragon/test-helpers/assertThrow') -const { deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy.js')(artifacts, web3) +const { bigExp, annualSalaryPerSecond } = require('../helpers/numbers')(web3) +const { NOW, ONE_MONTH, RATE_EXPIRATION_TIME } = require('../helpers/time') +const { deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy')(artifacts, web3) const MaliciousERC20 = artifacts.require('MaliciousERC20') const MaliciousEmployee = artifacts.require('MaliciousEmployee') @@ -8,11 +9,6 @@ const MaliciousEmployee = artifacts.require('MaliciousEmployee') contract('Payroll reentrancy guards', ([owner]) => { let dao, payroll, payrollBase, finance, vault, priceFeed, maliciousToken, employee - const NOW = 1553703809 // random fixed timestamp in seconds - const ONE_MONTH = 60 * 60 * 24 * 31 - const TWO_MONTHS = ONE_MONTH * 2 - const RATE_EXPIRATION_TIME = TWO_MONTHS - const REENTRANCY_ACTIONS = { PAYDAY: 0, CHANGE_ADDRESS: 1, SET_ALLOCATION: 2 } const increaseTime = async seconds => { @@ -43,7 +39,7 @@ contract('Payroll reentrancy guards', ([owner]) => { beforeEach('add malicious employee, set tokens allocations, and accrue some salary', async () => { await employee.setPayroll(payroll.address) - await payroll.addEmployee(employee.address, 1, 'Malicious Boss', await payroll.getTimestampPublic(), { from: owner }) + await payroll.addEmployee(employee.address, annualSalaryPerSecond(100000), 'Malicious Boss', await payroll.getTimestampPublic(), { from: owner }) await employee.determineAllocation([maliciousToken.address], [100]) await increaseTime(ONE_MONTH) diff --git a/future-apps/payroll/test/contracts/Payroll_reimbursements.test.js b/future-apps/payroll/test/contracts/Payroll_reimbursements.test.js index 0674e39709..4f6321b0b0 100644 --- a/future-apps/payroll/test/contracts/Payroll_reimbursements.test.js +++ b/future-apps/payroll/test/contracts/Payroll_reimbursements.test.js @@ -1,19 +1,13 @@ const PAYMENT_TYPES = require('../helpers/payment_types') -const setTokenRates = require('../helpers/set_token_rates')(web3) const { assertRevert } = require('@aragon/test-helpers/assertThrow') -const { bn, maxUint256 } = require('../helpers/numbers')(web3) const { getEvents, getEventArgument } = require('../helpers/events') -const { deployErc20TokenAndDeposit, deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy.js')(artifacts, web3) +const { NOW, ONE_MONTH, RATE_EXPIRATION_TIME } = require('../helpers/time') +const { deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy')(artifacts, web3) +const { ONE, bn, bigExp, annualSalaryPerSecond, MAX_UINT256 } = require('../helpers/numbers')(web3) +const { USD, DAI_RATE, ANT_RATE, exchangedAmount, deployDAI, deployANT, setTokenRates } = require('../helpers/tokens')(artifacts, web3) contract('Payroll reimbursements', ([owner, employee, anyone]) => { - let dao, payroll, payrollBase, finance, vault, priceFeed, denominationToken, anotherToken, anotherTokenRate - - const NOW = 1553703809 // random fixed timestamp in seconds - const ONE_MONTH = 60 * 60 * 24 * 31 - const TWO_MONTHS = ONE_MONTH * 2 - const RATE_EXPIRATION_TIME = TWO_MONTHS - - const TOKEN_DECIMALS = 18 + let dao, payroll, payrollBase, finance, vault, priceFeed, DAI, ANT const increaseTime = async seconds => { await payroll.mockIncreaseTime(seconds) @@ -22,8 +16,8 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { before('deploy base apps and tokens', async () => { ({ dao, finance, vault, payrollBase } = await deployContracts(owner)) - anotherToken = await deployErc20TokenAndDeposit(owner, finance, 'Another token', TOKEN_DECIMALS) - denominationToken = await deployErc20TokenAndDeposit(owner, finance, 'Denomination Token', TOKEN_DECIMALS) + ANT = await deployANT(owner, finance) + DAI = await deployDAI(owner, finance) }) beforeEach('create payroll and price feed instance', async () => { @@ -33,7 +27,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { describe('addReimbursement', () => { context('when it has already been initialized', function () { beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) context('when the sender has permissions', () => { @@ -43,49 +37,49 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { let employeeId beforeEach('add employee', async () => { - const receipt = await payroll.addEmployee(employee, 1000, 'Boss', await payroll.getTimestampPublic()) + const receipt = await payroll.addEmployee(employee, annualSalaryPerSecond(100000), 'Boss', await payroll.getTimestampPublic()) employeeId = getEventArgument(receipt, 'AddEmployee', 'employeeId') }) context('when the given employee is active', () => { - const itAddsReimbursementsSuccessfully = value => { + const itAddsReimbursementsSuccessfully = reimburse => { it('adds requested reimbursement', async () => { - await payroll.addReimbursement(employeeId, value, { from }) + await payroll.addReimbursement(employeeId, reimburse, { from }) const reimbursements = (await payroll.getEmployee(employeeId))[3] - assert.equal(reimbursements, value, 'reimbursement does not match') + assert.equal(reimbursements.toString(), reimburse.toString(), 'reimbursement does not match') }) it('emits an event', async () => { - const receipt = await payroll.addReimbursement(employeeId, value, { from }) + const receipt = await payroll.addReimbursement(employeeId, reimburse, { from }) const events = getEvents(receipt, 'AddEmployeeReimbursement') assert.equal(events.length, 1, 'number of AddEmployeeReimbursement emitted events does not match') assert.equal(events[0].args.employeeId.toString(), employeeId, 'employee id does not match') - assert.equal(events[0].args.amount.toString(), value, 'reimbursement does not match') + assert.equal(events[0].args.amount.toString(), reimburse, 'reimbursement does not match') }) } - context('when the given value greater than zero', () => { - const value = 1000 + context('when the given reimbursement greater than zero', () => { + const reimbursement = bigExp(1000, 18) - itAddsReimbursementsSuccessfully(value) + itAddsReimbursementsSuccessfully(reimbursement) }) - context('when the given value is zero', () => { - const value = 0 + context('when the given reimbursement is zero', () => { + const reimbursement = bn(0) - itAddsReimbursementsSuccessfully(value) + itAddsReimbursementsSuccessfully(reimbursement) }) - context('when the given value way greater than zero', () => { - const value = maxUint256() + context('when the given reimbursement way greater than zero', () => { + const reimbursement = MAX_UINT256 it('reverts', async () => { await payroll.addReimbursement(employeeId, 1, { from }) - await assertRevert(payroll.addReimbursement(employeeId, value, { from }), 'MATH_ADD_OVERFLOW') + await assertRevert(payroll.addReimbursement(employeeId, reimbursement, { from }), 'MATH_ADD_OVERFLOW') }) }) }) @@ -97,7 +91,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { }) it('reverts', async () => { - await assertRevert(payroll.addReimbursement(employeeId, 1000, { from }), 'PAYROLL_NON_ACTIVE_EMPLOYEE') + await assertRevert(payroll.addReimbursement(employeeId, bigExp(1000, 18), { from }), 'PAYROLL_NON_ACTIVE_EMPLOYEE') }) }) }) @@ -106,28 +100,28 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { const employeeId = 0 it('reverts', async () => { - await assertRevert(payroll.addReimbursement(employeeId, 1000, { from }), 'PAYROLL_NON_ACTIVE_EMPLOYEE') + await assertRevert(payroll.addReimbursement(employeeId, bigExp(1000, 18), { from }), 'PAYROLL_NON_ACTIVE_EMPLOYEE') }) }) }) context('when the sender does not have permissions', () => { const from = anyone - const value = 1000 const employeeId = 0 + const reimbursement = bigExp(1000, 18) it('reverts', async () => { - await assertRevert(payroll.addReimbursement(employeeId, value, { from }), 'APP_AUTH_FAILED') + await assertRevert(payroll.addReimbursement(employeeId, reimbursement, { from }), 'APP_AUTH_FAILED') }) }) }) context('when it has not been initialized yet', function () { - const value = 10000 const employeeId = 0 + const reimbursement = bigExp(1000, 18) it('reverts', async () => { - await assertRevert(payroll.addReimbursement(employeeId, value, { from: owner }), 'APP_AUTH_FAILED') + await assertRevert(payroll.addReimbursement(employeeId, reimbursement, { from: owner }), 'APP_AUTH_FAILED') }) }) }) @@ -135,17 +129,16 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { describe('reimbursements payday', () => { context('when it has already been initialized', function () { beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) beforeEach('set token rates', async () => { - anotherTokenRate = bn(5) - await setTokenRates(priceFeed, denominationToken, [anotherToken], [anotherTokenRate]) + await setTokenRates(priceFeed, USD, [DAI, ANT], [DAI_RATE, ANT_RATE]) }) context('when the sender is an employee', () => { const from = employee - let employeeId, salary = 1000 + let employeeId, salary = annualSalaryPerSecond(1000) beforeEach('add employee and accumulate some salary', async () => { const receipt = await payroll.addEmployee(employee, salary, 'Boss', await payroll.getTimestampPublic()) @@ -155,40 +148,40 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { }) context('when the employee has already set some token allocations', () => { - const denominationTokenAllocation = 80 - const anotherTokenAllocation = 20 + const allocationDAI = 80 + const allocationANT = 20 beforeEach('set tokens allocation', async () => { - await payroll.addAllowedToken(anotherToken.address, { from: owner }) - await payroll.addAllowedToken(denominationToken.address, { from: owner }) - await payroll.determineAllocation([denominationToken.address, anotherToken.address], [denominationTokenAllocation, anotherTokenAllocation], { from }) + await payroll.addAllowedToken(ANT.address, { from: owner }) + await payroll.addAllowedToken(DAI.address, { from: owner }) + await payroll.determineAllocation([DAI.address, ANT.address], [allocationDAI, allocationANT], { from }) }) context('when the employee has some pending reimbursements', () => { - const reimbursement = 100 + const reimbursement = bigExp(100, 18) beforeEach('add reimbursement', async () => { - await payroll.addReimbursement(employeeId, reimbursement / 2, { from: owner }) - await payroll.addReimbursement(employeeId, reimbursement / 2, { from: owner }) + await payroll.addReimbursement(employeeId, reimbursement.div(2), { from: owner }) + await payroll.addReimbursement(employeeId, reimbursement.div(2), { from: owner }) }) const assertTransferredAmounts = (requestedAmount, expectedRequestedAmount = requestedAmount) => { - const requestedDenominationTokenAmount = parseInt(expectedRequestedAmount * denominationTokenAllocation / 100) - const requestedAnotherTokenAmount = expectedRequestedAmount * anotherTokenAllocation / 100 + const requestedDAI = exchangedAmount(expectedRequestedAmount, DAI_RATE, allocationDAI) + const requestedANT = exchangedAmount(expectedRequestedAmount, ANT_RATE, allocationANT) it('transfers all the pending reimbursements', async () => { - const previousDenominationTokenBalance = await denominationToken.balanceOf(employee) - const previousAnotherTokenBalance = await anotherToken.balanceOf(employee) + const previousDAI = await DAI.balanceOf(employee) + const previousANT = await ANT.balanceOf(employee) await payroll.payday(PAYMENT_TYPES.REIMBURSEMENT, requestedAmount, { from }) - const currentDenominationTokenBalance = await denominationToken.balanceOf(employee) - const expectedDenominationTokenBalance = previousDenominationTokenBalance.plus(requestedDenominationTokenAmount); - assert.equal(currentDenominationTokenBalance.toString(), expectedDenominationTokenBalance.toString(), 'current denomination token balance does not match') + const currentDAI = await DAI.balanceOf(employee) + const expectedDAI = previousDAI.plus(requestedDAI); + assert.equal(currentDAI.toString(), expectedDAI.toString(), 'current DAI balance does not match') - const currentAnotherTokenBalance = await anotherToken.balanceOf(employee) - const expectedAnotherTokenBalance = anotherTokenRate.mul(requestedAnotherTokenAmount).plus(previousAnotherTokenBalance).trunc() - assert.equal(currentAnotherTokenBalance.toString(), expectedAnotherTokenBalance.toString(), 'current token balance does not match') + const currentANT = await ANT.balanceOf(employee) + const expectedANT = previousANT.plus(requestedANT) + assert.equal(currentANT.toString(), expectedANT.toString(), 'current ANT balance does not match') }) it('emits one event per allocated token', async () => { @@ -197,17 +190,17 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { const events = receipt.logs.filter(l => l.event === 'SendPayment') assert.equal(events.length, 2, 'should have emitted two events') - const denominationTokenEvent = events.find(e => e.args.token === denominationToken.address).args - assert.equal(denominationTokenEvent.employee, employee, 'employee address does not match') - assert.equal(denominationTokenEvent.token, denominationToken.address, 'denomination token address does not match') - assert.equal(denominationTokenEvent.amount.toString(), requestedDenominationTokenAmount, 'payment amount does not match') - assert.equal(denominationTokenEvent.paymentReference, 'Reimbursement', 'payment reference does not match') - - const anotherTokenEvent = events.find(e => e.args.token === anotherToken.address).args - assert.equal(anotherTokenEvent.employee, employee, 'employee address does not match') - assert.equal(anotherTokenEvent.token, anotherToken.address, 'token address does not match') - assert.equal(anotherTokenEvent.amount.div(anotherTokenRate).trunc().toString(), parseInt(requestedAnotherTokenAmount), 'payment amount does not match') - assert.equal(anotherTokenEvent.paymentReference, 'Reimbursement', 'payment reference does not match') + const eventDAI = events.find(e => e.args.token === DAI.address).args + assert.equal(eventDAI.employee, employee, 'employee address does not match') + assert.equal(eventDAI.token, DAI.address, 'DAI address does not match') + assert.equal(eventDAI.amount.toString(), requestedDAI, 'payment amount does not match') + assert.equal(eventDAI.paymentReference, 'Reimbursement', 'payment reference does not match') + + const eventANT = events.find(e => e.args.token === ANT.address).args + assert.equal(eventANT.employee, employee, 'employee address does not match') + assert.equal(eventANT.token, ANT.address, 'token address does not match') + assert.equal(eventANT.amount.toString(), requestedANT, 'payment amount does not match') + assert.equal(eventANT.paymentReference, 'Reimbursement', 'payment reference does not match') }) } @@ -219,7 +212,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { const [address, employeeSalary, _, reimbursements] = await payroll.getEmployee(employeeId) assert.equal(address, employee, 'employee address does not match') - assert.equal(employeeSalary, salary, 'employee salary does not match') + assert.equal(employeeSalary.toString(), salary.toString(), 'employee salary does not match') assert.equal(previousReimbursements.minus(expectedRequestedAmount).toString(), reimbursements.toString(), 'employee reimbursements does not match') }) } @@ -233,7 +226,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { context('when exchange rates are expired', () => { beforeEach('expire exchange rates', async () => { const expiredTimestamp = (await payroll.getTimestampPublic()).sub(RATE_EXPIRATION_TIME + 1) - await setTokenRates(priceFeed, denominationToken, [anotherToken], [anotherTokenRate], expiredTimestamp) + await setTokenRates(priceFeed, USD, [DAI, ANT], [DAI_RATE, ANT_RATE], expiredTimestamp) }) it('reverts', async () => { @@ -243,7 +236,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { } context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) context('when the employee has some pending salary', () => { context('when the employee is not terminated', () => { @@ -286,7 +279,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { context('when exchange rates are expired', () => { beforeEach('expire exchange rates', async () => { const expiredTimestamp = (await payroll.getTimestampPublic()).sub(RATE_EXPIRATION_TIME + 1) - await setTokenRates(priceFeed, denominationToken, [anotherToken], [anotherTokenRate], expiredTimestamp) + await setTokenRates(priceFeed, USD, [DAI, ANT], [DAI_RATE, ANT_RATE], expiredTimestamp) }) it('reverts', async () => { @@ -298,7 +291,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { }) context('when the requested amount is less than the total reimbursements amount', () => { - const requestedAmount = reimbursement - 1 + const requestedAmount = reimbursement.div(2) context('when the employee has some pending salary', () => { context('when the employee is not terminated', () => { @@ -377,7 +370,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { context('when exchange rates are expired', () => { beforeEach('expire exchange rates', async () => { const expiredTimestamp = (await payroll.getTimestampPublic()).sub(RATE_EXPIRATION_TIME + 1) - await setTokenRates(priceFeed, denominationToken, [anotherToken], [anotherTokenRate], expiredTimestamp) + await setTokenRates(priceFeed, USD, [DAI, ANT], [DAI_RATE, ANT_RATE], expiredTimestamp) }) it('reverts', async () => { @@ -389,7 +382,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { }) context('when the requested amount is greater than the total reimbursements amount', () => { - const requestedAmount = reimbursement + 1 + const requestedAmount = reimbursement.plus(1) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.REIMBURSEMENT, requestedAmount, { from }), 'PAYROLL_INVALID_REQUESTED_AMT') @@ -399,7 +392,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { context('when the employee does not have pending reimbursements', () => { context('when the requested amount is greater than zero', () => { - const requestedAmount = 100 + const requestedAmount = bigExp(100, 18) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.REIMBURSEMENT, requestedAmount, { from }), 'PAYROLL_NOTHING_PAID') @@ -407,7 +400,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.REIMBURSEMENT, requestedAmount, { from }), 'PAYROLL_NOTHING_PAID') @@ -418,7 +411,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { context('when the employee did not set any token allocations yet', () => { context('when the employee has some pending reimbursements', () => { - const reimbursement = 100 + const reimbursement = bigExp(1000, 18) beforeEach('add reimbursement', async () => { await payroll.addReimbursement(employeeId, reimbursement / 2, { from: owner }) @@ -426,7 +419,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.REIMBURSEMENT, requestedAmount, { from }), 'PAYROLL_NOTHING_PAID') @@ -434,7 +427,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { }) context('when the requested amount is less than the total reimbursements amount', () => { - const requestedAmount = reimbursement - 1 + const requestedAmount = reimbursement.div(2) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.REIMBURSEMENT, requestedAmount, { from }), 'PAYROLL_NOTHING_PAID') @@ -450,7 +443,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { }) context('when the requested amount is greater than the total reimbursements amount', () => { - const requestedAmount = reimbursement + 1 + const requestedAmount = reimbursement.plus(1) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.REIMBURSEMENT, requestedAmount, { from }), 'PAYROLL_INVALID_REQUESTED_AMT') @@ -460,7 +453,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { context('when the employee does not have pending reimbursements', () => { context('when the requested amount is greater than zero', () => { - const requestedAmount = 100 + const requestedAmount = bigExp(100, 18) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.REIMBURSEMENT, requestedAmount, { from }), 'PAYROLL_NOTHING_PAID') @@ -468,7 +461,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.REIMBURSEMENT, requestedAmount, { from }), 'PAYROLL_NOTHING_PAID') @@ -482,7 +475,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { const from = anyone context('when the requested amount is greater than zero', () => { - const requestedAmount = 100 + const requestedAmount = bigExp(100, 18) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.REIMBURSEMENT, requestedAmount, { from }), 'PAYROLL_EMPLOYEE_DOES_NOT_MATCH') @@ -490,7 +483,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { }) context('when the requested amount is zero', () => { - const requestedAmount = 0 + const requestedAmount = bn(0) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.REIMBURSEMENT, requestedAmount, { from }), 'PAYROLL_EMPLOYEE_DOES_NOT_MATCH') @@ -500,7 +493,7 @@ contract('Payroll reimbursements', ([owner, employee, anyone]) => { }) context('when it has not been initialized yet', function () { - const requestedAmount = 0 + const requestedAmount = bn(0) it('reverts', async () => { await assertRevert(payroll.payday(PAYMENT_TYPES.REIMBURSEMENT, requestedAmount, { from: employee }), 'PAYROLL_EMPLOYEE_DOES_NOT_MATCH') diff --git a/future-apps/payroll/test/contracts/Payroll_settings.test.js b/future-apps/payroll/test/contracts/Payroll_settings.test.js index 43cb3ac8c8..37604128b5 100644 --- a/future-apps/payroll/test/contracts/Payroll_settings.test.js +++ b/future-apps/payroll/test/contracts/Payroll_settings.test.js @@ -1,23 +1,18 @@ +const { USD } = require('../helpers/tokens')(artifacts, web3) const { getEvents } = require('../helpers/events') const { assertRevert } = require('@aragon/test-helpers/assertThrow') -const { deployErc20TokenAndDeposit, deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy.js')(artifacts, web3) +const { NOW, RATE_EXPIRATION_TIME } = require('../helpers/time') +const { deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy')(artifacts, web3) const PriceFeed = artifacts.require('PriceFeedMock') const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' contract('Payroll settings', ([owner, anyone]) => { - let dao, payroll, payrollBase, finance, vault, priceFeed, denominationToken + let dao, payroll, payrollBase, finance, vault, priceFeed - const NOW = 1553703809 // random fixed timestamp in seconds - const ONE_MONTH = 60 * 60 * 24 * 31 - const TWO_MONTHS = ONE_MONTH * 2 - const RATE_EXPIRATION_TIME = TWO_MONTHS - - const TOKEN_DECIMALS = 18 before('deploy base apps and tokens', async () => { ({ dao, finance, vault, payrollBase } = await deployContracts(owner)) - denominationToken = await deployErc20TokenAndDeposit(owner, finance, 'Denomination Token', TOKEN_DECIMALS) }) beforeEach('create payroll and price feed instance', async () => { @@ -32,8 +27,8 @@ contract('Payroll settings', ([owner, anyone]) => { }) context('when it has already been initialized', function () { - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) context('when the sender has permissions', async () => { @@ -86,8 +81,8 @@ contract('Payroll settings', ([owner, anyone]) => { describe('setRateExpiryTime', () => { context('when it has already been initialized', function () { - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) context('when the sender has permissions', async () => { diff --git a/future-apps/payroll/test/contracts/Payroll_terminate_employee.test.js b/future-apps/payroll/test/contracts/Payroll_terminate_employee.test.js index 89fcce510d..9eb687f4de 100644 --- a/future-apps/payroll/test/contracts/Payroll_terminate_employee.test.js +++ b/future-apps/payroll/test/contracts/Payroll_terminate_employee.test.js @@ -1,18 +1,13 @@ const PAYMENT_TYPES = require('../helpers/payment_types') const { assertRevert } = require('@aragon/test-helpers/assertThrow') const { getEvents, getEventArgument } = require('../helpers/events') -const { bn, maxUint64, annualSalaryPerSecond } = require('../helpers/numbers')(web3) -const { deployErc20TokenAndDeposit, deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy.js')(artifacts, web3) +const { NOW, ONE_MONTH, RATE_EXPIRATION_TIME } = require('../helpers/time') +const { USD, DAI_RATE, exchangedAmount, deployDAI, setTokenRate } = require('../helpers/tokens')(artifacts, web3) +const { ONE, bn, bigExp, MAX_UINT64, annualSalaryPerSecond } = require('../helpers/numbers')(web3) +const { deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy')(artifacts, web3) contract('Payroll employees termination', ([owner, employee, anyone]) => { - let dao, payroll, payrollBase, finance, vault, priceFeed, denominationToken - - const NOW = 1553703809 // random fixed timestamp in seconds - const ONE_MONTH = 60 * 60 * 24 * 31 - const TWO_MONTHS = ONE_MONTH * 2 - const RATE_EXPIRATION_TIME = TWO_MONTHS - - const TOKEN_DECIMALS = 18 + let dao, payroll, payrollBase, finance, vault, priceFeed, DAI const currentTimestamp = async () => payroll.getTimestampPublic() @@ -23,7 +18,7 @@ contract('Payroll employees termination', ([owner, employee, anyone]) => { before('deploy base apps and tokens', async () => { ({ dao, finance, vault, payrollBase } = await deployContracts(owner)) - denominationToken = await deployErc20TokenAndDeposit(owner, finance, 'Denomination Token', TOKEN_DECIMALS) + DAI = await deployDAI(owner, finance) }) beforeEach('create payroll and price feed instance', async () => { @@ -32,13 +27,13 @@ contract('Payroll employees termination', ([owner, employee, anyone]) => { describe('terminateEmployee', () => { context('when it has already been initialized', function () { - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) context('when the given employee id exists', () => { let employeeId - const salary = annualSalaryPerSecond(100000, TOKEN_DECIMALS) + const salary = annualSalaryPerSecond(100000) beforeEach('add employee', async () => { const receipt = await payroll.addEmployee(employee, salary, 'Boss', await payroll.getTimestampPublic(), { from: owner }) @@ -51,8 +46,9 @@ contract('Payroll employees termination', ([owner, employee, anyone]) => { context('when the employee was not terminated', () => { let endDate - beforeEach('allowed denomination token', async () => { - await payroll.addAllowedToken(denominationToken.address, { from: owner }) + beforeEach('allow DAI and set rate', async () => { + await setTokenRate(priceFeed, USD, DAI, DAI_RATE) + await payroll.addAllowedToken(DAI.address, { from: owner }) }) context('when the given end date is in the future ', () => { @@ -80,36 +76,38 @@ contract('Payroll employees termination', ([owner, employee, anyone]) => { }) it('does not reset the owed salary nor the reimbursements of the employee', async () => { - const previousBalance = await denominationToken.balanceOf(employee) - await payroll.determineAllocation([denominationToken.address], [100], { from: employee }) + const previousDAI = await DAI.balanceOf(employee) + await payroll.determineAllocation([DAI.address], [100], { from: employee }) // Accrue some salary and extras await increaseTime(ONE_MONTH) - const owedSalary = salary.times(ONE_MONTH) - const reimbursement = 1000 + const reimbursement = bigExp(100000, 18) await payroll.addReimbursement(employeeId, reimbursement, { from: owner }) // Terminate employee and travel some time in the future await payroll.terminateEmployee(employeeId, endDate, { from }) - await increaseTime(ONE_MONTH) + await increaseTime(ONE_MONTH - 1) // to avoid expire rates // Request owed money await payroll.payday(PAYMENT_TYPES.PAYROLL, 0, { from: employee }) await payroll.payday(PAYMENT_TYPES.REIMBURSEMENT, 0, { from: employee }) await assertRevert(payroll.getEmployee(employeeId), 'PAYROLL_EMPLOYEE_DOESNT_EXIST') - const currentBalance = await denominationToken.balanceOf(employee) - const expectedCurrentBalance = previousBalance.plus(owedSalary).plus(reimbursement) - assert.equal(currentBalance.toString(), expectedCurrentBalance.toString(), 'current balance does not match') + const owedSalaryInDai = exchangedAmount(salary.times(ONE_MONTH), DAI_RATE, 100) + const reimbursementInDai = exchangedAmount(reimbursement, DAI_RATE, 100) + + const currentDAI = await DAI.balanceOf(employee) + const expectedDAI = previousDAI.plus(owedSalaryInDai).plus(reimbursementInDai) + assert.equal(currentDAI.toString(), expectedDAI.toString(), 'current balance does not match') }) it('can re-add a removed employee', async () => { - await payroll.determineAllocation([denominationToken.address], [100], { from: employee }) + await payroll.determineAllocation([DAI.address], [100], { from: employee }) await increaseTime(ONE_MONTH) // Terminate employee and travel some time in the future await payroll.terminateEmployee(employeeId, endDate, { from }) - await increaseTime(ONE_MONTH) + await increaseTime(ONE_MONTH - 1) // to avoid expire rates // Request owed money await payroll.payday(PAYMENT_TYPES.PAYROLL, 0, { from: employee }) @@ -126,7 +124,7 @@ contract('Payroll employees termination', ([owner, employee, anyone]) => { assert.equal(bonus.toString(), 0, 'employee bonus does not match') assert.equal(reimbursements.toString(), 0, 'employee reimbursements does not match') assert.equal(accruedSalary.toString(), 0, 'employee accrued salary does not match') - assert.equal(date.toString(), maxUint64(), 'employee end date does not match') + assert.equal(date.toString(), MAX_UINT64, 'employee end date does not match') }) }) diff --git a/future-apps/payroll/test/contracts/Payroll_token_allocations.test.js b/future-apps/payroll/test/contracts/Payroll_token_allocations.test.js index 906b7104a8..003e58eaae 100644 --- a/future-apps/payroll/test/contracts/Payroll_token_allocations.test.js +++ b/future-apps/payroll/test/contracts/Payroll_token_allocations.test.js @@ -1,22 +1,18 @@ const { assertRevert } = require('@aragon/test-helpers/assertThrow') +const { annualSalaryPerSecond } = require('../helpers/numbers')(web3) const { getEvents, getEventArgument } = require('../helpers/events') -const { deployErc20TokenAndDeposit, deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy.js')(artifacts, web3) +const { NOW, ONE_MONTH, RATE_EXPIRATION_TIME } = require('../helpers/time') +const { USD, deployDAI, deployTokenAndDeposit } = require('../helpers/tokens')(artifacts, web3) +const { deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy')(artifacts, web3) const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' contract('Payroll token allocations', ([owner, employee, anyone]) => { - let dao, payroll, payrollBase, finance, vault, priceFeed, denominationToken - - const NOW = 1553703809 // random fixed timestamp in seconds - const ONE_MONTH = 60 * 60 * 24 * 31 - const TWO_MONTHS = ONE_MONTH * 2 - const RATE_EXPIRATION_TIME = TWO_MONTHS - - const TOKEN_DECIMALS = 18 + let dao, payroll, payrollBase, finance, vault, priceFeed, DAI before('deploy base apps and tokens', async () => { ({ dao, finance, vault, payrollBase } = await deployContracts(owner)) - denominationToken = await deployErc20TokenAndDeposit(owner, finance, 'Denomination Token', TOKEN_DECIMALS) + DAI = await deployDAI(owner, finance) }) beforeEach('create payroll and price feed instance', async () => { @@ -27,15 +23,15 @@ contract('Payroll token allocations', ([owner, employee, anyone]) => { const tokenAddresses = [] before('deploy some tokens', async () => { - const token1 = await deployErc20TokenAndDeposit(owner, finance, 'Token 1', 14) - const token2 = await deployErc20TokenAndDeposit(owner, finance, 'Token 2', 14) - const token3 = await deployErc20TokenAndDeposit(owner, finance, 'Token 3', 14) + const token1 = await deployTokenAndDeposit(owner, finance, 'Token 1', 14) + const token2 = await deployTokenAndDeposit(owner, finance, 'Token 2', 14) + const token3 = await deployTokenAndDeposit(owner, finance, 'Token 3', 14) tokenAddresses.push(token1.address, token2.address, token3.address) }) context('when it has already been initialized', function () { - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) beforeEach('allow multiple tokens', async () => { @@ -47,7 +43,7 @@ contract('Payroll token allocations', ([owner, employee, anyone]) => { let employeeId beforeEach('add employee', async () => { - const receipt = await payroll.addEmployee(employee, 100000, 'Boss', await payroll.getTimestampPublic(), { from: owner }) + const receipt = await payroll.addEmployee(employee, annualSalaryPerSecond(100000), 'Boss', await payroll.getTimestampPublic(), { from: owner }) employeeId = getEventArgument(receipt, 'AddEmployee', 'employeeId') }) @@ -80,7 +76,7 @@ contract('Payroll token allocations', ([owner, employee, anyone]) => { let token beforeEach('submit previous allocation', async () => { - token = await deployErc20TokenAndDeposit(owner, finance, 'Previous Token', 18) + token = await deployTokenAndDeposit(owner, finance, 'Previous Token', 18) await payroll.addAllowedToken(token.address, { from: owner }) await payroll.determineAllocation([token.address], [100], { from }) @@ -138,7 +134,7 @@ contract('Payroll token allocations', ([owner, employee, anyone]) => { let notAllowedToken beforeEach('deploy new token', async () => { - notAllowedToken = await deployErc20TokenAndDeposit(owner, finance, 'Not-allowed token', 14) + notAllowedToken = await deployTokenAndDeposit(owner, finance, 'Not-allowed token', 14) }) it('reverts', async () => { @@ -201,15 +197,15 @@ contract('Payroll token allocations', ([owner, employee, anyone]) => { describe('getAllocation', () => { context('when it has already been initialized', function () { - beforeEach('initialize payroll app', async () => { - await payroll.initialize(finance.address, denominationToken.address, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) + beforeEach('initialize payroll app using USD as denomination token', async () => { + await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner }) }) context('when the employee exists', () => { let employeeId beforeEach('add employee', async () => { - const receipt = await payroll.addEmployee(employee, 100000, 'Boss', await payroll.getTimestampPublic(), { from: owner }) + const receipt = await payroll.addEmployee(employee, annualSalaryPerSecond(100000), 'Boss', await payroll.getTimestampPublic(), { from: owner }) employeeId = getEventArgument(receipt, 'AddEmployee', 'employeeId') }) @@ -217,23 +213,23 @@ contract('Payroll token allocations', ([owner, employee, anyone]) => { context('when the given token is not the zero address', () => { context('when the given token was allowed', () => { beforeEach('allow denomination token', async () => { - await payroll.addAllowedToken(denominationToken.address, { from: owner }) + await payroll.addAllowedToken(DAI.address, { from: owner }) }) context('when the given token was picked by the employee', () => { beforeEach('determine allocation', async () => { - await payroll.determineAllocation([denominationToken.address], [100], { from: employee }) + await payroll.determineAllocation([DAI.address], [100], { from: employee }) }) it('tells its corresponding allocation', async () => { - const allocation = await payroll.getAllocation(employeeId, denominationToken.address) + const allocation = await payroll.getAllocation(employeeId, DAI.address) assert.equal(allocation.toString(), 100, 'token allocation does not match') }) }) context('when the given token was not picked by the employee', () => { it('returns 0', async () => { - const allocation = await payroll.getAllocation(employeeId, denominationToken.address) + const allocation = await payroll.getAllocation(employeeId, DAI.address) assert.equal(allocation.toString(), 0, 'token allocation should be zero') }) }) @@ -241,7 +237,7 @@ contract('Payroll token allocations', ([owner, employee, anyone]) => { context('when the given token was not allowed', () => { it('returns 0', async () => { - const allocation = await payroll.getAllocation(employeeId, denominationToken.address) + const allocation = await payroll.getAllocation(employeeId, DAI.address) assert.equal(allocation.toString(), 0, 'token allocation should be zero') }) }) @@ -301,7 +297,7 @@ contract('Payroll token allocations', ([owner, employee, anyone]) => { const employeeId = 0 it('reverts', async () => { - await assertRevert(payroll.getAllocation(employeeId, denominationToken.address), 'PAYROLL_EMPLOYEE_DOESNT_EXIST') + await assertRevert(payroll.getAllocation(employeeId, DAI.address), 'PAYROLL_EMPLOYEE_DOESNT_EXIST') }) }) }) @@ -310,7 +306,7 @@ contract('Payroll token allocations', ([owner, employee, anyone]) => { const employeeId = 0 it('reverts', async () => { - await assertRevert(payroll.getAllocation(employeeId, denominationToken.address), 'PAYROLL_EMPLOYEE_DOESNT_EXIST') + await assertRevert(payroll.getAllocation(employeeId, DAI.address), 'PAYROLL_EMPLOYEE_DOESNT_EXIST') }) }) }) diff --git a/future-apps/payroll/test/helpers/deploy.js b/future-apps/payroll/test/helpers/deploy.js index 68b8c5d7a9..f1083399ec 100644 --- a/future-apps/payroll/test/helpers/deploy.js +++ b/future-apps/payroll/test/helpers/deploy.js @@ -1,6 +1,8 @@ module.exports = (artifacts, web3) => { const { bigExp } = require('./numbers')(web3) const { getEventArgument } = require('./events') + const { SECONDS_IN_A_YEAR } = require('./time') + const getContract = name => artifacts.require(name) const ACL = getContract('ACL') @@ -59,7 +61,6 @@ module.exports = (artifacts, web3) => { await acl.createPermission(finance.address, vault.address, TRANSFER_ROLE, owner, { from: owner }) await vault.initialize() - const SECONDS_IN_A_YEAR = 31557600 // 365.25 days await finance.initialize(vault.address, SECONDS_IN_A_YEAR) // more than one day const payrollBase = await Payroll.new() diff --git a/future-apps/payroll/test/helpers/numbers.js b/future-apps/payroll/test/helpers/numbers.js index ff2b331a20..f672d96bef 100644 --- a/future-apps/payroll/test/helpers/numbers.js +++ b/future-apps/payroll/test/helpers/numbers.js @@ -1,20 +1,21 @@ -const SECONDS_IN_A_YEAR = 31557600 // 365.25 days +const { SECONDS_IN_A_YEAR } = require('./time') module.exports = web3 => { const bn = x => new web3.BigNumber(x) const bigExp = (x, y) => bn(x).mul(bn(10).pow(bn(y))) - const maxUint = (e) => bn(2).pow(bn(e)).sub(bn(1)) - const maxUint64 = () => maxUint(64) - const maxUint256 = () => maxUint(256) + const annualSalaryPerSecond = (amount, decimals = 18) => bigExp(amount, decimals).dividedToIntegerBy(SECONDS_IN_A_YEAR) - const annualSalaryPerSecond = (amount, decimals) => bigExp(amount, decimals).dividedToIntegerBy(SECONDS_IN_A_YEAR) + const ONE = bigExp(1, 18) + const MAX_UINT64 = maxUint(64) + const MAX_UINT256 = maxUint(256) return { bn, bigExp, - maxUint64, - maxUint256, - annualSalaryPerSecond + annualSalaryPerSecond, + ONE, + MAX_UINT64, + MAX_UINT256, } } diff --git a/future-apps/payroll/test/helpers/set_token_rates.js b/future-apps/payroll/test/helpers/set_token_rates.js deleted file mode 100644 index 7be2afdcef..0000000000 --- a/future-apps/payroll/test/helpers/set_token_rates.js +++ /dev/null @@ -1,23 +0,0 @@ -const SIG = '00'.repeat(65) // sig full of 0s - -module.exports = web3 => { - - function formatRate(n) { - const { bn, bigExp } = require('./numbers')(web3) - const ONE = bigExp(1, 18) - return bn(n.toFixed(18)).times(ONE) - } - - return async function setTokenRates(feed, denominationToken, tokens, rates, when = undefined) { - if (!when) when = await feed.getTimestampPublic() - - const bases = tokens.map(token => typeof(token) === 'object' ? token.address : token) - const quotes = tokens.map(() => typeof(denominationToken) === 'object' ? denominationToken.address : denominationToken) - const xrts = rates.map(rate => formatRate(rate)) - const whens = tokens.map(() => when) - const sigs = `0x${SIG.repeat(tokens.length)}` - - return feed.updateMany(bases, quotes, xrts, whens, sigs) - } - -} diff --git a/future-apps/payroll/test/helpers/time.js b/future-apps/payroll/test/helpers/time.js new file mode 100644 index 0000000000..7b4c2df9b0 --- /dev/null +++ b/future-apps/payroll/test/helpers/time.js @@ -0,0 +1,18 @@ +const NOW = 1553703809 // random fixed timestamp in seconds +const ONE_MINUTE = 60 +const TWO_MINUTES = ONE_MINUTE * 2 +const ONE_MONTH = 60 * 60 * 24 * 31 +const TWO_MONTHS = ONE_MONTH * 2 +const SECONDS_IN_A_YEAR = 31557600 // 365.25 days + +const RATE_EXPIRATION_TIME = TWO_MONTHS + +module.exports = { + NOW, + ONE_MINUTE, + TWO_MINUTES, + ONE_MONTH, + TWO_MONTHS, + RATE_EXPIRATION_TIME, + SECONDS_IN_A_YEAR +} diff --git a/future-apps/payroll/test/helpers/tokens.js b/future-apps/payroll/test/helpers/tokens.js new file mode 100644 index 0000000000..e8684cc426 --- /dev/null +++ b/future-apps/payroll/test/helpers/tokens.js @@ -0,0 +1,70 @@ +const SIG = '00'.repeat(65) // sig full of 0s + +const ETH = '0x0000000000000000000000000000000000000000' +const USD = '0xFFFfFfffffFFFFFFFfffFFFFFffffFfFfFAAaCbB' // USD identifier: https://github.com/aragon/ppf#tickers-and-token-addresses + +module.exports = (artifacts, web3) => { + const { ONE, bn, bigExp } = require('./numbers')(web3) + + const formatRate = n => bn(n.toFixed(18)).times(ONE) + + const ETH_RATE = formatRate(20) // 1 ETH = 20 USD + const DAI_RATE = formatRate(1) // 1 DAI = 1 USD + const ANT_RATE = formatRate(0.5) // 1 ANT = 0.5 USD + + function exchangedAmount(amount, rate, tokenAllocation) { + // Mimic PPF inversion truncation, as we set the denomination token always + // as the price feed's quote token + const inverseRate = ONE.pow(2).div(rate).trunc() + // Mimic EVM calculation and truncation for token conversion + return amount.mul(inverseRate).mul(tokenAllocation).div(ONE.mul(100)).trunc() + } + + const deployANT = async (sender, finance) => deployTokenAndDeposit(sender, finance, 'ANT') + const deployDAI = async (sender, finance) => deployTokenAndDeposit(sender, finance, 'DAI') + + async function deployTokenAndDeposit(sender, finance, name = 'ERC20Token', decimals = 18) { + const MiniMeToken = artifacts.require('MiniMeToken') + const token = await MiniMeToken.new('0x0', '0x0', 0, name, decimals, 'E20', true) // dummy parameters for minime + const amount = bigExp(1e18, decimals) + await token.generateTokens(sender, amount) + await token.approve(finance.address, amount, { from: sender }) + await finance.deposit(token.address, amount, `Initial ${name} deposit`, { from: sender }) + return token + } + + async function setTokenRate(feed, denominationToken, token, rate, when = undefined) { + if (!when) when = await feed.getTimestampPublic() + + const base = typeof(token) === 'object' ? token.address : token + const quote = typeof(denominationToken) === 'object' ? denominationToken.address : denominationToken + + return feed.update(base, quote, rate, when, SIG) + } + + async function setTokenRates(feed, denominationToken, tokens, rates, when = undefined) { + if (!when) when = await feed.getTimestampPublic() + + const bases = tokens.map(token => typeof(token) === 'object' ? token.address : token) + const quotes = tokens.map(() => typeof(denominationToken) === 'object' ? denominationToken.address : denominationToken) + const whens = tokens.map(() => when) + const sigs = `0x${SIG.repeat(tokens.length)}` + + return feed.updateMany(bases, quotes, rates, whens, sigs) + } + + return { + ETH, + USD, + ETH_RATE, + DAI_RATE, + ANT_RATE, + formatRate, + exchangedAmount, + deployANT, + deployDAI, + deployTokenAndDeposit, + setTokenRate, + setTokenRates, + } +}