diff --git a/contracts/mocks/ConditionalTokenEscrowMock.sol b/contracts/mocks/ConditionalTokenEscrowMock.sol new file mode 100644 index 00000000000..e7a0c83b2ce --- /dev/null +++ b/contracts/mocks/ConditionalTokenEscrowMock.sol @@ -0,0 +1,20 @@ +pragma solidity ^0.4.24; + +import "../payment/ConditionalTokenEscrow.sol"; +import "../token/ERC20/ERC20.sol"; + + +// mock class using ConditionalTokenEscrow +contract ConditionalTokenEscrowMock is ConditionalTokenEscrow { + mapping(address => bool) public allowed; + + constructor (ERC20 _token) public TokenEscrow(_token) { } + + function setAllowed(address _payee, bool _allowed) public { + allowed[_payee] = _allowed; + } + + function withdrawalAllowed(address _payee) public view returns (bool) { + return allowed[_payee]; + } +} diff --git a/contracts/payment/ConditionalTokenEscrow.sol b/contracts/payment/ConditionalTokenEscrow.sol new file mode 100644 index 00000000000..2f7ef29f446 --- /dev/null +++ b/contracts/payment/ConditionalTokenEscrow.sol @@ -0,0 +1,23 @@ +pragma solidity ^0.4.24; + +import "./TokenEscrow.sol"; + + +/** + * @title ConditionalTokenEscrow + * @dev Base abstract escrow to only allow withdrawal of tokens + * if a condition is met. + */ +contract ConditionalTokenEscrow is TokenEscrow { + /** + * @dev Returns whether an address is allowed to withdraw their tokens. + * To be implemented by derived contracts. + * @param _payee The destination address of the tokens. + */ + function withdrawalAllowed(address _payee) public view returns (bool); + + function withdraw(address _payee) public { + require(withdrawalAllowed(_payee)); + super.withdraw(_payee); + } +} diff --git a/contracts/payment/RefundTokenEscrow.sol b/contracts/payment/RefundTokenEscrow.sol new file mode 100644 index 00000000000..3385dcfa964 --- /dev/null +++ b/contracts/payment/RefundTokenEscrow.sol @@ -0,0 +1,78 @@ +pragma solidity ^0.4.24; + +import "./ConditionalTokenEscrow.sol"; +import "../token/ERC20/ERC20.sol"; + + +/** + * @title RefundTokenEscrow + * @dev Escrow that holds tokens for a beneficiary, deposited from multiple parties. + * The contract owner may close the deposit period, and allow for either withdrawal + * by the beneficiary, or refunds to the depositors. + */ +contract RefundTokenEscrow is ConditionalTokenEscrow { + + enum State { Active, Refunding, Closed } + + event Closed(); + event RefundsEnabled(); + + State public state; + address public beneficiary; + + /** + * @dev Constructor. + * @param _token Address of the ERC20 token that will be put in escrow. + * @param _beneficiary The beneficiary of the deposits. + */ + constructor(ERC20 _token, address _beneficiary) public TokenEscrow(_token) { + require(_beneficiary != address(0)); + beneficiary = _beneficiary; + state = State.Active; + } + + /** + * @dev Stores tokens that may later be refunded. + * @param _refundee The address tokens will be sent to if a refund occurs. + * @param _amount The amount of tokens to store. + */ + function deposit(address _refundee, uint256 _amount) public { + require(state == State.Active); + super.deposit(_refundee, _amount); + } + + /** + * @dev Allows for the beneficiary to withdraw their tokens, rejecting + * further deposits. + */ + function close() public onlyOwner { + require(state == State.Active); + state = State.Closed; + emit Closed(); + } + + /** + * @dev Allows for refunds to take place, rejecting further deposits. + */ + function enableRefunds() public onlyOwner { + require(state == State.Active); + state = State.Refunding; + emit RefundsEnabled(); + } + + /** + * @dev Withdraws the beneficiary's tokens. + */ + function beneficiaryWithdraw() public { + require(state == State.Closed); + uint256 amount = token.balanceOf(address(this)); + token.safeTransfer(beneficiary, amount); + } + + /** + * @dev Returns whether refundees can withdraw their deposits (be refunded). + */ + function withdrawalAllowed(address _payee) public view returns (bool) { + return state == State.Refunding; + } +} diff --git a/contracts/payment/TimelockedEscrow.sol b/contracts/payment/TimelockedEscrow.sol new file mode 100644 index 00000000000..4f296189f08 --- /dev/null +++ b/contracts/payment/TimelockedEscrow.sol @@ -0,0 +1,33 @@ +pragma solidity ^0.4.24; + +import "./ConditionalEscrow.sol"; + + +/** + * @title TimelockedEscrow + * @dev Escrow that holds funds for given amount of time, + * preventing their withdrawal until the time has passed. + */ +contract TimelockedEscrow is ConditionalEscrow { + + uint256 public releaseTime; + + /** + * @dev Constructor. + * @param _releaseTime Time when the funds will be available for withdrawal. + */ + constructor (uint256 _releaseTime) public { + // solium-disable-next-line security/no-block-members + require(_releaseTime > block.timestamp); + releaseTime = _releaseTime; + } + + /** + * @dev Returns whether an address is allowed to withdraw their funds. + */ + function withdrawalAllowed(address _payee) public view returns (bool) { + // solium-disable-next-line security/no-block-members + require(block.timestamp >= releaseTime); + return true; + } +} diff --git a/contracts/payment/TimelockedTokenEscrow.sol b/contracts/payment/TimelockedTokenEscrow.sol new file mode 100644 index 00000000000..69ad4e8eac6 --- /dev/null +++ b/contracts/payment/TimelockedTokenEscrow.sol @@ -0,0 +1,34 @@ +pragma solidity ^0.4.24; + +import "./ConditionalTokenEscrow.sol"; + + +/** + * @title TimelockedTokenEscrow + * @dev Escrow that holds tokens for given amount of time, + * preventing their whithdrawal until the time has passed. + */ +contract TimelockedTokenEscrow is ConditionalTokenEscrow { + + uint256 public releaseTime; + + /** + * @dev Constructor. + * @param _token Address of the ERC20 token that will be put in escrow. + * @param _releaseTime Time when the tokens will be available for withdrawal. + */ + constructor (ERC20 _token, uint256 _releaseTime) public TokenEscrow(_token) { + // solium-disable-next-line security/no-block-members + require(_releaseTime > block.timestamp); + releaseTime = _releaseTime; + } + + /** + * @dev Returns whether an address is allowed to withdraw their funds. + */ + function withdrawalAllowed(address _payee) public view returns (bool) { + // solium-disable-next-line security/no-block-members + require(block.timestamp >= releaseTime); + return true; + } +} diff --git a/test/payment/ConditionalTokenEscrow.test.js b/test/payment/ConditionalTokenEscrow.test.js new file mode 100644 index 00000000000..0db0e2d7480 --- /dev/null +++ b/test/payment/ConditionalTokenEscrow.test.js @@ -0,0 +1,55 @@ +const { shouldBehaveLikeTokenEscrow } = require('./TokenEscrow.behavior'); +const { expectThrow } = require('../helpers/expectThrow'); +const { EVMRevert } = require('../helpers/EVMRevert'); + +const BigNumber = web3.BigNumber; + +require('chai') + .use(require('chai-bignumber')(BigNumber)) + .should(); + +const ConditionalTokenEscrow = artifacts.require('ConditionalTokenEscrowMock'); +const StandardToken = artifacts.require('StandardTokenMock'); + +contract('ConditionalTokenEscrow', function ([_, owner, payee, ...otherAccounts]) { + const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); + + it('reverts when deployed with a null token address', async function () { + await expectThrow( + ConditionalTokenEscrow.new(ZERO_ADDRESS, { from: owner }), EVMRevert + ); + }); + + context('with token', function () { + beforeEach(async function () { + this.token = await StandardToken.new(owner, MAX_UINT256); + this.escrow = await ConditionalTokenEscrow.new(this.token.address, { from: owner }); + }); + + context('when withdrawal is allowed', function () { + beforeEach(async function () { + await Promise.all(otherAccounts.map( + payee => this.escrow.setAllowed(payee, true)) + ); + }); + + shouldBehaveLikeTokenEscrow(owner, otherAccounts); + }); + + context('when withdrawal is disallowed', function () { + const amount = web3.toWei(23.0, 'ether'); + + beforeEach(async function () { + await this.token.approve(this.escrow.address, MAX_UINT256, { from: owner }); + await this.escrow.setAllowed(payee, false); + }); + + it('reverts on withdrawals', async function () { + await this.escrow.deposit(payee, amount, { from: owner }); + + await expectThrow(this.escrow.withdraw(payee, { from: owner }), EVMRevert); + }); + }); + }); +}); diff --git a/test/payment/RefundTokenEscrow.test.js b/test/payment/RefundTokenEscrow.test.js new file mode 100644 index 00000000000..cc904172ed9 --- /dev/null +++ b/test/payment/RefundTokenEscrow.test.js @@ -0,0 +1,160 @@ +const { expectThrow } = require('../helpers/expectThrow'); +const { EVMRevert } = require('../helpers/EVMRevert'); +const expectEvent = require('../helpers/expectEvent'); + +const BigNumber = web3.BigNumber; + +require('chai') + .use(require('chai-bignumber')(BigNumber)) + .should(); + +const RefundTokenEscrow = artifacts.require('RefundTokenEscrow'); +const StandardToken = artifacts.require('StandardTokenMock'); + +contract('RefundTokenEscrow', function ([_, owner, beneficiary, refundee1, refundee2]) { + const amount = web3.toWei(54.0, 'ether'); + const refundees = [refundee1, refundee2]; + const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); + + it('reverts when deployed with a null token address', async function () { + await expectThrow( + RefundTokenEscrow.new(ZERO_ADDRESS, beneficiary, { from: owner }) + ); + }); + + context('with token', function () { + beforeEach(async function () { + this.token = await StandardToken.new(owner, MAX_UINT256); + }); + + it('reverts when deployed with a null beneficiary', async function () { + await expectThrow( + RefundTokenEscrow.new(this.token.address, ZERO_ADDRESS, { from: owner }) + ); + }); + + context('once deployed', function () { + beforeEach(async function () { + this.escrow = await RefundTokenEscrow.new(this.token.address, beneficiary, { from: owner }); + this.token.approve(this.escrow.address, MAX_UINT256, { from: owner }); + }); + + context('active state', function () { + it('accepts deposits', async function () { + await this.escrow.deposit(refundee1, amount, { from: owner }); + + (await this.escrow.depositsOf(refundee1)).should.be.bignumber.equal(amount); + + (await this.token.balanceOf(this.escrow.address)).should.be.bignumber.equal(amount); + }); + + it('reverts on refund attempts', async function () { + await this.escrow.deposit(refundee1, amount, { from: owner }); + await expectThrow(this.escrow.withdraw(refundee1), EVMRevert); + }); + + it('reverts on beneficiary withdrawal', async function () { + await this.escrow.deposit(refundee1, amount, { from: owner }); + await expectThrow(this.escrow.beneficiaryWithdraw(), EVMRevert); + }); + }); + + it('reverts when non-owners enter close state', async function () { + await expectThrow(this.escrow.close({ from: beneficiary }), EVMRevert); + }); + + it('emits Closed event when owner enters close state', async function () { + const receipt = await this.escrow.close({ from: owner }); + + expectEvent.inLogs(receipt.logs, 'Closed'); + }); + + context('closed state', function () { + beforeEach(async function () { + await Promise.all(refundees.map( + refundee => this.escrow.deposit(refundee, amount, { from: owner })) + ); + + await this.escrow.close({ from: owner }); + }); + + it('rejects deposits', async function () { + await expectThrow(this.escrow.deposit(refundee1, amount, { from: owner }), EVMRevert); + }); + + it('reverts on refund attempts', async function () { + await expectThrow(this.escrow.withdraw(refundee1), EVMRevert); + }); + + it('allows beneficiary withdrawal', async function () { + const beneficiaryInitialBalance = await this.token.balanceOf(beneficiary); + await this.escrow.beneficiaryWithdraw(); + const beneficiaryFinalBalance = await this.token.balanceOf(beneficiary); + + beneficiaryFinalBalance + .sub(beneficiaryInitialBalance) + .should.be.bignumber.equal(amount * refundees.length); + + (await this.token.balanceOf(this.escrow.address)).should.be.bignumber.equal(0); + }); + + it('reverts on entering the refund state', async function () { + await expectThrow(this.escrow.enableRefunds({ from: owner }), EVMRevert); + }); + + it('reverts on re-entering the closed state', async function () { + await expectThrow(this.escrow.close({ from: owner }), EVMRevert); + }); + }); + + it('reverts when non-owners enter refund state', async function () { + await expectThrow(this.escrow.enableRefunds({ from: beneficiary }), EVMRevert); + }); + + it('emits RefundsEnabled event when owner enters refund state', async function () { + const receipt = await this.escrow.enableRefunds({ from: owner }); + + expectEvent.inLogs(receipt.logs, 'RefundsEnabled'); + }); + + context('refund state', function () { + beforeEach(async function () { + await Promise.all(refundees.map( + refundee => this.escrow.deposit(refundee, amount, { from: owner })) + ); + + await this.escrow.enableRefunds({ from: owner }); + }); + + it('rejects deposits', async function () { + await expectThrow(this.escrow.deposit(refundee1, amount, { from: owner }), EVMRevert); + }); + + it('refunds refundees', async function () { + for (const refundee of [refundee1, refundee2]) { + const refundeeInitialBalance = await this.token.balanceOf(refundee); + await this.escrow.withdraw(refundee, { from: owner }); + const refundeeFinalBalance = await this.token.balanceOf(refundee); + + refundeeFinalBalance.sub(refundeeInitialBalance).should.be.bignumber.equal(amount); + } + + (await this.token.balanceOf(this.escrow.address)).should.be.bignumber.equal(0); + }); + + it('reverts on beneficiary withdrawal', async function () { + await expectThrow(this.escrow.beneficiaryWithdraw(), EVMRevert); + }); + + it('reverts on entering the closed state', async function () { + await expectThrow(this.escrow.close({ from: owner }), EVMRevert); + }); + + it('reverts on re-entering the refund state', async function () { + await expectThrow(this.escrow.enableRefunds({ from: owner }), EVMRevert); + }); + }); + }); + }); +}); diff --git a/test/payment/TimelockedEscrow.test.js b/test/payment/TimelockedEscrow.test.js new file mode 100644 index 00000000000..5082e763299 --- /dev/null +++ b/test/payment/TimelockedEscrow.test.js @@ -0,0 +1,66 @@ +const { expectThrow } = require('../helpers/expectThrow'); +const { EVMRevert } = require('../helpers/EVMRevert'); +const { latestTime } = require('../helpers/latestTime'); +const { increaseTimeTo, duration } = require('../helpers/increaseTime'); +const { ethGetBalance } = require('../helpers/web3'); + +const BigNumber = web3.BigNumber; + +require('chai') + .use(require('chai-bignumber')(BigNumber)) + .should(); + +const TimelockedEscrow = artifacts.require('TimelockedEscrow'); + +contract('TimelockedEscrow', function ([_, owner, payee, ...otherAccounts]) { + it('reverts when release time is in the past', async function () { + const releaseTime = (await latestTime()) - duration.minutes(1); + await expectThrow(TimelockedEscrow.new(releaseTime, { from: owner }), EVMRevert); + }); + + context('once deployed', function () { + const amount = web3.toWei(10.0, 'ether'); + + beforeEach(async function () { + this.releaseTime = (await latestTime()) + duration.years(1); + this.escrow = await TimelockedEscrow.new(this.releaseTime, { from: owner }); + await this.escrow.deposit(payee, { from: owner, value: amount }); + }); + + it('stores the release time', async function () { + const releaseTime = await this.escrow.releaseTime(); + releaseTime.should.be.bignumber.equal(this.releaseTime); + }); + + it('rejects withdrawals before release time', async function () { + await expectThrow(this.escrow.withdraw(payee, { from: owner }), EVMRevert); + }); + + it('rejects withdrawals right before release time', async function () { + await increaseTimeTo(this.releaseTime - duration.seconds(5)); + await expectThrow(this.escrow.withdraw(payee, { from: owner }), EVMRevert); + }); + + it('allows withdrawals right after release time', async function () { + await increaseTimeTo(this.releaseTime + duration.seconds(5)); + + const payeeInitialBalance = await ethGetBalance(payee); + + await this.escrow.withdraw(payee, { from: owner }); + + const payeeFinalBalance = await ethGetBalance(payee); + payeeFinalBalance.sub(payeeInitialBalance).should.be.bignumber.equal(amount); + }); + + it('allows withdrawals after release time', async function () { + await increaseTimeTo(this.releaseTime + duration.years(1)); + + const payeeInitialBalance = await ethGetBalance(payee); + + await this.escrow.withdraw(payee, { from: owner }); + + const payeeFinalBalance = await ethGetBalance(payee); + payeeFinalBalance.sub(payeeInitialBalance).should.be.bignumber.equal(amount); + }); + }); +}); diff --git a/test/payment/TimelockedTokenEscrow.test.js b/test/payment/TimelockedTokenEscrow.test.js new file mode 100644 index 00000000000..6c24680cb17 --- /dev/null +++ b/test/payment/TimelockedTokenEscrow.test.js @@ -0,0 +1,80 @@ +const { expectThrow } = require('../helpers/expectThrow'); +const { EVMRevert } = require('../helpers/EVMRevert'); +const { latestTime } = require('../helpers/latestTime'); +const { increaseTimeTo, duration } = require('../helpers/increaseTime'); + +const BigNumber = web3.BigNumber; + +require('chai') + .use(require('chai-bignumber')(BigNumber)) + .should(); + +const TimelockedTokenEscrow = artifacts.require('TimelockedTokenEscrow'); +const StandardToken = artifacts.require('StandardTokenMock'); + +contract('TimelockedTokenEscrow', function ([_, owner, payee, ...otherAccounts]) { + const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); + + beforeEach(async function () { + this.token = await StandardToken.new(owner, MAX_UINT256); + }); + + it('reverts when release time is in the past', async function () { + const releaseTime = (await latestTime()) - duration.minutes(1); + await expectThrow( + TimelockedTokenEscrow.new(this.token.address, releaseTime, { from: owner }), + EVMRevert + ); + }); + + context('once deployed', function () { + const amount = new BigNumber(100); + + beforeEach(async function () { + this.releaseTime = (await latestTime()) + duration.years(1); + this.escrow = await TimelockedTokenEscrow.new( + this.token.address, + this.releaseTime, + { from: owner } + ); + await this.token.approve(this.escrow.address, MAX_UINT256, { from: owner }); + await this.escrow.deposit(payee, amount, { from: owner }); + }); + + it('stores the release time', async function () { + const releaseTime = await this.escrow.releaseTime(); + releaseTime.should.be.bignumber.equal(this.releaseTime); + }); + + it('rejects withdrawals before release time', async function () { + await expectThrow(this.escrow.withdraw(payee, { from: owner }), EVMRevert); + }); + + it('rejects withdrawals right before release time', async function () { + await increaseTimeTo(this.releaseTime - duration.seconds(5)); + await expectThrow(this.escrow.withdraw(payee, { from: owner }), EVMRevert); + }); + + it('allows withdrawals right after release time', async function () { + await increaseTimeTo(this.releaseTime + duration.seconds(5)); + + const payeeInitialBalance = await this.token.balanceOf(payee); + + await this.escrow.withdraw(payee, { from: owner }); + + const payeeFinalBalance = await this.token.balanceOf(payee); + payeeFinalBalance.sub(payeeInitialBalance).should.be.bignumber.equal(amount); + }); + + it('allows withdrawals after release time', async function () { + await increaseTimeTo(this.releaseTime + duration.years(1)); + + const payeeInitialBalance = await this.token.balanceOf(payee); + + await this.escrow.withdraw(payee, { from: owner }); + + const payeeFinalBalance = await this.token.balanceOf(payee); + payeeFinalBalance.sub(payeeInitialBalance).should.be.bignumber.equal(amount); + }); + }); +}); diff --git a/test/payment/TokenEscrow.behavior.js b/test/payment/TokenEscrow.behavior.js new file mode 100644 index 00000000000..76e5732bbb6 --- /dev/null +++ b/test/payment/TokenEscrow.behavior.js @@ -0,0 +1,122 @@ +const { expectThrow } = require('../helpers/expectThrow'); +const { EVMRevert } = require('../helpers/EVMRevert'); +const expectEvent = require('../helpers/expectEvent'); + +const BigNumber = web3.BigNumber; + +require('chai') + .use(require('chai-bignumber')(BigNumber)) + .should(); + +function shouldBehaveLikeTokenEscrow (owner, [payee1, payee2]) { + const amount = new BigNumber(100); + const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); + + it('stores the token\'s address', async function () { + const address = await this.escrow.token(); + address.should.be.equal(this.token.address); + }); + + context('when not approved by payer', function () { + it('reverts on deposits', async function () { + await expectThrow( + this.escrow.deposit(payee1, amount, { from: owner }), + EVMRevert + ); + }); + }); + + context('when approved by payer', function () { + beforeEach(async function () { + this.token.approve(this.escrow.address, MAX_UINT256, { from: owner }); + }); + + describe('deposits', function () { + it('accepts a single deposit', async function () { + await this.escrow.deposit(payee1, amount, { from: owner }); + + (await this.token.balanceOf(this.escrow.address)).should.be.bignumber.equal(amount); + + (await this.escrow.depositsOf(payee1)).should.be.bignumber.equal(amount); + }); + + it('accepts an empty deposit', async function () { + await this.escrow.deposit(payee1, new BigNumber(0), { from: owner }); + }); + + it('reverts when non-owners deposit', async function () { + await expectThrow(this.escrow.deposit(payee1, amount, { from: payee2 }), EVMRevert); + }); + + it('emits a deposited event', async function () { + const receipt = await this.escrow.deposit(payee1, amount, { from: owner }); + + const event = expectEvent.inLogs(receipt.logs, 'Deposited', { payee: payee1 }); + event.args.tokenAmount.should.be.bignumber.equal(amount); + }); + + it('adds multiple deposits on a single account', async function () { + await this.escrow.deposit(payee1, amount, { from: owner }); + await this.escrow.deposit(payee1, amount * 2, { from: owner }); + + (await this.token.balanceOf(this.escrow.address)).should.be.bignumber.equal(amount * 3); + + (await this.escrow.depositsOf(payee1)).should.be.bignumber.equal(amount * 3); + }); + + it('tracks deposits to multiple accounts', async function () { + await this.escrow.deposit(payee1, amount, { from: owner }); + await this.escrow.deposit(payee2, amount * 2, { from: owner }); + + (await this.escrow.depositsOf(payee1)).should.be.bignumber.equal(amount); + (await this.escrow.depositsOf(payee2)).should.be.bignumber.equal(amount * 2); + + (await this.token.balanceOf(this.escrow.address)).should.be.bignumber.equal(amount * 3); + }); + }); + + context('with deposit', function () { + beforeEach(async function () { + await this.escrow.deposit(payee1, amount, { from: owner }); + }); + + describe('withdrawals', function () { + it('withdraws payments', async function () { + const payeeInitialBalance = await this.token.balanceOf(payee1); + + await this.escrow.withdraw(payee1, { from: owner }); + + (await this.token.balanceOf(this.escrow.address)).should.be.bignumber.equal(0); + + (await this.escrow.depositsOf(payee1)).should.be.bignumber.equal(0); + + const payeeFinalBalance = await this.token.balanceOf(payee1); + payeeFinalBalance.sub(payeeInitialBalance).should.be.bignumber.equal(amount); + }); + + it('accepts empty withdrawal', async function () { + await this.escrow.withdraw(payee1, { from: owner }); + + (await this.escrow.depositsOf(payee1)).should.be.bignumber.equal(0); + + await this.escrow.withdraw(payee1, { from: owner }); + }); + + it('reverts when non-owners withdraw', async function () { + await expectThrow(this.escrow.withdraw(payee1, { from: payee1 }), EVMRevert); + }); + + it('emits a withdrawn event', async function () { + const receipt = await this.escrow.withdraw(payee1, { from: owner }); + + const event = expectEvent.inLogs(receipt.logs, 'Withdrawn', { payee: payee1 }); + event.args.tokenAmount.should.be.bignumber.equal(amount); + }); + }); + }); + }); +} + +module.exports = { + shouldBehaveLikeTokenEscrow, +}; diff --git a/test/payment/TokenEscrow.test.js b/test/payment/TokenEscrow.test.js index 43738e93ad4..656a9935bf9 100644 --- a/test/payment/TokenEscrow.test.js +++ b/test/payment/TokenEscrow.test.js @@ -1,18 +1,13 @@ +const { shouldBehaveLikeTokenEscrow } = require('./TokenEscrow.behavior'); const { expectThrow } = require('../helpers/expectThrow'); const { EVMRevert } = require('../helpers/EVMRevert'); -const expectEvent = require('../helpers/expectEvent'); const BigNumber = web3.BigNumber; -require('chai') - .use(require('chai-bignumber')(BigNumber)) - .should(); - const TokenEscrow = artifacts.require('TokenEscrow'); const StandardToken = artifacts.require('StandardTokenMock'); -contract('TokenEscrow', function ([_, owner, payee1, payee2]) { - const amount = new BigNumber(100); +contract('TokenEscrow', function ([_, owner, ...otherAccounts]) { const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); @@ -25,111 +20,9 @@ contract('TokenEscrow', function ([_, owner, payee1, payee2]) { context('with token', function () { beforeEach(async function () { this.token = await StandardToken.new(owner, MAX_UINT256); - this.tokenEscrow = await TokenEscrow.new(this.token.address, { from: owner }); - }); - - it('stores the token\'s address', async function () { - const address = await this.tokenEscrow.token(); - address.should.be.equal(this.token.address); - }); - - context('when not approved by payer', function () { - it('reverts on deposits', async function () { - await expectThrow( - this.tokenEscrow.deposit(payee1, amount, { from: owner }), - EVMRevert - ); - }); + this.escrow = await TokenEscrow.new(this.token.address, { from: owner }); }); - context('when approved by payer', function () { - beforeEach(async function () { - this.token.approve(this.tokenEscrow.address, MAX_UINT256, { from: owner }); - }); - - describe('deposits', function () { - it('accepts a single deposit', async function () { - await this.tokenEscrow.deposit(payee1, amount, { from: owner }); - - (await this.token.balanceOf(this.tokenEscrow.address)).should.be.bignumber.equal(amount); - - (await this.tokenEscrow.depositsOf(payee1)).should.be.bignumber.equal(amount); - }); - - it('accepts an empty deposit', async function () { - await this.tokenEscrow.deposit(payee1, new BigNumber(0), { from: owner }); - }); - - it('reverts when non-owners deposit', async function () { - await expectThrow(this.tokenEscrow.deposit(payee1, amount, { from: payee2 }), EVMRevert); - }); - - it('emits a deposited event', async function () { - const receipt = await this.tokenEscrow.deposit(payee1, amount, { from: owner }); - - const event = expectEvent.inLogs(receipt.logs, 'Deposited', { payee: payee1 }); - event.args.tokenAmount.should.be.bignumber.equal(amount); - }); - - it('adds multiple deposits on a single account', async function () { - await this.tokenEscrow.deposit(payee1, amount, { from: owner }); - await this.tokenEscrow.deposit(payee1, amount * 2, { from: owner }); - - (await this.token.balanceOf(this.tokenEscrow.address)).should.be.bignumber.equal(amount * 3); - - (await this.tokenEscrow.depositsOf(payee1)).should.be.bignumber.equal(amount * 3); - }); - - it('tracks deposits to multiple accounts', async function () { - await this.tokenEscrow.deposit(payee1, amount, { from: owner }); - await this.tokenEscrow.deposit(payee2, amount * 2, { from: owner }); - - (await this.tokenEscrow.depositsOf(payee1)).should.be.bignumber.equal(amount); - (await this.tokenEscrow.depositsOf(payee2)).should.be.bignumber.equal(amount * 2); - - (await this.token.balanceOf(this.tokenEscrow.address)).should.be.bignumber.equal(amount * 3); - }); - }); - - context('with deposit', function () { - beforeEach(async function () { - await this.tokenEscrow.deposit(payee1, amount, { from: owner }); - }); - - describe('withdrawals', function () { - it('withdraws payments', async function () { - const payeeInitialBalance = await this.token.balanceOf(payee1); - - await this.tokenEscrow.withdraw(payee1, { from: owner }); - - (await this.token.balanceOf(this.tokenEscrow.address)).should.be.bignumber.equal(0); - - (await this.tokenEscrow.depositsOf(payee1)).should.be.bignumber.equal(0); - - const payeeFinalBalance = await this.token.balanceOf(payee1); - payeeFinalBalance.sub(payeeInitialBalance).should.be.bignumber.equal(amount); - }); - - it('accepts empty withdrawal', async function () { - await this.tokenEscrow.withdraw(payee1, { from: owner }); - - (await this.tokenEscrow.depositsOf(payee1)).should.be.bignumber.equal(0); - - await this.tokenEscrow.withdraw(payee1, { from: owner }); - }); - - it('reverts when non-owners withdraw', async function () { - await expectThrow(this.tokenEscrow.withdraw(payee1, { from: payee1 }), EVMRevert); - }); - - it('emits a withdrawn event', async function () { - const receipt = await this.tokenEscrow.withdraw(payee1, { from: owner }); - - const event = expectEvent.inLogs(receipt.logs, 'Withdrawn', { payee: payee1 }); - event.args.tokenAmount.should.be.bignumber.equal(amount); - }); - }); - }); - }); + shouldBehaveLikeTokenEscrow(owner, otherAccounts); }); });