-
Notifications
You must be signed in to change notification settings - Fork 62
/
FundingVault.sol
347 lines (314 loc) · 17.1 KB
/
FundingVault.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
/**
* @title Funding Vault
* @author Clément Lesaege - <clement@lesaege.com>
* Bug Bounties: This code hasn't undertaken a bug bounty program yet.
*/
pragma solidity ^0.4.15;
import "../Arbitrable.sol";
import "minimetoken/contracts/MiniMeToken.sol";
/** @title Funding Vault
* A contract storing the ETH raised in a crowdfunding event.
* Funds are delivered when milestones are reached.
* The team can claim a milestone is reached. Token holders will have some time to dispute that claim.
* When some token holders vote to dispute the claim, extra time is given to other token holders to dispute that claim.
* If a sufficient amount of token holders dispute it. A dispute is created and the arbitrator will decide if the milestone has been reached.
* When there is a disagreement a vote token is created. Holders should send the voteToken to the Vault to disagree with the milestone.
* Token holders can also claim that the team failed to deliver and ask for the remaining ETH to be given back to a different contract.
* This contract can be the vault of another team, or a contract to reimburse.
*/
contract FundingVault is Arbitrable {
address public team;
MiniMeToken public token;
address public funder;
uint public disputeThreshold;
uint public claimToWithdrawTime;
uint public additionalTimeToWithdraw;
uint public timeout;
struct Milestone {
uint amount; // The maximum amount which can be unlocked for this milestone.
uint amountClaimed; // The current amount which is claimed.
uint claimTime; // The time the current claim was made. Or 0 if it's not currently claimed.
bool disputed; // True if a dispute has been raised.
uint feeTeam; // Arbitration fee paid by the team.
uint feeHolders; // Arbitration fee paid by token holders.
MiniMeToken voteToken; // Forked token which will be used to vote.
uint disputeID; // ID of the dispute if this claim is disputed.
uint lastTotalFeePayment; // Time of the last total fee payment, useful for timeouts.
bool lastTotalFeePaymentIsTeam; // True if the last interaction is from the team.
address payerForHolders; // The address who first paid the arbitration fee and will be refunded in case of victory.
}
Milestone[] public milestones;
mapping(uint => uint) public disputeIDToMilstoneID; // Map (disputeID => milestoneID).
uint ousterID; //The ID of the milestone created at construction. To be disputed when the funders claim the team is not doing their job.
bool canChangeTeam; //True if the holders have attempted an oust and won the dispute. Allows the funder to select a new team (only once).
uint8 constant AMOUNT_OF_CHOICES = 2;
uint8 constant TEAM_WINS = 1;
uint8 constant HOLDERS_WINS = 2;
/** @dev Constructor. Choose the arbitrator.
* @param _arbitrator The arbitrator of the contract.
* @param _team The address of the team who will be able to claim milestone completion.
* @param _token The token whose holders are able to dispute milestone claims.
* @param _funder The party putting funds in the vault.
* @param _disputeThreshold The ‱ of tokens required to dispute a milestone.
* @param _claimToWithdrawTime The base time in seconds after a claim is considered non-disputed (i.e if no token holders dispute it).
* @param _additionalTimeToWithdraw The time in seconds which is added per ‱ of tokens disputing the claim.
* @param _timeout Maximum time to pay arbitration fees after the other side did.
*/
constructor(
Arbitrator _arbitrator,
bytes _arbitratorExtraData,
address _team,
address _token,
address _funder,
uint _disputeThreshold,
uint _claimToWithdrawTime,
uint _additionalTimeToWithdraw,
uint _timeout
) public Arbitrable(_arbitrator,_arbitratorExtraData) {
team = _team;
token = MiniMeToken(_token);
funder = _funder;
disputeThreshold = _disputeThreshold;
claimToWithdrawTime = _claimToWithdrawTime;
additionalTimeToWithdraw = _additionalTimeToWithdraw;
timeout = _timeout;
ousterID = milestones.push(Milestone({ //Create a base milestone to be disputed when the funders claim the team is not doing their job.
amount:0,
amountClaimed:0,
claimTime:0,
disputed:false,
feeTeam:0,
feeHolders:0,
voteToken:MiniMeToken(0x0),
disputeID:0,
lastTotalFeePayment:0,
lastTotalFeePaymentIsTeam:false,
payerForHolders:0x0
}))-1;
canChangeTeam = false;
}
/** @dev Give the funds for a milestone.
* @return milestoneID The ID of the milestone which was created.
*/
function fundMilestone() public payable returns(uint milestoneID) {
require(msg.sender == funder, "The caller must be the funder.");
return milestones.push(Milestone({
amount:msg.value,
amountClaimed:0,
claimTime:0,
disputed:false,
feeTeam:0,
feeHolders:0,
voteToken:MiniMeToken(0x0),
disputeID:0,
lastTotalFeePayment:0,
lastTotalFeePaymentIsTeam:false,
payerForHolders:0x0
}))-1;
}
//Restricts Milestone function with functionality not necessary for Ouster.
modifier isNotOuster(uint _milestoneID) {
require(ousterID != _milestoneID, "Milestone cannot be the ouster milestone.");
_;
}
/** @dev Claim funds of a milestone.
* @param _milestoneID The ID of the milestone.
* @param _amount The amount claim. Note that the team can claim less than the amount of a milestone. This allows partial completion claims.
*/
function claimMilestone(uint _milestoneID, uint _amount) public isNotOuster(_milestoneID) {
Milestone storage milestone = milestones[_milestoneID];
require(msg.sender == team, "Can only be called by the team.");
require(milestone.claimTime == 0, "Cannot be called when other claims are active."); // Verify another claim is not active.
require(milestone.amount <= _amount, "TODO.");
milestone.claimTime = now;
}
/** @dev Make a forked token to dispute a claim.
* This avoid creating a token all the time, since most milestones should not be disputed.
* @param _milestoneID The ID of the milestone.
*/
function makeVoteToken(uint _milestoneID) public {
Milestone storage milestone = milestones[_milestoneID];
// The milestone is currently claimed by the team, unless this is the ouster.
if (ousterID != _milestoneID) {require(milestone.claimTime != 0, "The milestone must be claimed by the team.");}
require(address(milestone.voteToken) == 0x0, "The token has already been made.");
milestone.voteToken = MiniMeToken(token.createCloneToken("", token.decimals(), "", block.number, true));
}
/** @dev Pay fee to dispute a milestone. To be called by parties claiming the milestone was not completed.
* The first party to pay the fee entirely will be reimbursed if the dispute is won.
* Note that holders can make a smart contract to crowdfund the fee.
* In the rare event the arbitrationCost is increased, anyone can pay the extra, but it is always the first payer who can be reimbursed.
* @param _milestoneID The milestone which is disputed.
*/
function payDisputeFeeByHolders(uint _milestoneID) public payable {
Milestone storage milestone = milestones[_milestoneID];
uint arbitrationCost = arbitrator.arbitrationCost(arbitratorExtraData);
require(!milestone.disputed, "The milestone cannot be already disputed.");
require(milestone.voteToken.balanceOf(this) >= (disputeThreshold*milestone.voteToken.totalSupply()) / 1000, "Not enough votes.");
require(milestone.feeHolders < arbitrationCost, "Fees have not been paid before.");
require(milestone.feeHolders + msg.value >= arbitrationCost, "Not enough fees have been paid.");
milestone.feeHolders += msg.value;
if (milestone.payerForHolders==0x0)
milestone.payerForHolders = msg.sender;
if (milestone.feeTeam>=arbitrationCost) { // Enough has been paid by all sides.
createDispute(_milestoneID,arbitrationCost);
} else if (milestone.lastTotalFeePayment==0) { // First time the fee is paid.
milestone.lastTotalFeePayment = now;
} else if(milestone.lastTotalFeePaymentIsTeam) { // The team was the last one who had paid entirely.
milestone.lastTotalFeePaymentIsTeam = false;
milestone.lastTotalFeePayment = now;
}
}
/** @dev Pay fee to for a milestone dispute. To be called by the team when the holders have enough votes and fee paid.
* @param _milestoneID The milestone which is disputed.
*/
function payDisputeFeeByTeam(uint _milestoneID) public payable {
Milestone storage milestone = milestones[_milestoneID];
uint arbitrationCost = arbitrator.arbitrationCost(arbitratorExtraData);
require(msg.sender == team, "Can only be called by the team.");
require(!milestone.disputed, "The milestone cannot be already disputed.");
require(milestone.voteToken.balanceOf(this) >= (disputeThreshold*milestone.voteToken.totalSupply()) / 1000, "Not enough votes.");
require(milestone.feeTeam + msg.value >= arbitrationCost, "Not enough fees have been paid.");
milestone.feeTeam += msg.value;
if (milestone.feeHolders>=arbitrationCost) { // Enough has been paid by all sides.
createDispute(_milestoneID,arbitrationCost);
}
else if (milestone.lastTotalFeePayment==0) { // First time the fee is paid.
milestone.lastTotalFeePayment = now;
milestone.lastTotalFeePaymentIsTeam = true;
} else if(!milestone.lastTotalFeePaymentIsTeam) { // The holders were the last ones who had paid entirely.
milestone.lastTotalFeePaymentIsTeam = true;
milestone.lastTotalFeePayment = now;
}
}
/** @dev Create a dispute.
* @param _milestoneID The milestone which is disputed.
* @param _arbitrationCost The amount which should be paid to the arbitrator.
*/
function createDispute(uint _milestoneID, uint _arbitrationCost) internal {
Milestone storage milestone = milestones[_milestoneID];
milestone.disputed = true;
// Remove the fee from the team pool for accounting. Note that at this point it does not matter which fee variable we decrement.
milestone.feeTeam -= _arbitrationCost;
milestone.disputeID = arbitrator.createDispute(AMOUNT_OF_CHOICES,arbitratorExtraData);
disputeIDToMilstoneID[milestone.disputeID] = _milestoneID;
}
/** @dev Withdraw the money claimed in a milestone.
* To be called when a dispute has not been created within the time limit.
* @param _milestoneID The milestone which is disputed.
*/
function withdraw(uint _milestoneID) public isNotOuster(_milestoneID) {
Milestone storage milestone = milestones[_milestoneID];
require(msg.sender == team, "Can only be called by the team.");
require(!milestone.disputed, "Milestone cannot be disputed.");
require(
milestone.voteToken.balanceOf(this) < (disputeThreshold*milestone.voteToken.totalSupply()) / 1000,
"Not enough votes."
); // There is not enough votes.
require(
// solium-disable-next-line indentation
(now - milestone.claimTime) > claimToWithdrawTime + (additionalTimeToWithdraw * milestone.voteToken.balanceOf(this)) / (1000 * milestone.voteToken.totalSupply()),
"Time limit has not passed yet."
);
team.transfer(milestone.amountClaimed+milestone.feeTeam+milestone.feeHolders); // Pay the amount claimed and the unused fees.
milestone.amount -= milestone.amountClaimed;
milestone.amountClaimed = 0;
milestone.claimTime = 0;
milestone.feeTeam = 0;
milestone.feeHolders = 0;
}
// TODO: Timeouts
/** @dev Timeout to use when the holders don't pay the fee.
* @param _milestoneID The milestone which is disputed.
*/
function timeoutByTeam(uint _milestoneID) public {
Milestone storage milestone = milestones[_milestoneID];
require(msg.sender == team, "Can only be called by the team.");
require(milestone.lastTotalFeePaymentIsTeam, "Team wasn't the last to pay.");
require(now - milestone.lastTotalFeePayment > timeout, "Timeout has not passed.");
team.transfer(milestone.amountClaimed+milestone.feeTeam+milestone.feeHolders); // Pay the amount claimed and the unused fees to the team.
milestone.amount -= milestone.amountClaimed;
milestone.amountClaimed = 0;
milestone.claimTime = 0;
milestone.feeTeam = 0;
milestone.feeHolders = 0;
milestone.voteToken = MiniMeToken(0x0);
milestone.lastTotalFeePayment = 0;
milestone.lastTotalFeePaymentIsTeam = false;
milestone.payerForHolders = 0x0;
}
/** @dev Timeout to use whe the team don't pay the fee.
* @param _milestoneID The milestone which is disputed.
*/
function timeoutByHolders(uint _milestoneID) public {
Milestone storage milestone = milestones[_milestoneID];
require(!milestone.lastTotalFeePaymentIsTeam, "Team wasn't the last to pay.");
require(now - milestone.lastTotalFeePayment > timeout, "Timeout has not passed.");
milestone.payerForHolders.transfer(milestone.feeTeam+milestone.feeHolders); // Pay the unused fees to the payer for holders.
milestone.amountClaimed = 0;
milestone.claimTime = 0;
milestone.disputed = false;
milestone.feeTeam = 0;
milestone.feeHolders = 0;
milestone.voteToken = MiniMeToken(0x0);
milestone.lastTotalFeePayment = 0;
milestone.payerForHolders = 0x0;
canChangeTeam = true; //since the team was nonresponsive, the holders are free to change the team.
}
/** @dev Appeal an appealable ruling.
* Transfer the funds to the arbitrator.
* @param _milestoneID The milestone which is disputed.
*/
function appeal(uint _milestoneID) public payable {
Milestone storage milestone = milestones[_milestoneID];
arbitrator.appeal.value(msg.value)(milestone.disputeID, arbitratorExtraData);
}
/** @dev Execute a ruling of a dispute.
* @param _disputeID ID of the dispute in the Arbitrator contract.
* @param _ruling Ruling given by the arbitrator. Note that 0 is reserved for "Not able/wanting to make a decision".
*/
function executeRuling(uint _disputeID, uint _ruling) internal{
Milestone storage milestone = milestones[disputeIDToMilstoneID[_disputeID]];
// Make sure there is enough votes to protect against a malicious arbitrator.
require(milestone.voteToken.balanceOf(this) >= (disputeThreshold*milestone.voteToken.totalSupply()) / 1000, "Not enough votes.");
uint _milestoneID = disputeIDToMilstoneID[_disputeID];
if (_ruling==TEAM_WINS) {
team.transfer(milestone.amountClaimed+milestone.feeTeam+milestone.feeHolders); // Pay the amount claimed and the unused fees to the team.
milestone.amount -= milestone.amountClaimed;
milestone.amountClaimed = 0;
milestone.claimTime = 0;
milestone.disputed = false;
milestone.feeTeam = 0;
milestone.feeHolders = 0;
milestone.voteToken = MiniMeToken(0x0);
milestone.disputeID = 0;
milestone.lastTotalFeePayment = 0;
milestone.lastTotalFeePaymentIsTeam = false;
milestone.payerForHolders = 0x0;
} else if (_ruling==HOLDERS_WINS) {
milestone.payerForHolders.transfer(milestone.feeTeam+milestone.feeHolders); // Pay the unused fees to the payer for holders.
milestone.amountClaimed = 0;
milestone.claimTime = 0;
milestone.disputed = false;
milestone.feeTeam = 0;
milestone.feeHolders = 0;
milestone.voteToken = MiniMeToken(0x0);
milestone.disputeID = 0;
milestone.lastTotalFeePayment = 0;
milestone.lastTotalFeePaymentIsTeam = false;
milestone.payerForHolders = 0x0;
if( ousterID == _milestoneID ) { //if this is the ouster milestone
canChangeTeam = true; //allow the funder to change the team
}
}
}
/** @dev Change the team. Note that the holders would have to make
* the funder a votable contract if they want a vote on who the new team will be.
* @param _newTeam the new team.
*/
function changeTeam(address _newTeam) public {
require(msg.sender == funder, "The caller must be the funder.");
require(canChangeTeam, "Changing teams is not allowed.");
team = _newTeam;
canChangeTeam = false; //This can only be called once.
}
}