zcash-grant-system/contract/contracts/CrowdFund.sol

302 lines
12 KiB
Solidity

pragma solidity ^0.4.24;
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
contract CrowdFund {
using SafeMath for uint256;
enum FreezeReason {
CALLER_IS_TRUSTEE,
CROWD_FUND_FAILED,
MAJORITY_VOTING_TO_REFUND
}
FreezeReason freezeReason;
struct Milestone {
uint amount;
uint amountVotingAgainstPayout;
uint payoutRequestVoteDeadline;
bool paid;
}
struct Contributor {
uint contributionAmount;
// array index bool reflect milestone index vote
bool[] milestoneNoVotes;
bool refundVote;
bool refunded;
}
event Deposited(address indexed payee, uint256 weiAmount);
event Withdrawn(address indexed payee, uint256 weiAmount);
bool public frozen;
bool public isRaiseGoalReached;
bool public immediateFirstMilestonePayout;
uint public milestoneVotingPeriod;
uint public deadline;
uint public raiseGoal;
uint public amountRaised;
uint public frozenBalance;
uint public minimumContributionAmount;
uint public amountVotingForRefund;
address public beneficiary;
mapping(address => Contributor) public contributors;
address[] public contributorList;
// authorized addresses to ask for milestone payouts
address[] public trustees;
// constructor ensures that all values combined equal raiseGoal
Milestone[] public milestones;
constructor(
uint _raiseGoal,
address _beneficiary,
address[] _trustees,
uint[] _milestones,
uint _deadline,
uint _milestoneVotingPeriod,
bool _immediateFirstMilestonePayout)
public {
require(_raiseGoal >= 1 ether, "Raise goal is smaller than 1 ether");
require(_trustees.length >= 1 && _trustees.length <= 10, "Trustee addresses must be at least 1 and not more than 10");
require(_milestones.length >= 1 && _milestones.length <= 10, "Milestones must be at least 1 and not more than 10");
// TODO - require minimum duration
// 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,
payoutRequestVoteDeadline: 0,
amountVotingAgainstPayout: 0,
paid: false
}));
}
require(milestoneTotal == _raiseGoal, "Milestone total must equal raise goal");
// TODO - increase minimum contribution amount is 0.1% of raise goal
minimumContributionAmount = 1;
raiseGoal = _raiseGoal;
beneficiary = _beneficiary;
trustees = _trustees;
deadline = now + _deadline;
milestoneVotingPeriod = _milestoneVotingPeriod;
immediateFirstMilestonePayout = _immediateFirstMilestonePayout;
isRaiseGoalReached = false;
amountVotingForRefund = 0;
frozen = false;
// assumes no ether contributed as part of contract deployment
amountRaised = 0;
}
function contribute() public payable onlyOnGoing onlyUnfrozen {
// don't allow overfunding
uint newAmountRaised = amountRaised.add(msg.value);
require(newAmountRaised <= raiseGoal, "Contribution exceeds the raise goal.");
// require minimumContributionAmount (set during construction)
// there may be a special case where just enough has been raised so that the remaining raise amount is just smaller than the minimumContributionAmount
// in this case, allow that the msg.value + amountRaised will equal the raiseGoal.
// This makes sure that we don't enter a scenario where a proposal can never be fully funded
bool greaterThanMinimum = msg.value >= minimumContributionAmount;
bool exactlyRaiseGoal = newAmountRaised == raiseGoal;
require(greaterThanMinimum || exactlyRaiseGoal, "msg.value greater than minimum, or msg.value == remaining amount to be raised");
// in cases where an address pays > 1 times
if (contributors[msg.sender].contributionAmount == 0) {
contributors[msg.sender] = Contributor({
contributionAmount: msg.value,
milestoneNoVotes: new bool[](milestones.length),
refundVote: false,
refunded: false
});
contributorList.push(msg.sender);
}
else {
contributors[msg.sender].contributionAmount = contributors[msg.sender].contributionAmount.add(msg.value);
}
amountRaised = newAmountRaised;
if (amountRaised == raiseGoal) {
isRaiseGoalReached = true;
}
emit Deposited(msg.sender, msg.value);
}
function requestMilestonePayout (uint index) public onlyTrustee onlyRaised onlyUnfrozen {
bool milestoneAlreadyPaid = milestones[index].paid;
bool voteDeadlineHasPassed = milestones[index].payoutRequestVoteDeadline > now;
bool majorityAgainstPayout = isMajorityVoting(milestones[index].amountVotingAgainstPayout);
// 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
if (milestones[index].payoutRequestVoteDeadline == 0) {
if (index == 0 && immediateFirstMilestonePayout) {
// make milestone payouts immediately avtheailable for the first milestone if immediateFirstMilestonePayout is set during consutrction
milestones[index].payoutRequestVoteDeadline = 1;
} else {
milestones[index].payoutRequestVoteDeadline = now.add(milestoneVotingPeriod);
}
}
// if the payoutRequestVoteDealine has passed and majority voted against it previously, begin the grace period with 2 times the deadline
else if (voteDeadlineHasPassed && majorityAgainstPayout) {
milestones[index].payoutRequestVoteDeadline = now.add(milestoneVotingPeriod.mul(2));
}
}
function voteMilestonePayout(uint index, bool vote) public onlyContributor onlyRaised onlyUnfrozen {
bool existingMilestoneNoVote = contributors[msg.sender].milestoneNoVotes[index];
require(existingMilestoneNoVote != vote, "Vote value must be different than existing vote state");
bool milestoneVotingStarted = milestones[index].payoutRequestVoteDeadline > 0;
bool votePeriodHasEnded = milestones[index].payoutRequestVoteDeadline <= now;
bool onGoingVote = milestoneVotingStarted && !votePeriodHasEnded;
require(onGoingVote, "Milestone voting must be open");
contributors[msg.sender].milestoneNoVotes[index] = vote;
if (!vote) {
milestones[index].amountVotingAgainstPayout = milestones[index].amountVotingAgainstPayout.sub(contributors[msg.sender].contributionAmount);
} else {
milestones[index].amountVotingAgainstPayout = milestones[index].amountVotingAgainstPayout.add(contributors[msg.sender].contributionAmount);
}
}
function payMilestonePayout(uint index) public onlyRaised onlyUnfrozen {
bool voteDeadlineHasPassed = milestones[index].payoutRequestVoteDeadline < now;
bool majorityVotedNo = isMajorityVoting(milestones[index].amountVotingAgainstPayout);
bool milestoneAlreadyPaid = milestones[index].paid;
if (voteDeadlineHasPassed && !majorityVotedNo && !milestoneAlreadyPaid) {
milestones[index].paid = true;
uint amount = milestones[index].amount;
beneficiary.transfer(amount);
emit Withdrawn(beneficiary, amount);
// if the final milestone just got paid
if (milestones.length.sub(index) == 1) {
// useful to selfdestruct in case funds were forcefully deposited into contract. otherwise they are lost.
selfdestruct(beneficiary);
}
} else {
revert("required conditions were not satisfied");
}
}
function voteRefund(bool vote) public onlyContributor onlyRaised onlyUnfrozen {
bool refundVote = contributors[msg.sender].refundVote;
require(vote != refundVote, "Existing vote state is identical to vote value");
contributors[msg.sender].refundVote = vote;
if (!vote) {
amountVotingForRefund = amountVotingForRefund.sub(contributors[msg.sender].contributionAmount);
} else {
amountVotingForRefund = amountVotingForRefund.add(contributors[msg.sender].contributionAmount);
}
}
function refund() public onlyUnfrozen {
bool callerIsTrustee = isCallerTrustee();
bool crowdFundFailed = isFailed();
bool majorityVotingToRefund = isMajorityVoting(amountVotingForRefund);
require(callerIsTrustee || crowdFundFailed || majorityVotingToRefund, "Required conditions for refund are not met");
if (callerIsTrustee) {
freezeReason = FreezeReason.CALLER_IS_TRUSTEE;
} else if (crowdFundFailed) {
freezeReason = FreezeReason.CROWD_FUND_FAILED;
} else {
freezeReason = FreezeReason.MAJORITY_VOTING_TO_REFUND;
}
frozen = true;
frozenBalance = address(this).balance;
}
// anyone can refund a contributor if a crowdfund has been frozen
function withdraw(address refundAddress) public onlyFrozen {
require(frozen, "CrowdFund is not frozen");
bool isRefunded = contributors[refundAddress].refunded;
require(!isRefunded, "Specified address is already refunded");
contributors[refundAddress].refunded = true;
uint contributionAmount = contributors[refundAddress].contributionAmount;
uint amountToRefund = contributionAmount.mul(address(this).balance).div(frozenBalance);
refundAddress.transfer(amountToRefund);
emit Withdrawn(refundAddress, amountToRefund);
}
// it may be useful to selfdestruct in case funds were force deposited to the contract
function destroy() public onlyTrustee onlyFrozen {
for (uint i = 0; i < contributorList.length; i++) {
address contributorAddress = contributorList[i];
if (!contributors[contributorAddress].refunded) {
revert("At least one contributor has not yet refunded");
}
}
selfdestruct(beneficiary);
}
function isMajorityVoting(uint valueVoting) public view returns (bool) {
return valueVoting.mul(2) > amountRaised;
}
function isCallerTrustee() public view returns (bool) {
for (uint i = 0; i < trustees.length; i++) {
if (msg.sender == trustees[i]) {
return true;
}
}
return false;
}
function isFailed() public view returns (bool) {
return now >= deadline && !isRaiseGoalReached;
}
function getContributorMilestoneVote(address contributorAddress, uint milestoneIndex) public view returns (bool) {
return contributors[contributorAddress].milestoneNoVotes[milestoneIndex];
}
function getContributorContributionAmount(address contributorAddress) public view returns (uint) {
return contributors[contributorAddress].contributionAmount;
}
function getFreezeReason() public view returns (uint) {
return uint(freezeReason);
}
modifier onlyFrozen() {
require(frozen, "CrowdFund is not frozen");
_;
}
modifier onlyUnfrozen() {
require(!frozen, "CrowdFund is frozen");
_;
}
modifier onlyRaised() {
require(isRaiseGoalReached, "Raise goal is not reached");
_;
}
modifier onlyOnGoing() {
require(now <= deadline && !isRaiseGoalReached, "CrowdFund is not ongoing");
_;
}
modifier onlyContributor() {
require(contributors[msg.sender].contributionAmount != 0, "Caller is not a contributor");
_;
}
modifier onlyTrustee() {
require(isCallerTrustee(), "Caller is not a trustee");
_;
}
}