From fe63f2e19c3d6f02d1d864f09811bdaf7bfde0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 15 Jun 2018 17:37:41 -0300 Subject: [PATCH] Added RefundableEscrow. --- contracts/payment/RefundableEscrow.sol | 65 +++++++++++++++ test/payment/Escrow.behaviour.js | 2 +- test/payment/RefundableEscrow.test.js | 109 +++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 contracts/payment/RefundableEscrow.sol create mode 100644 test/payment/RefundableEscrow.test.js diff --git a/contracts/payment/RefundableEscrow.sol b/contracts/payment/RefundableEscrow.sol new file mode 100644 index 00000000000..1031c1d4a6d --- /dev/null +++ b/contracts/payment/RefundableEscrow.sol @@ -0,0 +1,65 @@ +pragma solidity ^0.4.23; + +import "./ConditionalEscrow.sol"; +import "../ownership/Ownable.sol"; + + +/** + * @title RefundableEscrow + * @dev Escrow that holds investor funds for a unique benefitiary, and allows for + * either withdrawal by the benefiatiary, or refunds to the investors. + */ +contract RefundableEscrow is ConditionalEscrow, Ownable { + enum State { Active, Refunding, Closed } + + event Closed(); + event RefundsEnabled(); + + State public state; + address public beneficiary; + + constructor(address _beneficiary) public { + require(_beneficiary != address(0)); + beneficiary = _beneficiary; + state = State.Active; + } + + function invest() payable public { + require(state == State.Active); + super.deposit(msg.sender); + } + + // Disable the base deposit function, use invest instead. + function deposit(address _payee) payable public { + revert(); + } + + function close() onlyOwner public { + require(state == State.Active); + state = State.Closed; + emit Closed(); + } + + function enableRefunds() onlyOwner public { + require(state == State.Active); + state = State.Refunding; + emit RefundsEnabled(); + } + + function withdrawalAllowed(address _payee) public view returns (bool) { + return state == State.Refunding; + } + + function withdraw(address _payee) public { + if (_payee == beneficiary) { + beneficiaryWithdrawal(); + } else { + super.withdraw(_payee); + } + } + + function beneficiaryWithdrawal() internal { + require(state == State.Closed); + beneficiary.transfer(address(this).balance); + } +} diff --git a/test/payment/Escrow.behaviour.js b/test/payment/Escrow.behaviour.js index 1c43817f153..d1540b0d2de 100644 --- a/test/payment/Escrow.behaviour.js +++ b/test/payment/Escrow.behaviour.js @@ -49,7 +49,7 @@ export default function ([payer1, payer2, payee1, payee2]) { }); it('can withdraw payments from any account', async function () { - let payeeInitialBalance = await web3.eth.getBalance(payee1); + const payeeInitialBalance = await web3.eth.getBalance(payee1); await this.contract.deposit(payee1, { from: payer1, value: amount }); await this.contract.withdraw(payee1, { from: payer2 }); diff --git a/test/payment/RefundableEscrow.test.js b/test/payment/RefundableEscrow.test.js new file mode 100644 index 00000000000..6b1bafd57c3 --- /dev/null +++ b/test/payment/RefundableEscrow.test.js @@ -0,0 +1,109 @@ +import EVMRevert from '../helpers/EVMRevert'; + +const BigNumber = web3.BigNumber; + +require('chai') + .use(require('chai-bignumber')(BigNumber)) + .should(); + +const RefundableEscrow = artifacts.require('RefundableEscrow'); + +contract('RefundableEscrow', function ([owner, beneficiary, investor1, investor2]) { + const amount = web3.toWei(54.0, 'ether'); + const investors = [investor1, investor2]; + + beforeEach(async function () { + this.contract = await RefundableEscrow.new(beneficiary); + }); + + it('disallows direct deposits', async function () { + await this.contract.deposit(investor1, { from: investor1, value: amount }).should.be.rejectedWith(EVMRevert); + }); + + context('active state', function () { + it('accepts investments', async function () { + await this.contract.invest({ from: investor1, value: amount }); + + const investment = await this.contract.deposits(investor1); + investment.should.be.bignumber.equal(amount); + }); + + it('does not refund investors', async function () { + await this.contract.invest({ from: investor1, value: amount }); + await this.contract.withdraw(investor1).should.be.rejectedWith(EVMRevert); + }); + + it('does not allow beneficiary withdrawal', async function () { + await this.contract.invest({ from: investor1, value: amount }); + await this.contract.withdraw(beneficiary).should.be.rejectedWith(EVMRevert); + }); + }); + + it('only owner can enter closed state', async function () { + await this.contract.close({ from: beneficiary }).should.be.rejectedWith(EVMRevert); + + const receipt = await this.contract.close({ from: owner }); + + receipt.logs.length.should.equal(1); + receipt.logs[0].event.should.equal('Closed'); + }); + + context('closed state', function () { + beforeEach(async function () { + await Promise.all(investors.map(investor => this.contract.invest({ from: investor, value: amount }))); + + await this.contract.close({ from: owner }); + }); + + it('rejects investments', async function () { + await this.contract.invest({ from: investor1, value: amount }).should.be.rejectedWith(EVMRevert); + }); + + it('does not refund investors', async function () { + await this.contract.withdraw(investor1).should.be.rejectedWith(EVMRevert); + }); + + it('allows beneficiary withdrawal', async function () { + const beneficiaryInitialBalance = await web3.eth.getBalance(beneficiary); + await this.contract.withdraw(beneficiary); + const beneficiaryFinalBalance = await web3.eth.getBalance(beneficiary); + + beneficiaryFinalBalance.sub(beneficiaryInitialBalance).should.be.bignumber.equal(amount * investors.length); + }); + }); + + it('only owner can enter refund state', async function () { + await this.contract.enableRefunds({ from: beneficiary }).should.be.rejectedWith(EVMRevert); + + const receipt = await this.contract.enableRefunds({ from: owner }); + + receipt.logs.length.should.equal(1); + receipt.logs[0].event.should.equal('RefundsEnabled'); + }); + + context('refund state', function () { + beforeEach(async function () { + await Promise.all(investors.map(investor => this.contract.invest({ from: investor, value: amount }))); + + await this.contract.enableRefunds({ from: owner }); + }); + + it('rejects investments', async function () { + await this.contract.invest({ from: investor1, value: amount }).should.be.rejectedWith(EVMRevert); + }); + + it('refunds investors', async function () { + for (let investor of [investor1, investor2]) { + const investorInitialBalance = await web3.eth.getBalance(investor); + await this.contract.withdraw(investor); + const investorFinalBalance = await web3.eth.getBalance(investor); + + investorFinalBalance.sub(investorInitialBalance).should.be.bignumber.equal(amount); + } + }); + + it('does not allow beneficiary withdrawal', async function () { + await this.contract.withdraw(beneficiary).should.be.rejectedWith(EVMRevert); + }); + }); +});