上一篇我们详解了Compound治理源码,这篇我们来根据上面的治理逻辑完成简单的,完整的提案执行过程。
上一篇我们详解了 Compound治理源码, 这篇我们来根据上面的治理逻辑完成简单的,完整的提案执行过程。
我们先创建一个我们自己的Dao,取名为JCDao(杰出Dao),用合约JCDao.sol来记录Dao中成员的身份
mapping(address => bool) public Admin;
mapping(address => bool) public whitelisted;
function isDaoMember(address _address) public view returns(bool){
return whitelisted[_address];
}
function setAdmin(address _addr) external onlyOwner{
Admin[_addr] = true;
}
function isDaoAdmin(address _address) public view returns(bool){
return Admin[_address];
}
function addMember(address _member) external{
require(Admin[msg.sender]|| msg.sender == owner ,"You must be an admin&Owner to add a member");
whitelisted[_member] = true;
}
然后我们规定贡献ETH就可获得等量的token:
function contribute() external payable{
require(msg.value > 0,"You must contribute more than 0 ether");
require(isDaoMember(msg.sender),"You must be a DAO member to contribute");
contributions[msg.sender] += msg.value;
JCToken(jcToken).mintDao(msg.sender,msg.value);
emit Contribute(msg.sender,msg.value);
}
设置timelock和一个withdraw方法,规定只有timelock能调用withdraw方法。
function setTimeLock(address _timeLock) external onlyOwner{
timeLock = _timeLock;
}
function withdraw(address to,uint256 amount) external{
require(msg.sender == timeLock,"You must be the timeLock to call this function");
payable(to).transfer(amount);
emit Withdraw(to,amount);
}
这就意味着只有通过治理,发起提案并通过才能调用withdraw方法,我们的目标就是完成这一流程。
然后我们来写我们的治理合约
同样的,一个代理合约,一个实现合约
我们在代理合约中设置admin modifier,只允许admin来设置实现合约:
modifier onlyAdmin() {
require(
JCDao(dao).isDaoAdmin(msg.sender) || msg.sender == owner,
"Governor:_setImplementation: admin&owner only"
);
_;
}
function setImplementation(address _implementation) public onlyAdmin{
require(
_implementation != address(0),
"Governor:_setImplementation: invalid implementation address"
);
address oldImplementation = implementation;
implementation = _implementation;
emit NewImplementation(oldImplementation, implementation);
}
然后开始写我们的实现合约:
我们将所需的event和一部分属性放在tool文件中:
这里的属性前面已经讲过,我在compound源码上进行了简化,以便理解
contract GovernorEvents {
/// @notice Emitted when implementation is changed
event NewImplementation(
address oldImplementation,
address newImplementation
);
/// @notice An event emitted when a proposal has been canceled
event ProposalCanceled(uint id);
/// @notice An event emitted when a proposal has been queued in the Timelock
event ProposalQueued(uint id, uint eta);
/// @notice An event emitted when a proposal has been executed in the Timelock
event ProposalExecuted(uint id);
event ProposalCreated(
uint id,
address proposer,
address[] targets,
uint[] values,
string[] signatures,
bytes[] calldatas,
uint startBlock,
uint endBlock,
string description
);
event VoteCast(
address indexed voter,
uint proposalId,
uint8 support,
uint votes,
string reason
);
}
contract GovernImpV1 {
address public admin;
address public pendingAdmin;
address public implementation;
}
contract GovernImpV2 is GovernImpV1 {
/// @notice The delay before voting on a proposal may take place, once proposed, in blocks
uint public votingDelay;
/// @notice The duration of voting on a proposal, in blocks
uint public votingPeriod;
/// @notice The official record of all proposals ever proposed
mapping(uint => Proposal) public proposals;
/// @notice The latest proposal for each proposer
mapping(address => uint) public latestProposalIds;
/// @notice The total number of proposals
uint public proposalCount;
/// @notice The address of the Compound Protocol Timelock
TimelockInterface public timelock;
struct Proposal {
uint id;
address proposer;
uint eta; //提案可用于执行的时间戳,在投票成功后设置
address[] targets;
uint[] values;
string[] signatures;
bytes[] calldatas;
uint startBlock;
uint endBlock;
uint forVotes;
uint againstVotes;
uint abstainVotes;
bool canceled;
bool executed;
mapping(address => Receipt) receipts;
}
/// @notice Ballot receipt record for a voter
struct Receipt {
bool hasVoted;
uint8 support;
uint96 votes;
}
enum ProposalState {
Pending,
Active, //在投票中
Canceled,
Defeated,
Succeeded,
Queued,
Expired, //过期了
Executed
}
}
继承完工具类后,增添必要属性:
在这里我们规定,每个提案获得的票数超过300 ether就可以通过
address public jcToken;
address public dao;
uint public quorumVotes = 300 ether; //每个提案通过所需的最少票数
初始化:
function initialize(
address _dao,
address _timelock,
address _token,
uint _votingPeriod,
uint _votingDelay
) public {
require(
address(timelock) == address(0),
"GovernorBravo::initialize: can only initialize once"
);
require(
_timelock != address(0),
"GovernorBravo::initialize: invalid timelock address"
);
require(
_token != address(0),
"GovernorBravo::initialize: invalid comp address"
);
timelock = TimelockInterface(_timelock);
jcToken = _token;
votingPeriod = _votingPeriod;
votingDelay = _votingDelay;
dao = _dao;
}
提案函数与源码大致相同,我们在这只实现一种提案方式:
/**
* @dev 提议
* @param targets 目标合约地址
* @param values 转账金额
* @param calldatas 调用数据
* @param description 提议描述
* @return proposalId 提议ID
*/
function propose(
address[] memory targets,
uint[] memory values,
string[] memory signatures,
bytes[] memory calldatas,
string memory description
) external payable returns (uint proposalId) {
return
_proposeInternal(
msg.sender,
targets,
values,
signatures,
calldatas,
description
);
}
function _proposeInternal(
address proposer,
address[] memory targets,
uint[] memory values,
string[] memory signatures,
bytes[] memory calldatas,
string memory description
) internal returns (uint) {
// 在提交本次提案之前先判断上一个提案是否被处理:
uint latestProposalId = latestProposalIds[proposer];
if (latestProposalId != 0) {
ProposalState proposersLatestProposalState = state(
latestProposalId
);
//
require(
proposersLatestProposalState != ProposalState.Active,
"GovernorBravo::proposeInternal: one live proposal per proposer, found an already active proposal"
);
require(
proposersLatestProposalState != ProposalState.Pending,
"GovernorBravo::proposeInternal: one live proposal per proposer, found an already pending proposal"
);
}
uint startBlock = block.number + votingDelay;
uint endBlock = startBlock + votingPeriod;
proposalCount++;
uint newProposalID = proposalCount;
Proposal storage newProposal = proposals[newProposalID];
newProposal.id = newProposalID;
newProposal.proposer = proposer;
newProposal.targets = targets;
newProposal.values = values;
newProposal.signatures = signatures;
newProposal.calldatas = calldatas;
newProposal.startBlock = startBlock;
newProposal.endBlock = endBlock;
newProposal.forVotes = 0;
newProposal.againstVotes = 0;
newProposal.abstainVotes = 0;
newProposal.canceled = false;
newProposal.executed = false;
latestProposalIds[newProposal.proposer] = newProposal.id;
emit ProposalCreated(
newProposal.id,
proposer,
targets,
values,
signatures,
calldatas,
startBlock,
endBlock,
description
);
return newProposal.id;
}
投票函数:
/**
* @dev 投票
* @param proposalId 提议ID
* @param support 支持或反对或中立:0,1,2
*/
function castVote(uint proposalId, uint8 support) external {
emit VoteCast(
msg.sender,
proposalId,
support,
_castVoteInternal(msg.sender, proposalId, support),
""
);
}
/**
* @dev 投票internal
* @param voter 投票人
* @param proposalId 提议ID
* @param support 反对或支持或中立:0,1,2
*/
function _castVoteInternal(
address voter,
uint proposalId,
uint8 support
) internal returns (uint256) {
require(
state(proposalId) == ProposalState.Active,
"GovernorBravo::castVoteInternal: voting is closed"
);
require(
support <= 2,
"GovernorBravo::castVoteInternal: invalid vote type"
);
Proposal storage proposal = proposals[proposalId];
Receipt storage receipt = proposal.receipts[voter];
uint256 votes = IJCToken(jcToken).getPriorVotes(
voter,
proposal.startBlock
);
if (support == 0) {
proposal.againstVotes = proposal.againstVotes + votes;
} else if (support == 1) {
proposal.forVotes = proposal.forVotes + votes;
} else if (support == 2) {
proposal.abstainVotes = proposal.abstainVotes + votes;
}
receipt.hasVoted = true;
receipt.support = support;
receipt.votes = uint96(votes);
return votes;
}
入队函数,在这里为了后面的测试方便,我们都以区块高度作为时间度量(源码是时间戳)
//提议被投票通过标准后可进入执行队列
function queue(uint proposalId) external {
require(
state(proposalId) == ProposalState.Succeeded,
"GovernorBravo::queue: proposal can only be queued if it is succeeded"
);
Proposal storage proposal = proposals[proposalId];
uint eta = block.number + timelock.delay();
console.log("queue eta:",eta);
for (uint i = 0; i < proposal.targets.length; i++) {
_queueOrRevertInternal(
proposal.targets[i],
proposal.values[i],
proposal.signatures[i],
proposal.calldatas[i],
eta
);
}
proposal.eta = eta; //成功进入执行队列后,设置执行时间戳
emit ProposalQueued(proposalId, eta);
}
function _queueOrRevertInternal(
address target,
uint value,
string memory signature,
bytes memory data,
uint eta
) internal {
require(
!timelock.queuedTransactions(
keccak256(abi.encode(target, value, signature, data, eta))
),
"GovernorBravo::queueOrRevertInternal: identical proposal action already queued at eta"
);
timelock.queueTransaction(target, value, signature, data, eta);
}
执行函数:
//执行提议
function execute(uint proposalId) external payable {
require(
state(proposalId) == ProposalState.Queued,
"GovernorBravo::execute: proposal can only be executed if it is queued"
);
Proposal storage proposal = proposals[proposalId];
proposal.executed = true;
// 执行提案中每一个动作。
for (uint i = 0; i < proposal.targets.length; i++) {
console.log("eta:",proposal.eta);
timelock.executeTransaction(
proposal.targets[i],
proposal.values[i],
proposal.signatures[i],
proposal.calldatas[i],
proposal.eta
);
}
emit ProposalExecuted(proposalId);
}
state:
//获取提案的状态&根据投票结果得出提案是否通过
function state(uint proposalId) public view returns (ProposalState) {
Proposal storage proposal = proposals[proposalId];
if (proposal.canceled) {
return ProposalState.Canceled;
} else if (block.number <= proposal.startBlock) {
return ProposalState.Pending;
} else if (block.number <= proposal.endBlock) {
return ProposalState.Active;
} else if (
proposal.forVotes <= proposal.againstVotes ||
proposal.forVotes < quorumVotes
) {
return ProposalState.Defeated;
} else if (proposal.eta == 0) {
return ProposalState.Succeeded;
} else if (proposal.executed) {
return ProposalState.Executed;
} else if (block.number >= proposal.eta + timelock.GRACE_PERIOD()) {
return ProposalState.Expired;
} else {
return ProposalState.Queued;
}
}
完成了治理合约,我们来写token:
在这里我们直接继承openzeppelin的ERC20,省去了源码的transfer函数,
增加一个mintDao函数,用于给贡献者发币:
function mintDao(address account, uint256 amount) external onlyDao {
_mint(account, amount);
}
其余基于源码做了一些微调,读者可自行查看:
function delegate(address delegatee) public {
return _delegate(msg.sender, delegatee);
}
function _delegate(address delegator, address delegatee) internal {
address currentDelegate = delegates[delegator];
uint256 delegatorBalance = balanceOf(delegator);
delegates[delegator] = delegatee;
emit DelegateChanged(delegator, currentDelegate, delegatee);
_moveDelegates(currentDelegate, delegatee, delegatorBalance);
}
function _moveDelegates(
address fromRep,
address toRep,
uint256 amount
) internal {
if (fromRep != toRep && amount > 0) {
if (fromRep != address(0)) {
uint256 fromRepNum = numCheckpoints[fromRep];
uint256 fromRepOld = fromRepNum > 0
? checkpoints[fromRep][fromRepNum - 1].votes
: 0;
uint256 fromRepNew = fromRepOld.sub(amount);
_writeCheckpoint(fromRep, fromRepNum, fromRepOld, fromRepNew);
}
if (toRep != address(0)) {
uint256 toRepNum = numCheckpoints[toRep];
uint256 toRepOld = toRepNum > 0
? checkpoints[toRep][toRepNum - 1].votes
: 0;
uint256 toRepNew = amount.add(toRepOld);
_writeCheckpoint(toRep, toRepNum, toRepOld, toRepNew);
}
}
}
function _writeCheckpoint(
address delegatee,
uint256 nCheckpoints,
uint256 oldVotes,
uint256 newVotes
) internal {
uint256 blockNumber =block.number;
if (
nCheckpoints > 0 &&
checkpoints[delegatee][nCheckpoints - 1].fromBlock == blockNumber
) {
checkpoints[delegatee][nCheckpoints - 1].votes = newVotes;
} else {
checkpoints[delegatee][nCheckpoints] = Checkpoint(
blockNumber,
newVotes
);
numCheckpoints[delegatee] = nCheckpoints + 1;
}
emit DelegateVotesChanged(delegatee, oldVotes, newVotes);
}
//计票函数:
function getPriorVotes(
address account,
uint blockNumber
) external view returns (uint256) {
require(
blockNumber < block.number,
"Comp::getPriorVotes: not yet determined"
);
uint256 nCheckpoints = numCheckpoints[account];
if (nCheckpoints == 0) {
return 0;
}
// First check most recent balance
if (checkpoints[account][nCheckpoints - 1].fromBlock <= blockNumber) {
return checkpoints[account][nCheckpoints - 1].votes;
}
// Next check implicit zero balance
if (checkpoints[account][0].fromBlock > blockNumber) {
return 0;
}
uint256 lower = 0;
uint256 upper = nCheckpoints - 1;
while (upper > lower) {
uint256 center = upper - (upper - lower) / 2; // ceil, avoiding overflow
Checkpoint memory cp = checkpoints[account][center];
if (cp.fromBlock == blockNumber) {
return cp.votes;
} else if (cp.fromBlock < blockNumber) {
lower = center;
} else {
upper = center - 1;
}
}
return checkpoints[account][lower].votes;
}
function getCurrentVotes(address account) external view returns (uint256) {
uint256 nCheckpoints = numCheckpoints[account];
return
nCheckpoints > 0 ? checkpoints[account][nCheckpoints - 1].votes : 0;
}
加上我们的时间锁合约:
contract TimeLock is TimelockInterface {
using SafeMath for uint;
address public admin;
address public pendingAdmin;
uint public delay;//在提案生效前的一段宽限期,可以选择不接受此提案而退出。
uint public constant GRACE_PERIOD = 10;
mapping(bytes32 => bool) public queuedTransactions;
event NewDelay(uint indexed newDelay);
event CancelTransaction(
bytes32 indexed txHash,
address indexed target,
uint value,
string signature,
bytes data,
uint eta
);
event ExecuteTransaction(
bytes32 indexed txHash,
address indexed target,
uint value,
string signature,
bytes data,
uint eta
);
event QueueTransaction(
bytes32 indexed txHash,
address indexed target,
uint value,
string signature,
bytes data,
uint eta
);
constructor(address _admin, uint _delay) public {
admin = _admin;
delay = _delay;
}
modifier onlyAdmin() {
require(
msg.sender == pendingAdmin,
"Timelock::onlyAdmin: Call must come from pendingAdmin."
);
_;
}
//pendingAdmin一般为治理合约
function setAdmin(address _admin) public {
require(msg.sender == admin, "call must by admin");
require(
_admin != address(0),
"Timelock::setAdmin: New admin cannot be the zero address."
);
pendingAdmin = _admin;
}
function setDelay(uint _delay) public onlyAdmin {
require(
msg.sender == address(this),
"Timelock::setDelay: Call must come from Timelock."
);
delay = _delay;
emit NewDelay(delay);
}
//将在执行队列中的提案信息hash,并设此提案hash为true,代表此提案已入队列
function queueTransaction(
address target,
uint value,
string memory signature,
bytes memory data,
uint eta
) public onlyAdmin returns (bytes32) {
require(
eta >= getBlockTimestamp().add(delay),
"Timelock::queueTransaction: Estimated execution block must satisfy delay."
);
bytes32 txHash = keccak256(
abi.encode(target, value, signature, data, eta)
);
queuedTransactions[txHash] = true;
emit QueueTransaction(txHash, target, value, signature, data, eta);
return txHash;
}
//取消提案
function cancelTransaction(
address target,
uint value,
string memory signature,
bytes memory data,
uint eta
) public onlyAdmin {
bytes32 txHash = keccak256(
abi.encode(target, value, signature, data, eta)
);
queuedTransactions[txHash] = false;
emit CancelTransaction(txHash, target, value, signature, data, eta);
}
//执行队列中的提案,这里一般是
function executeTransaction(
address target,
uint value,
string memory signature,
bytes memory data,
uint eta
) public payable onlyAdmin returns (bytes memory) {
bytes32 txHash = keccak256(
abi.encode(target, value, signature, data, eta)
);
require(
queuedTransactions[txHash],
"Timelock::executeTransaction: Transaction hasn't been queued."
);
require(
getBlockTimestamp() >= eta,
"Timelock::executeTransaction: Transaction hasn't surpassed time lock."
);
queuedTransactions[txHash] = false;
bytes memory callData;
// 如果没有签名,直接调用data
if (bytes(signature).length == 0) {
callData = data;
} else {
// 如果有签名,将签名和data打包
callData = abi.encodePacked(
bytes4(keccak256(bytes(signature))),
data
);
}
// 执行提案中要执行的target合约交易
(bool success, bytes memory returnData) = target.call{value: value}(
callData
);
require(
success,
"Timelock::executeTransaction: Transaction execution reverted."
);
emit ExecuteTransaction(txHash, target, value, signature, data, eta);
return returnData;
}
function getBlockTimestamp() internal view returns (uint) {
return block.number;
}
}
最后,附上测试代码:简单的测试逻辑是否通畅
我们发出一个提案:
bytes memory dataP = abi.encodeWithSignature( "withdraw(address,uint256)", bob, 100 ether );
从JCDao合约给bob withdraw 100 个ETH
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import "../src/JCDao/Dao.sol";
import "../src/JCDao/JCGovern.sol";
import "../src/JCDao/JCGovernImp.sol";
import "../src/JCDao/JCToken.sol";
import "../src/JCDao/TimeLock.sol";
contract JCDaoTest is Test {
address public owner;
address public alice;
address public bob;
address public david;
address public lucy;
address public delegatee1;
address public delegatee2;
address public delegatee3;
JCDao public dao;
JCGovern public govern;
JCGovernImp public governImp;
JCToken public token;
TimeLock public timelock;
function setUp() public {
owner = makeAddr("owner");
alice = makeAddr("alice");
bob = makeAddr("bob");
david = makeAddr("david");
lucy = makeAddr("lucy");
delegatee1 = makeAddr("delegatee1");
delegatee2 = makeAddr("delegatee2");
delegatee3 = makeAddr("delegator3");
deal(owner, 10000 ether);
deal(alice, 10000 ether);
deal(bob, 10000 ether);
deal(david, 10000 ether);
deal(lucy, 10000 ether);
vm.startPrank(owner);
{
token = new JCToken();
dao = new JCDao(owner, address(token));
governImp = new JCGovernImp();
timelock = new TimeLock(owner, 20);
govern = new JCGovern(
address(dao),
address(timelock),
address(token),
address(governImp),
20,
10
);
token.setDao(address(dao));
timelock.setAdmin(address(govern));
dao.setTimeLock(address(timelock));
dao.setAdmin(alice);
dao.addMember(alice);
dao.addMember(bob);
dao.addMember(david);
dao.addMember(lucy);
dao.addMember(owner);
dao.contribute{value: 200 ether}();
}
vm.stopPrank();
vm.startPrank(alice);
{
dao.contribute{value: 100 ether}();
}
vm.stopPrank();
vm.startPrank(bob);
{
dao.contribute{value: 100 ether}();
}
vm.stopPrank();
vm.startPrank(lucy);
{
dao.contribute{value: 100 ether}();
}
vm.stopPrank();
vm.startPrank(david);
{
dao.contribute{value: 100 ether}();
}
vm.stopPrank();
}
address[] targets;
uint[] values;
string[] signatures;
bytes[] calldatas;
string description;
function test() public {
vm.startPrank(alice);
{
bytes memory dataP = abi.encodeWithSignature(
"withdraw(address,uint256)",
bob,
100 ether
);
targets.push(address(dao));
values.push(0);
signatures.push("");
calldatas.push(dataP);
description = "Test proposal";
bytes memory dataG = abi.encodeWithSignature(
"propose(address[],uint256[],string[],bytes[],string)",
targets,
values,
signatures,
calldatas,
description
);
address(govern).call{value: 100}(dataG);
// 委托
token.delegate(delegatee1);
}
vm.stopPrank();
vm.roll(11);
//委托代理
vm.startPrank(bob);
{
token.delegate(delegatee1);
}
vm.stopPrank();
vm.startPrank(owner);
{
token.delegate(delegatee2);
}
vm.stopPrank();
vm.startPrank(david);
{
token.delegate(delegatee2);
}
vm.stopPrank();
vm.startPrank(lucy);
{
token.delegate(delegatee3);
}
vm.stopPrank();
vm.roll(21);
//代理投票
vm.startPrank(delegatee1);
{
bytes memory data = abi.encodeWithSignature(
"castVote(uint256,uint8)",
1,
0
);
address(govern).call(data);
}
vm.stopPrank();
vm.startPrank(delegatee2);
{
bytes memory data = abi.encodeWithSignature(
"castVote(uint256,uint8)",
1,
1
);
address(govern).call(data);
}
vm.stopPrank();
vm.startPrank(delegatee3);
{
bytes memory data = abi.encodeWithSignature(
"castVote(uint256,uint8)",
1,
2
);
address(govern).call(data);
}
vm.stopPrank();
vm.roll(32);
// 将提案加入执行队列(谁来执行都无所谓)
vm.startPrank(owner);
{
bytes memory data = abi.encodeWithSignature("queue(uint256)", 1);
address(govern).call(data);
}
vm.stopPrank();
// 执行提案
//必须等20个区块的delay时间过了
vm.roll(53);
//先记录bob之前的余额:
uint beginBalance = bob.balance;
vm.startPrank(owner);
{
bytes memory data = abi.encodeWithSignature("execute(uint256)", 1);
address(govern).call{value:100}(data);
}
vm.stopPrank();
// 检查提案是否成功执行
vm.roll(54);
assertEq(bob.balance,beginBalance+100 ether);
console.log("proposal success!");
}
}
可以看到,最后bob的余额增加了100个ETH !
完整源码可见作者仓库
https://github.com/TheLastHobbit/OpenSpace-Study/tree/main/day13/src/JCDao
我是Sanji,他们都叫我山鸡,在校大学生,web3小学生,有交流或Hackathon组队意向都可私信
个人微信:Z18382250961.
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!