Skip to content

Commit

Permalink
Added RefundableEscrow.
Browse files Browse the repository at this point in the history
  • Loading branch information
nventuro committed Jun 15, 2018
1 parent 3600240 commit fe63f2e
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 1 deletion.
65 changes: 65 additions & 0 deletions contracts/payment/RefundableEscrow.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 1 addition & 1 deletion test/payment/Escrow.behaviour.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
109 changes: 109 additions & 0 deletions test/payment/RefundableEscrow.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});

0 comments on commit fe63f2e

Please sign in to comment.