Skip to content

Instantly share code, notes, and snippets.

@alexvandesande
Last active April 5, 2022 08:05
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save alexvandesande/122c05a0f8a7108523aed29ae7ec92b1 to your computer and use it in GitHub Desktop.
Save alexvandesande/122c05a0f8a7108523aed29ae7ec92b1 to your computer and use it in GitHub Desktop.
Proposal Framework for The DAO
/*
The contract is free software: you can redistribute it and/or modify
it under the terms of the GNU lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
The contract is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU lesser General Public License for more details.
*/
/*
DAO PROPOSAL FRAMEWORK
======================
The intent of this document is to suggest a Proposal Framework that would solve
most of the larger attack vectors that the DAO current has. This contract has not
been tested and further analysis is required to make sure it works from both a
technical standpoint but also a game-theoretical sense.
The main features this implements:
* Before a proposal is whitelisted to the DAO, this allows the proposal to gather
informal support. This allows any token holder to show their support of the proposal
an information that can be used both by the proposers, the curators or app makers.
Due to gas block limits, a maximum of 256 supporters is allowed, the smallest one
always being replaced by larger ones.
* At most 48 hours before voting deadline, the proposal can watch its own votes. If
it sees that it wasn't able to win by that window, it will return the funds immediately
upon receiving them. This creates an incentive for YEA supporters to vote early, while
giving NAY supporters an extra 48 hours on their voting deadline.
* Immediately after the vote is over, the contract will check again its votes for quorum.
If it sees that YEA has not met 75% of the minQuorum, it will return the funds immediately
upon receiving them. This diminishes the fear that voting NO might help the quorum.
* After the contract receives its funds, there is a 3 week period in which either the Curators
or the DAO can request to cancel the contract and get the funds back.
* After the contract is finally activated, only a initial payment is made. Further payments
can be made only after a set of milestones are reached. The milestoneOracle is a
judge that was agreed upon by both the contractor and the DAO. Milestones also have a
minimum due date, so the contractor can use them to receive periodical payments by
setting the milestone oracle as the same as the curator.
* All payments are not set in ether, but instead in any unit desirable. The unit conversion is
set by a priceFeedOracle, a contract agreed upon by both the contractor and The DAO. Oracles
can be changed by mutual agreement. If the contractor wants to be paid in ether, they can
do so by setting the priceFeedOracle to always return 1 ether. All ether left at the end of the
contract can be sent back to the DAO. If the contract becomes insolvent due to price fall,
further funding is required in order to keep the payments.
* When all milestones are completed OR when the maximum duration of the contract is reached
(this is final non-negotiable deadline so contractors must take these in consideration) then
the contract is destroyed and all remaining funds are sent to the DAO
*/
// The DAO functions that the contract uses
contract DAO {
Proposal[] public proposals;
struct Proposal {
address recipient;
uint amount;
// string description;
uint votingDeadline;
bool open;
bool proposalPassed;
bytes32 proposalHash;
uint proposalDeposit;
bool newCurator;
//SplitData[] splitData;
uint yea;
uint nay;
// mapping (address => bool) votedYes;
// mapping (address => bool) votedNo;
address creator;
}
uint public minQuorumDivisor;
uint public totalSupply;
function balanceOf(address _owner) constant returns (uint256 balance);
}
// A price oracle so that the contract can be set in any currency
contract PriceOracle {
function convertToWei(uint units) returns (uint amountOfWei) {
return units * 1 ether;
}
}
/*
An Offer from a Contractor to the DAO without any reward going back to
the DAO.
Feel free to use as a template for your own proposal.
Actors:
- Offerer: the entity that creates the Offer. Usually it is the initial
Contractor.
- Contractor: the entity that has rights to withdraw ether to perform
its project.
- Client: the DAO that gives ether to the Contractor. It signs off
the Offer, can adjust daily withdraw limit or even fire the
Contractor.
*/
contract DAOProposal {
// Period of time after which money can be withdrawn from this contract
uint constant payoutFreezePeriod = 3 weeks;
// The address of the Contractor.
DAO client = DAO(0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413); // address of DAO
DAO originalClient = client; // address of DAO who signed the contract
address public contractor;
address public curators = 0xDa4a4626d3E16e094De3225A751aAb7128e96526;
address public milestoneOracle;
PriceOracle public priceFeed;
bytes32 public clientOracleCandidates;
bytes32 public contractorOracleCandidates;
// The hash of the Proposal/Offer document.
bytes32 public hashOfTheProposalDocument;
uint public dateOfSignature;
uint initialPayment;
uint totalPayments;
uint proposalId;
bool wasApprovedBeforeDeadline;
uint maxContractDuration;
address[256] supporters;
uint maxSupportGathered;
uint lastSupportGathered;
uint lastCheckedSupport;
// creates contract states and milestones
enum State { PreVote, Voting, Approved, Active, Void }
State public state;
struct Milestone {
uint value;
uint validAfter;
bool completed;
bool paid;
}
Milestone[] public milestones;
uint public totalMilestones;
// Some functions can only called by The DAO
modifier onlyClient {
if (msg.sender != address(client))
throw;
_
}
// Some functions can only called by The DAO
modifier onlyContractor {
if (msg.sender != address(contractor))
throw;
_
}
// Checks the state of the contract
modifier inState(State _state) {
if (state != _state) throw;
_
}
// Prevents methods from perfoming any value transfer
modifier noEther() {if (msg.value > 0) throw; _}
// STAGES OF A PROPOSAL
/*
STATE: PREVOTE
==============
*/
// When the proposal is created it's in a "prevote" state where it can be analyzed and
// optionally public support might be gathered
function DAOProposal(
address _contractor,
bytes32 _hashOfTheProposalDocument,
uint _initialPayout,
address _milestoneOracle,
address _priceFeed,
uint[] milestoneDates,
uint[] milestoneValues,
uint _maxContractDuration
) {
contractor = _contractor;
hashOfTheProposalDocument = _hashOfTheProposalDocument;
state = State.PreVote;
initialPayment = _initialPayout;
totalPayments = _initialPayout;
milestoneOracle = _milestoneOracle;
priceFeed = PriceOracle(_priceFeed);
totalMilestones = milestoneValues.length;
maxContractDuration = _maxContractDuration;
for (uint i=0; i< milestoneValues.length; i++) {
milestones.length++;
milestones[i] = Milestone({
value: milestoneValues[i],
validAfter: milestoneDates[i],
completed: false,
paid: false
}) ;
totalPayments += milestoneValues[i];
}
}
// This allows an option where token holders can show support for a proposal before it goes live
// This can be used by the contractor as a way to change the terms if there's not much support
// Or can be used by curators as a way to know which proposal to prioritize on the whitelist
function getSupport() inState(State.PreVote) {
bool loop = true;
uint currentSupport = 0;
lastCheckedSupport = now;
uint smallestSupporter = 256;
uint smallestSupporterBalance = 10^100; // make it a googol
uint thisBalance = 0;
for (uint i = 0; i < 256; i++) {
thisBalance = client.balanceOf(supporters[i]);
currentSupport += thisBalance;
if ( thisBalance < smallestSupporterBalance) {
smallestSupporterBalance = thisBalance;
smallestSupporter = i;
}
}
uint senderBalance = client.balanceOf(msg.sender);
if (senderBalance > smallestSupporterBalance) {
supporters[smallestSupporter] = msg.sender;
currentSupport = currentSupport - smallestSupporterBalance + senderBalance;
}
if (currentSupport > maxSupportGathered)
maxSupportGathered = currentSupport;
lastSupportGathered = currentSupport;
}
/*
STATE: VOTING
=============
*/
// Once a proposal is submitted, the Contractor can save its proposalId so
// the vote can be watched
function watchProposal(uint _proposalId) inState(State.PreVote) onlyContractor {
address recipient;
uint votingDeadline;
bool open;
(recipient,,votingDeadline, open, ) = client.proposals(_proposalId);
if (recipient == address(this)
&& votingDeadline > now
&& open
&& proposalId == 0
) {
proposalId = _proposalId;
state = State.Voting;
}
}
// The proposal will not accept the results of the vote if it wasn't able to
// be sure that YEA was able to suceed 48 hours before the deadline
function protectFromAmbush() {
// This function can be called by anyone
uint votingDeadline;
uint yea;
uint nay;
(,,votingDeadline,,,,,,yea,nay,) = client.proposals(proposalId);
// Only execute until 48 hours before the deadline
if (now > votingDeadline - 48 hours) throw;
// If so, then check if was approved at that moment
wasApprovedBeforeDeadline = (yea > nay);
}
/*
STATE: APPROVED
===============
*/
// The contract is considered "signed" when the DAO sends money to it
function() {
if (msg.sender == address(client)
&& state == State.Voting) {
uint votingDeadline;
uint yea;
uint nay;
(,,votingDeadline,,,,,,yea,nay,) = client.proposals(proposalId);
uint quorum = client.totalSupply() / client.minQuorumDivisor();
// Check if Yea is at least 75% of the quorum and if it was not approved by ambush
if (4 * yea < 3 * quorum
|| !wasApprovedBeforeDeadline) {
client.send(this.balance);
state = State.Void;
} else {
dateOfSignature = now;
state = State.Approved;
}
}
}
// After a contract is approved, it can still be cancelled by the DAO or curators for 3 weeks
function cancelContract() inState(State.Approved) {
if (msg.sender != address(client) || msg.sender != curators)
throw;
state = State.Void;
endContract();
}
/*
STATE: ACTIVE
=============
*/
// Once the 3 weeks are up, the contract can be activated and first payment made
function activateContract() inState(State.Approved) {
if (now < dateOfSignature + payoutFreezePeriod) throw;
state = State.Active;
if (!contractor.send( priceFeed.convertToWei( initialPayment ))) throw;
}
// The milestoneOracle can approve or disapprove milestones
// Milestones can be used for multiple payouts by simply using
// the contractor as the oracle and using "validAfter" dates.
function reachMilestone(uint milestoneId, bool reached) {
if (msg.sender != address(milestoneOracle))
throw;
Milestone m = milestones[milestoneId];
m.completed = reached;
}
// Once the oracle has approved it and the minimun date is reached, then the payment can be made
// Payment is in uints set by the price oracle, agreed by client and contractor
// If ether price raises, the remainder can be returned to the client
// If ether price falls and the contract becomes insolvent, further funding will be required
function receiveMilestonePayment(uint milestoneId) {
Milestone m = milestones[milestoneId];
if ( now < m.validAfter
|| !m.completed
|| !contractor.send( priceFeed.convertToWei( m.value ))) throw;
m.paid = true;
// Check all milestones
Milestone mm = milestones[0];
uint totalMilestonesPaid = 0;
for (uint i=0; i< totalMilestones; i++) {
if (mm.paid) totalMilestonesPaid++;
}
if (totalMilestonesPaid == totalMilestones)
state = State.Void;
}
/*
STATE: VOID
===========
*/
// Contract can be terminated once it has reached its maximum duration,
// if it has completed all milestones or during the approval period
// by the DAO or Curators
function endContract() {
if (state != State.Void || now > dateOfSignature + maxContractDuration * 1 days)
throw;
client.send(this.balance);
suicide(client);
}
/*
STATE: ANY
==========
*/
// Change the client DAO by giving the new DAO's address
// warning: The new DAO must come either from a split of the original
// DAO or an update via `newContract()` so that it can claim rewards
function updateClientAddress(DAO _newClient) onlyClient noEther {
client = _newClient;
}
// Change the contractor payout address
function updateContractorAddress(DAO _newClient) onlyContractor noEther {
client = _newClient;
}
// Change the Oracles
function changeOracles(address newMilestoneOracle, address newPriceFeedOracle)
returns (bool changedIt) {
if (msg.sender == address(client)) {
clientOracleCandidates = sha3(newMilestoneOracle, newPriceFeedOracle);
} else if (msg.sender == contractor) {
contractorOracleCandidates = sha3(newMilestoneOracle, newPriceFeedOracle);
}
if (clientOracleCandidates == contractorOracleCandidates) {
milestoneOracle = newMilestoneOracle;
priceFeed = PriceOracle(newPriceFeedOracle);
return true;
} else {
return false;
}
}
}
contract MyProposal is DAOProposal {
// Add here your own code
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment