202 lines
7.4 KiB
Solidity
202 lines
7.4 KiB
Solidity
pragma solidity ^0.4.24;
|
|
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
|
|
|
|
contract PrivateFund {
|
|
|
|
using SafeMath for uint256;
|
|
|
|
struct Milestone {
|
|
uint amount;
|
|
bool openRequest;
|
|
bool paid;
|
|
}
|
|
|
|
struct BoardMember {
|
|
bool[] milestoneApprovals;
|
|
address refundAddress;
|
|
// TODO - refactor not to waste space like this;
|
|
bool exists;
|
|
}
|
|
|
|
event Transfered(address payee, uint weiAmount);
|
|
event Deposited(address indexed payee, uint256 weiAmount);
|
|
event Withdrawn(address indexed payee, uint256 weiAmount);
|
|
|
|
uint public amountRaised;
|
|
uint public raiseGoal;
|
|
uint public quorum;
|
|
address public beneficiary;
|
|
address public funder;
|
|
address[] public trustees;
|
|
bool unanimityForRefunds;
|
|
|
|
mapping(address => BoardMember) public boardMembers;
|
|
address[] public boardMembersList;
|
|
// constructor ensures that all values combined equal raiseGoal
|
|
Milestone[] public milestones;
|
|
|
|
constructor(
|
|
uint _raiseGoal,
|
|
address _beneficiary,
|
|
address[] _trustees,
|
|
uint _quorum,
|
|
address[] _boardMembers,
|
|
uint[] _milestones,
|
|
address _funder,
|
|
bool _unanimityForRefunds)
|
|
public {
|
|
require(_raiseGoal >= 1 ether, "Raise goal is smaller than 1 ether");
|
|
require(_milestones.length >= 1, "Milestones must be at least 1");
|
|
require(_quorum <= _boardMembers.length, "quorum is larger than total number of boardMembers");
|
|
require(_quorum >= 1, "quorum must be at least 1");
|
|
// TODO - require minimum milestone voting period
|
|
|
|
// ensure that cumalative milestone payouts equal raiseGoalAmount
|
|
uint milestoneTotal = 0;
|
|
for (uint i = 0; i < _milestones.length; i++) {
|
|
uint milestoneAmount = _milestones[i];
|
|
require(milestoneAmount > 0, "Milestone amount must be greater than 0");
|
|
milestoneTotal = milestoneTotal.add(milestoneAmount);
|
|
milestones.push(Milestone({
|
|
amount: milestoneAmount,
|
|
openRequest: false,
|
|
paid: false
|
|
}));
|
|
}
|
|
require(milestoneTotal == _raiseGoal, "Milestone total must equal raise goal");
|
|
|
|
boardMembersList = _boardMembers;
|
|
for (uint e = 0; e < boardMembersList.length; e++) {
|
|
address boardMemberAddress = boardMembersList[e];
|
|
boardMembers[boardMemberAddress] = BoardMember({
|
|
milestoneApprovals: new bool[](milestones.length),
|
|
refundAddress: 0,
|
|
exists: true
|
|
});
|
|
}
|
|
|
|
quorum = _quorum;
|
|
raiseGoal = _raiseGoal;
|
|
beneficiary = _beneficiary;
|
|
trustees = _trustees;
|
|
funder = _funder;
|
|
unanimityForRefunds = _unanimityForRefunds;
|
|
amountRaised = 0;
|
|
}
|
|
|
|
function contribute() public payable {
|
|
require(msg.sender == funder, "Sender must be funder");
|
|
require(amountRaised.add(msg.value) == raiseGoal, "Contribution must be exactly raise goal");
|
|
amountRaised = msg.value;
|
|
emit Deposited(msg.sender, msg.value);
|
|
}
|
|
|
|
function requestMilestonePayout (uint index) public onlyTrustee onlyRaised {
|
|
bool milestoneAlreadyPaid = milestones[index].paid;
|
|
// prevent requesting paid milestones
|
|
require(!milestoneAlreadyPaid, "Milestone already paid");
|
|
|
|
int lowestIndexPaid = -1;
|
|
for (uint i = 0; i < milestones.length; i++) {
|
|
if (milestones[i].paid) {
|
|
lowestIndexPaid = int(i);
|
|
}
|
|
}
|
|
|
|
require(index == uint(lowestIndexPaid + 1), "Milestone request must be for first unpaid milestone");
|
|
// begin grace period for contributors to vote no on milestone payout
|
|
require(!milestones[index].openRequest, "Milestone must not have already been requested");
|
|
milestones[index].openRequest = true;
|
|
}
|
|
|
|
function voteMilestonePayout(uint index, bool vote) public onlyBoardMember onlyRaised {
|
|
bool existingMilestoneVote = boardMembers[msg.sender].milestoneApprovals[index];
|
|
require(existingMilestoneVote != vote, "Vote value must be different than existing vote state");
|
|
require(milestones[index].openRequest, "Milestone voting must be open");
|
|
boardMembers[msg.sender].milestoneApprovals[index] = vote;
|
|
}
|
|
|
|
function payMilestonePayout(uint index) public onlyRaised {
|
|
bool quorumReached = isQuorumReachedForMilestonePayout(index);
|
|
bool milestoneAlreadyPaid = milestones[index].paid;
|
|
if (quorumReached && !milestoneAlreadyPaid) {
|
|
milestones[index].paid = true;
|
|
milestones[index].openRequest = false;
|
|
fundTransfer(beneficiary, milestones[index].amount);
|
|
// TODO trigger self-destruct with any un-spent funds (since funds could have been force sent at any point)
|
|
} else {
|
|
revert("required conditions were not satisfied");
|
|
}
|
|
}
|
|
|
|
function voteRefundAddress(address refundAddress) public onlyBoardMember onlyRaised {
|
|
boardMembers[msg.sender].refundAddress = refundAddress;
|
|
}
|
|
|
|
function refund(address refundAddress) public onlyBoardMember onlyRaised {
|
|
require(isConsensusReachedForRefund(refundAddress), "Unanimity is not reached to refund to given address");
|
|
selfdestruct(refundAddress);
|
|
}
|
|
|
|
function fundTransfer(address etherReceiver, uint256 amount) private {
|
|
etherReceiver.transfer(amount);
|
|
emit Transfered(etherReceiver, amount);
|
|
}
|
|
|
|
function isConsensusReachedForRefund(address refundAddress) public view onlyRaised returns (bool) {
|
|
uint yesVotes = 0;
|
|
for (uint i = 0; i < boardMembersList.length; i++) {
|
|
address boardMemberAddress = boardMembersList[i];
|
|
address boardMemberRefundAddressSelection = boardMembers[boardMemberAddress].refundAddress;
|
|
if (boardMemberRefundAddressSelection == refundAddress) {
|
|
yesVotes += 1;
|
|
}
|
|
}
|
|
if (unanimityForRefunds) {
|
|
return yesVotes == boardMembersList.length;
|
|
} else {
|
|
return yesVotes >= quorum;
|
|
}
|
|
}
|
|
|
|
function isQuorumReachedForMilestonePayout(uint milestoneIndex) public view onlyRaised returns (bool) {
|
|
uint yesVotes = 0;
|
|
for (uint i = 0; i < boardMembersList.length; i++) {
|
|
address boardMemberAddress = boardMembersList[i];
|
|
bool boardMemberVote = boardMembers[boardMemberAddress].milestoneApprovals[milestoneIndex];
|
|
if (boardMemberVote) {
|
|
yesVotes += 1;
|
|
}
|
|
}
|
|
return yesVotes >= quorum;
|
|
}
|
|
|
|
function isCallerTrustee() public view returns (bool) {
|
|
for (uint i = 0; i < trustees.length; i++) {
|
|
if (msg.sender == trustees[i]) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function getBoardMemberMilestoneVote(address boardMemberAddress, uint milestoneIndex) public view returns (bool) {
|
|
return boardMembers[boardMemberAddress].milestoneApprovals[milestoneIndex];
|
|
}
|
|
|
|
modifier onlyRaised() {
|
|
require(raiseGoal == amountRaised, "Proposal is not funded");
|
|
_;
|
|
}
|
|
|
|
modifier onlyBoardMember() {
|
|
require(boardMembers[msg.sender].exists, "Caller is not a board member");
|
|
_;
|
|
}
|
|
|
|
modifier onlyTrustee() {
|
|
require(isCallerTrustee(), "Caller is not a trustee");
|
|
_;
|
|
}
|
|
|
|
} |