Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
100 changes: 100 additions & 0 deletions src/chief-v2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
pragma solidity >=0.5.0;

import "ds-note/note.sol";

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

contract Chief is DSNote {
// --- Init ---
constructor(address gov_) public {
gov = TokenLike(gov_);
threshold = ONE / 2;
}

// --- Math ---
uint256 constant ONE = 10 ** 18;
function add(uint x, uint y) internal pure returns (uint z) {
Comment thread
jparklev marked this conversation as resolved.
Outdated
z = x + y;
require(z >= x);
}
function sub(uint x, uint y) internal pure returns (uint z) {
z = x - y;
require(z <= x);
}
function wmul(uint x, uint y) internal pure returns (uint z) {
z = x * y;
require(y == 0 || z / y == x);
z = z / ONE;
}

// --- Data ---
uint256 public threshold; // locked % needed to enact a proposal
TokenLike public gov; // governance token
Comment thread
jparklev marked this conversation as resolved.
Outdated
uint256 public locked; // total locked gov
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(gov.transferFrom(msg.sender, address(this), wad));
Comment thread
jparklev marked this conversation as resolved.
Outdated
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(gov.transferFrom(address(this), msg.sender, wad));
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.

TransferFrom should be Transfer. One could also consider merging lock and free under one function using signed integers

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.

actually in DSToken transfer calls transferFrom, but it is true this might a bit confusing and if in the future gov uses another implementation, it might fail.

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]);

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]);
require(votes[proposal] > wmul(locked, threshold));

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.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.gov()), 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