Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@
[submodule "lib/ds-thing"]
path = lib/ds-thing
url = https://github.com/dapphub/ds-thing
[submodule "lib/ds-math"]
path = lib/ds-math
url = https://github.com/dapphub/ds-math
1 change: 1 addition & 0 deletions lib/ds-math
Submodule ds-math added at 784079
86 changes: 86 additions & 0 deletions src/chief-v2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
pragma solidity >=0.5.0;

import "ds-note/note.sol";
import "ds-math/math.sol";

contract TokenLike {
function transferFrom(address, address, uint) public returns (bool);
}

contract Chief is DSNote, DSMath {
// --- Init ---
constructor(address governanceToken_) public {
governanceToken = TokenLike(governanceToken_);
threshold = WAD / 2;
}

// --- Data ---
uint256 public threshold; // locked % needed to enact a proposal
TokenLike public governanceToken;
uint256 public locked; // total locked governanceToken
mapping(bytes32 => bool) public hasFired; // proposal => hasFired?
mapping(address => uint256) public balances; // guy => lockedGovHowMuch
mapping(address => bytes32) public picks; // guy => proposalHash
mapping(bytes32 => uint256) public votes; // proposalHash => votesHowMany

// --- Events ---
event Voted(
bytes32 indexed proposalHash,
address indexed voter,
uint256 weight
);
event Executed(
address caller,
bytes32 proposal,
address indexed app,
bytes data
);

// --- Voting Interface ---
function lock(uint256 wad) public note {
require(governanceToken.transferFrom(msg.sender, address(this), wad), "ds-chief-transfer-failed");
balances[msg.sender] = add(balances[msg.sender], wad);
locked = add(locked, wad);

bytes32 currPick = picks[msg.sender];
if (currPick != bytes32(0) && !hasFired[currPick])
votes[currPick] = add(votes[currPick], wad);
}
function free(uint256 wad) public note {
balances[msg.sender] = sub(balances[msg.sender], wad);
require(governanceToken.transferFrom(address(this), msg.sender, wad), "ds-chief-transfer-failed");
locked = sub(locked, wad);

bytes32 currPick = picks[msg.sender];
if (currPick != bytes32(0) && !hasFired[currPick])
votes[currPick] = sub(votes[currPick], wad);
}

function vote(bytes32 currPick) public {
require(!hasFired[currPick], "ds-chief-proposal-has-already-been-enacted");

uint256 weight = balances[msg.sender];
bytes32 prevPick = picks[msg.sender];

if (prevPick != bytes32(0) && !hasFired[prevPick])
votes[prevPick] = sub(votes[prevPick], weight);

votes[currPick] = add(votes[currPick], weight);
picks[msg.sender] = currPick;

emit Voted(currPick, msg.sender, weight);
}
function exec(address app, bytes memory data) public {
bytes32 proposal = keccak256(abi.encode(app, data));
require(!hasFired[proposal], "ds-chief-proposal-has-already-been-enacted");
require(votes[proposal] > wmul(locked, threshold), "ds-chief-proposal-does-not-pass-threshold");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can someone explain the math behind wmul(locked, threshold) and how it's equivalent to votes[proposal] > locked / 2?

How is it different from votes[proposal] > wdiv(locked, 2 * WAD)?

Copy link
Copy Markdown
Author

@jparklev jparklev Apr 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When threshold is 0.5 WAD, wmul(locked, 0.5 WAD) yields the same as locked / 2 (or wdiv(locked, 2 * WAD), as you say). wmul was chosen b/c it makes threshold represent the "percent of locked MKR needed to pass a proposal," which felt more natural than the alternatives. If we allow threshold to be a variable, we are giving MKR voters the ability to change the MKR % required to elect a proposal (modifiable w/ a delegate call). You could argue that they should not have this power, the decision in this case was arbitrary.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xwvvvvwx do you think we should give chief the ability to alter its own "quorum" post-deployment?


assembly {
let ok := delegatecall(sub(gas, 5000), app, add(data, 0x20), mload(data), 0, 0)
if eq(ok, 0) { revert(0, 0) }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider passing the returndata of the delegatecall to the caller of exec. Then for generality this should pass returndata in the fail case as well, for passing revert messages. See ds-proxy.

}

hasFired[proposal] = true;
emit Executed(msg.sender, proposal, app, data);
}
}
180 changes: 180 additions & 0 deletions src/chief-v2.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
pragma solidity >=0.5.0;

import "ds-test/test.sol";
import "ds-token/token.sol";

import "./chief-v2.sol";

contract Voter {
DSToken gov;
address chief;

constructor(DSToken gov_, address chief_) public {
gov = gov_;
chief = chief_;
}

function approveChief() public { gov.approve(chief); }

function tryLock(uint256 wad) public returns (bool ok) {
(ok, ) = chief.call(abi.encodeWithSignature(
"lock(uint256)", wad
));
}

function tryFree(uint256 wad) public returns (bool ok) {
(ok, ) = chief.call(abi.encodeWithSignature(
"free(uint256)", wad
));
}

function tryVote(bytes32 pick) public returns (bool ok) {
(ok, ) = chief.call(abi.encodeWithSignature(
"vote(bytes32)", pick
));
}

function tryExec(address app, bytes memory data) public returns (bool ok) {
(ok, ) = chief.call(abi.encodeWithSignature(
"exec(address,bytes)", app, data
));
}
}

contract Cache {
uint256 public val;
function set(uint256 val_) public { val = val_; }
}

contract CacheScript {
function setCache(address target, uint256 val) public {
Cache(target).set(val);
}
}

contract ThresholdScript {
uint256 public threshold;
function updateThreshold(uint val_) public {
threshold = val_;
}
}


contract ChiefTest is DSTest {
Chief chief;
DSToken gov;
Cache cache;

Voter ben;
Voter sam;
Voter ava;

function setUp() public {
gov = new DSToken("gov");
chief = new Chief(address(gov));
cache = new Cache();

ben = new Voter(gov, address(chief));
sam = new Voter(gov, address(chief));
ava = new Voter(gov, address(chief));
gov.mint(address(ben), 100 ether);
gov.mint(address(sam), 100 ether);
gov.mint(address(ava), 100 ether);
}

function test_sanity_setup_check() public {
assertEq(chief.threshold(), 10 ** 18 / 2);
assertEq(chief.locked(), 0);
assertEq(address(chief.governanceToken()), address(gov));
assertEq(chief.balances(address(ben)), 0);
assertEq(chief.picks(address(ben)), bytes32(0));

assertEq(cache.val(), 0);

assertEq(gov.balanceOf(address(ben)), 100 ether);
}

function test_lock_free() public {
// ben gives chief unlimited approvals over his gov token
ben.approveChief();

// ben locks some gov in chief
assertTrue(ben.tryLock(10 ether));
assertEq(chief.balances(address(ben)), 10 ether);
assertEq(chief.locked(), 10 ether);
assertTrue(ben.tryLock(1 ether));
assertEq(chief.balances(address(ben)), 11 ether);
assertEq(chief.locked(), 11 ether);

// ben frees the same amount of gov from chief
assertTrue(ben.tryFree(11 ether));
assertEq(chief.balances(address(ben)), 0);
assertEq(chief.locked(), 0);
}

function test_vote_exec() public {
ben.approveChief();
assertTrue(ben.tryLock(10 ether));

// create a useful contract for chief to delegatecall
CacheScript cacheScript = new CacheScript();
uint256 newVal = 123;

// create a proposal
bytes memory data = abi.encodeWithSignature(
"setCache(address,uint256)", cache, newVal
);
bytes32 proposal = keccak256(abi.encode(cacheScript, data));

// ben votes for the proposal
assertTrue(ben.tryVote(proposal));
assertEq(chief.picks(address(ben)), proposal);
assertEq(chief.votes(proposal), 10 ether);
assertTrue(!chief.hasFired(proposal));

// ben executes the proposal
assertEq(cache.val(), 0);
assertTrue(ben.tryExec(address(cacheScript), data));

// the proposal was successfully executed
assertEq(cache.val(), newVal);
assertTrue(chief.hasFired(proposal));

// the proposal can only be executed once
assertTrue(!ben.tryExec(address(cacheScript), data));
}

function test_modify_threshold() public {
ben.approveChief();
assertTrue(ben.tryLock(10 ether));

ThresholdScript thresholdScript = new ThresholdScript();
uint256 newThreshold = 10 ** 18;

uint256 oldThreshold = chief.threshold();
assertTrue(newThreshold != oldThreshold);

bytes memory data = abi.encodeWithSignature(
"updateThreshold(uint256)", newThreshold
);
bytes32 proposal = keccak256(abi.encode(thresholdScript, data));

assertTrue(ben.tryVote(proposal));
assertTrue(ben.tryExec(address(thresholdScript), data));
assertEq(chief.threshold(), newThreshold);
}

function test_fail_free_too_much() public {
ben.approveChief();
assertTrue( ben.tryLock(10 ether));
assertTrue(!ben.tryFree(11 ether));

sam.approveChief();
assertTrue( sam.tryLock(10 ether));
assertTrue( ben.tryFree(9 ether ));
assertTrue( ben.tryFree(1 ether ));

assertTrue(!ben.tryFree(1 ether ));
assertTrue( sam.tryFree(10 ether));
}
}
Loading