-
Notifications
You must be signed in to change notification settings - Fork 12k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
175 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |