-
Notifications
You must be signed in to change notification settings - Fork 1
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
4 changed files
with
428 additions
and
56 deletions.
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 |
---|---|---|
@@ -1,31 +1,349 @@ | ||
pragma solidity ^0.4.24; | ||
|
||
// to enable Params passing to constructor and method | ||
pragma experimental ABIEncoderV2; | ||
import "zeppelin-solidity/contracts/math/SafeMath.sol"; | ||
import "zeppelin-solidity/contracts/ownership/Ownable.sol"; | ||
import "zeppelin-solidity/contracts/token/ERC20/ERC20.sol"; | ||
|
||
import "@thetta/core/contracts/DaoClient.sol"; | ||
import "@thetta/core/contracts/IDaoBase.sol"; | ||
import "@thetta/core/contracts/tokens/StdDaoToken.sol"; | ||
import "./DaicoProject.sol"; | ||
/** | ||
* @title Daico | ||
*/ | ||
contract Daico is Ownable { | ||
|
||
using SafeMath for uint; | ||
|
||
contract Daico is DaoClient { | ||
mapping(uint => DaicoProject) public projects; | ||
uint projectsCount; | ||
address[] public investors; | ||
ERC20 public daiToken; | ||
ERC20 public projectToken; | ||
|
||
bytes32 public NEXT_STAGE = keccak256("nextStage"); | ||
address public projectOwner; | ||
|
||
constructor(IDaoBase _daoBase, address[] _investors) DaoClient(_daoBase) { | ||
investors = _investors; | ||
uint public minQuorumRate; | ||
uint public minVoteRate; | ||
uint public tapsCount; | ||
uint public tokenHoldersCount; | ||
uint[] public tapAmounts; | ||
uint[] public tapTimestampsFinishAt; | ||
|
||
enum VotingType { ReleaseTap, ReleaseTapDecreasedQuorum, ChangeRoadmap, TerminateProject } | ||
enum VotingResult { Accept, Decline, QuorumNotReached, NoDecision } | ||
|
||
mapping(uint => mapping(uint => uint)) public tapVotings; | ||
mapping(uint => uint) public tapVotingsCount; | ||
|
||
mapping(uint => Voting) public votings; | ||
uint public votingsCount; | ||
|
||
mapping(uint => TapPayment) public tapPayments; | ||
|
||
struct TapPayment { | ||
uint amount; | ||
uint createdAt; | ||
bool isWithdrawn; | ||
} | ||
|
||
struct Voting { | ||
uint tapIndex; | ||
uint yesVotesCount; | ||
uint noVotesCount; | ||
uint quorumRate; | ||
uint createdAt; | ||
uint finishAt; | ||
VotingType votingType; | ||
mapping(address => bool) voted; | ||
} | ||
|
||
/** | ||
* Modifiers | ||
*/ | ||
|
||
/** | ||
* Modifier checks that method can be called only by investor / project token holder | ||
*/ | ||
modifier onlyInvestor() { | ||
require(projectToken.balanceOf(msg.sender) > 0); | ||
_; | ||
} | ||
|
||
/** | ||
* Modifier checks that tap index exists | ||
*/ | ||
modifier validTapIndex(uint _tapIndex) { | ||
require(_tapIndex < tapsCount); | ||
_; | ||
} | ||
|
||
function addNewProject(uint _stagesCount, uint _stageAmount) { | ||
DaicoProject project = new DaicoProject(_stagesCount, _stageAmount, msg.sender, address(this)); | ||
projects[projectsCount] = project; | ||
projectsCount++; | ||
/** | ||
* Modifier checks that voting index exists | ||
*/ | ||
modifier validVotingIndex(uint _votingIndex) { | ||
require(_votingIndex < votingsCount); | ||
_; | ||
} | ||
|
||
function addAmountToFund() public payable {} | ||
/** | ||
* @dev Contract constructor | ||
* @param _daiTokenAddress address of the DAI token contract, project gets payments in DAI tokens | ||
* @param _projectTokenAddress project token address, investors hold this token | ||
* @param _projectOwnerAddress project owner address who can receive tap payments | ||
* @param _tapsCount how many times project should get payments, NOTICE: we can get taps count from _tapAmounts.length but contract deployer can force so that _tapsCount != _tapAmounts.length | ||
* @param _tapAmounts array of DAI token amounts in wei that describes how many tokens project gets per single stage | ||
* @param _tapTimestampsFinishAt array of deadline timestamps, project should get payment before each deadline timestamp | ||
* @param _minQuorumRate min quorum rate, 100 == 100% | ||
* @param _minVoteRate min vote rate for proposal to be accepted/declined, 100 == 100% | ||
* @param _tokenHoldersCount amount of token holders | ||
*/ | ||
constructor( | ||
address _daiTokenAddress, | ||
address _projectTokenAddress, | ||
address _projectOwnerAddress, | ||
uint _tapsCount, | ||
uint[] _tapAmounts, | ||
uint[] _tapTimestampsFinishAt, | ||
uint _minQuorumRate, | ||
uint _minVoteRate, | ||
uint _tokenHoldersCount | ||
) public { | ||
// validation | ||
require(_daiTokenAddress != address(0)); | ||
require(_projectTokenAddress != address(0)); | ||
require(_projectOwnerAddress != address(0)); | ||
require(_tapsCount > 0); | ||
require(_tapAmounts.length == _tapsCount); | ||
require(_tapTimestampsFinishAt.length == _tapsCount); | ||
require(_minQuorumRate > 0); | ||
require(_minVoteRate > 0); | ||
require(_tokenHoldersCount > 0); | ||
// setting contract properties | ||
daiToken = ERC20(_daiTokenAddress); | ||
projectToken = ERC20(_projectTokenAddress); | ||
projectOwner = _projectOwnerAddress; | ||
tapsCount = _tapsCount; | ||
tapAmounts = _tapAmounts; | ||
tapTimestampsFinishAt = _tapTimestampsFinishAt; | ||
minQuorumRate = _minQuorumRate; | ||
minVoteRate = _minVoteRate; | ||
tokenHoldersCount = _tokenHoldersCount; | ||
// create initial ReleaseTap votings for all taps | ||
for(uint i = 0; i < tapsCount; i++) { | ||
uint createdAt = tapTimestampsFinishAt[i] - 7 days; | ||
_createVoting(i, minQuorumRate, createdAt, tapTimestampsFinishAt[i], VotingType.ReleaseTap); | ||
} | ||
} | ||
|
||
/** | ||
* Public methods | ||
*/ | ||
|
||
/** | ||
* @dev Returns voting result | ||
* @param _votingIndex voting index | ||
* @return voting result | ||
*/ | ||
function getVotingResult(uint _votingIndex) public view validVotingIndex(_votingIndex) returns(VotingResult) { | ||
Voting memory voting = votings[_votingIndex]; | ||
uint totalVotesCount = voting.yesVotesCount.add(voting.noVotesCount); | ||
// check whether quorum is reached | ||
if(totalVotesCount.mul(100) <= tokenHoldersCount.mul(voting.quorumRate)) { | ||
return VotingResult.QuorumNotReached; | ||
} | ||
// check whether voting result is strongly accepted | ||
if(voting.yesVotesCount.mul(100) >= totalVotesCount.mul(minVoteRate)) { | ||
return VotingResult.Accept; | ||
} | ||
// check whether voting result is strongly declined | ||
if(voting.noVotesCount.mul(100) >= totalVotesCount.mul(minVoteRate)) { | ||
return VotingResult.Decline; | ||
} | ||
// by default return no decision result | ||
return VotingResult.NoDecision; | ||
} | ||
|
||
/** | ||
* @dev Checks whether project is terminated | ||
* @return is project terminated | ||
*/ | ||
function isProjectTerminated() public view returns(bool) { | ||
bool isTerminated = false; | ||
Voting memory latestVoting = votings[votingsCount.sub(1)]; | ||
// if latest voting is of type TerminateProject and result Accept then set isTerminated to true | ||
if((latestVoting.votingType == VotingType.TerminateProject) && (getVotingResult(votingsCount.sub(1)) == VotingResult.Accept)) { | ||
isTerminated = true; | ||
} | ||
return isTerminated; | ||
} | ||
|
||
/** | ||
* @dev Checks whether tap withdraw is accepted by investors for project owner | ||
* @param _tapIndex tap index | ||
* @return whether withdraw is accepted | ||
*/ | ||
function isTapWithdrawAcceptedByInvestors(uint _tapIndex) public view validTapIndex(_tapIndex) returns(bool) { | ||
bool isWithdrawAccepted = false; | ||
// get latest voting for tap | ||
uint latestVotingIndex = tapVotings[_tapIndex][tapVotingsCount[_tapIndex].sub(1)]; | ||
Voting memory voting = votings[latestVotingIndex]; | ||
bool isVotingAccepted = getVotingResult(latestVotingIndex) == VotingResult.Accept; | ||
// if voting of type ReleaseTap and result is Accept then set isWithdrawAccepted to true | ||
if((voting.votingType == VotingType.ReleaseTap) && isVotingAccepted) { | ||
isWithdrawAccepted = true; | ||
} | ||
// if voting of type ReleaseTapDecreasedQuorum and result is Accept then set isWithdrawAccepted to true | ||
if((voting.votingType == VotingType.ReleaseTapDecreasedQuorum) && isVotingAccepted) { | ||
isWithdrawAccepted = true; | ||
} | ||
// if voting of type ChangeRoadmap and result is Accept then set isWithdrawAccepted to true | ||
if((voting.votingType == VotingType.ChangeRoadmap) && isVotingAccepted) { | ||
isWithdrawAccepted = true; | ||
} | ||
return isWithdrawAccepted; | ||
} | ||
|
||
/** | ||
* Investor methods | ||
*/ | ||
|
||
/** | ||
* @dev Creates a new voting by investor. Investors can create votings of 2 types: ChangeRoadmap and TerminateProject. | ||
* @param _tapIndex tap index | ||
* @param _votingType voting type | ||
*/ | ||
function createVotingByInvestor(uint _tapIndex, VotingType _votingType) external onlyInvestor validTapIndex(_tapIndex) { | ||
// common validation | ||
require(_votingType == VotingType.ChangeRoadmap || _votingType == VotingType.TerminateProject); | ||
uint latestVotingIndex = tapVotings[_tapIndex][tapVotingsCount[_tapIndex].sub(1)]; | ||
Voting memory latestVoting = votings[latestVotingIndex]; | ||
require(now >= latestVoting.finishAt); | ||
|
||
// if investor wants to create voting of type ChangeRoadmap | ||
if(_votingType == VotingType.ChangeRoadmap) { | ||
// check that latest voting is of type ReleaseTap or ReleaseTapDecreasedQuorum | ||
require(latestVoting.votingType == VotingType.ReleaseTap || latestVoting.votingType == VotingType.ReleaseTapDecreasedQuorum); | ||
// check that latest voting result is no decision | ||
require(getVotingResult(latestVotingIndex) == VotingResult.NoDecision); | ||
// create a new voting | ||
_createVoting(_tapIndex, minQuorumRate, now + 3 weeks, now + 4 weeks, VotingType.ChangeRoadmap); | ||
} | ||
|
||
// if investor wants to create voting of type TerminateProject | ||
if(_votingType == VotingType.TerminateProject) { | ||
// check that latest voting result is decline | ||
require(getVotingResult(latestVotingIndex) == VotingResult.Decline); | ||
// create a new voting | ||
_createVoting(_tapIndex, minQuorumRate, now, now + 2 weeks, VotingType.TerminateProject); | ||
} | ||
} | ||
|
||
/** | ||
* @dev Voting by investor | ||
* @param _votingIndex voting index | ||
* @param _isYes positive/negative decision | ||
*/ | ||
function vote(uint _votingIndex, bool _isYes) external onlyInvestor validVotingIndex(_votingIndex) { | ||
// validation | ||
require(now >= votings[_votingIndex].createdAt); | ||
require(now < votings[_votingIndex].finishAt); | ||
require(!votings[_votingIndex].voted[msg.sender]); | ||
// vote | ||
votings[_votingIndex].voted[msg.sender] = true; | ||
if(_isYes) { | ||
votings[_votingIndex].yesVotesCount = votings[_votingIndex].yesVotesCount.add(1); | ||
} else { | ||
votings[_votingIndex].noVotesCount = votings[_votingIndex].noVotesCount.add(1); | ||
} | ||
} | ||
|
||
/** | ||
* Evercity member / owner methods | ||
*/ | ||
|
||
/** | ||
* @dev Creates a new voting by owner. Owner can create votings only of type ReleaseTapDecreasedQuorum | ||
* @param _tapIndex tap index | ||
* @param _votingType voting type | ||
*/ | ||
function createVotingByOwner(uint _tapIndex, VotingType _votingType) external onlyOwner validTapIndex(_tapIndex) { | ||
// validation | ||
require(_votingType == VotingType.ReleaseTapDecreasedQuorum); | ||
uint latestVotingIndex = tapVotings[_tapIndex][tapVotingsCount[_tapIndex].sub(1)]; | ||
Voting memory latestVoting = votings[latestVotingIndex]; | ||
// check that latest voting is finished | ||
require(now >= latestVoting.finishAt); | ||
// check that latest voting is of type ReleaseTap or ReleaseTapDecreasedQuorum or TerminateProject | ||
require(latestVoting.votingType == VotingType.ReleaseTap || latestVoting.votingType == VotingType.ReleaseTapDecreasedQuorum || latestVoting.votingType == VotingType.TerminateProject); | ||
// check that latest voting result is quorum not reached | ||
require(getVotingResult(latestVotingIndex) == VotingResult.QuorumNotReached); | ||
// create a new voting | ||
_createVoting(_tapIndex, 50, now, now + 7 days, VotingType.ReleaseTapDecreasedQuorum); | ||
} | ||
|
||
/** | ||
* @dev Withdraws DAI tokens in case project is terminated | ||
*/ | ||
function withdrawFunding() external onlyOwner { | ||
// validation | ||
require(isProjectTerminated()); | ||
// calculate amount of DAI tokens to withdraw | ||
uint amountToWithdraw = 0; | ||
for(uint i = 0; i < tapsCount; i++) { | ||
if(!tapPayments[i].isWithdrawn) { | ||
amountToWithdraw = amountToWithdraw.add(tapAmounts[i]); | ||
} | ||
} | ||
// transfer DAI tokens to owner | ||
daiToken.transfer(owner, amountToWithdraw); | ||
} | ||
|
||
/** | ||
* Project owner methods | ||
*/ | ||
|
||
/** | ||
* @dev Withdraws tap payment by project owner | ||
* @param _tapIndex tap index | ||
*/ | ||
function withdrawTapPayment(uint _tapIndex) external validTapIndex(_tapIndex) { | ||
// validation | ||
require(msg.sender == projectOwner); | ||
require(!tapPayments[_tapIndex].isWithdrawn); | ||
require(isTapWithdrawAcceptedByInvestors(_tapIndex)); | ||
// create tap payment | ||
TapPayment memory tapPayment; | ||
tapPayment.amount = tapAmounts[_tapIndex]; | ||
tapPayment.createdAt = now; | ||
tapPayment.isWithdrawn = true; | ||
tapPayments[_tapIndex] = tapPayment; | ||
// transfer DAI tokens for selected tap to project owner | ||
daiToken.transfer(projectOwner, tapAmounts[_tapIndex]); | ||
} | ||
|
||
/** | ||
* Internal methods | ||
*/ | ||
|
||
/** | ||
* @dev Creates a new voting | ||
* @param _tapIndex tap index | ||
* @param _quorumRate quorum rate | ||
* @param _createdAt when voting was created timestamp | ||
* @param _finishAt when voting should be finished timestamp | ||
* @param _votingType voting type | ||
*/ | ||
function _createVoting(uint _tapIndex, uint _quorumRate, uint _createdAt, uint _finishAt, VotingType _votingType) internal validTapIndex(_tapIndex) { | ||
// validation | ||
require(_quorumRate > 0); | ||
require(_createdAt > 0); | ||
require(_finishAt > 0); | ||
// create a new voting | ||
Voting memory voting; | ||
voting.tapIndex = _tapIndex; | ||
voting.quorumRate = _quorumRate; | ||
voting.createdAt = _createdAt; | ||
voting.finishAt = _finishAt; | ||
voting.votingType = _votingType; | ||
votings[votingsCount] = voting; | ||
// update contract properties | ||
tapVotings[_tapIndex][tapVotingsCount[_tapIndex]] = votingsCount; | ||
tapVotingsCount[_tapIndex] = tapVotingsCount[_tapIndex].add(1); | ||
votingsCount = votingsCount.add(1); | ||
} | ||
|
||
} | ||
} |
Oops, something went wrong.