SimpleDAO.sol// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title SimpleDAO - 简化版 DAO 治理合约
/// @notice 教学演示用,不可用于生产
interface IERC20 {
function balanceOf(address account) external view returns (uint);
}
contract SimpleDAO {
IERC20 public governanceToken;
uint public proposalCount;
uint public constant VOTING_PERIOD = 3 days; // 投票期
uint public constant TIMELOCK_DELAY = 2 days; // 执行延迟
uint public constant QUORUM = 100e18; // 最低投票总数(100 票)
enum ProposalState { Active, Defeated, Succeeded, Queued, Executed }
struct Proposal {
address proposer;
address target;
bytes data;
string description;
uint voteFor;
uint voteAgainst;
uint startTime;
uint endTime;
uint eta; // Estimated Time for execution
ProposalState state;
}
mapping(uint => Proposal) public proposals;
mapping(uint => mapping(address => bool)) public hasVoted;
event ProposalCreated(uint id, address proposer, string description);
event Voted(uint id, address voter, bool support, uint weight);
event ProposalQueued(uint id, uint eta);
event ProposalExecuted(uint id);
constructor(address _token) {
governanceToken = IERC20(_token);
}
/// @notice 创建提案
function propose(address target, bytes calldata data, string calldata description) external {
proposalCount++;
proposals[proposalCount] = Proposal({
proposer: msg.sender,
target: target,
data: data,
description: description,
voteFor: 0,
voteAgainst: 0,
startTime: block.timestamp,
endTime: block.timestamp + VOTING_PERIOD,
eta: 0,
state: ProposalState.Active
});
emit ProposalCreated(proposalCount, msg.sender, description);
}
/// @notice 投票
function vote(uint proposalId, bool support) external {
Proposal storage proposal = proposals[proposalId];
require(block.timestamp >= proposal.startTime, "voting not started");
require(block.timestamp <= proposal.endTime, "voting ended");
require(!hasVoted[proposalId][msg.sender], "already voted");
uint weight = governanceToken.balanceOf(msg.sender);
require(weight > 0, "no voting power");
if (support) {
proposal.voteFor += weight;
} else {
proposal.voteAgainst += weight;
}
hasVoted[proposalId][msg.sender] = true;
emit Voted(proposalId, msg.sender, support, weight);
}
/// @notice 投票结果检查,并进入 Timelock 队列
function queue(uint proposalId) external {
Proposal storage proposal = proposals[proposalId];
require(block.timestamp > proposal.endTime, "voting not ended");
require(proposal.state == ProposalState.Active, "not active");
if (proposal.voteFor <= proposal.voteAgainst || proposal.voteFor < QUORUM) {
proposal.state = ProposalState.Defeated;
} else {
proposal.state = ProposalState.Queued;
proposal.eta = block.timestamp + TIMELOCK_DELAY;
emit ProposalQueued(proposalId, proposal.eta);
}
}
/// @notice 执行提案
function execute(uint proposalId) external {
Proposal storage proposal = proposals[proposalId];
require(proposal.state == ProposalState.Queued, "not queued");
require(block.timestamp >= proposal.eta, "timelock not expired");
(bool success, ) = proposal.target.call(proposal.data);
require(success, "execution failed");
proposal.state = ProposalState.Executed;
emit ProposalExecuted(proposalId);
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/SimpleDAO.sol";
/// @notice 简单的治理代币 (ERC20-like)
contract GovernanceToken is IERC20 {
string public name = "GovToken";
string public symbol = "GOV";
uint8 public decimals = 18;
uint public totalSupply;
mapping(address => uint) public balanceOf;
function mint(address to, uint amount) external {
balanceOf[to] += amount;
totalSupply += amount;
}
}
/// @notice 被治理的目标合约(DAO 将控制它)
contract TargetContract {
uint public value;
function setValue(uint _value) external {
value = _value;
}
}
contract SimpleDAOTest is Test {
GovernanceToken public gov;
SimpleDAO public dao;
TargetContract public target;
address alice = address(0x123);
address bob = address(0x234);
function setUp() public {
gov = new GovernanceToken();
dao = new SimpleDAO(address(gov));
target = new TargetContract();
// 给 Alice 和 Bob 铸造治理代币
gov.mint(alice, 100e18);
gov.mint(bob, 50e18);
}
/// @notice 测试完整的提案生命周期
function testProposalLifecycle() public {
vm.startPrank(alice);
// Alice 提出一个提案:调用 target.setValue(42)
bytes memory data = abi.encodeWithSignature("setValue(uint256)", 42);
dao.propose(address(target), data, "Set value to 42");
vm.stopPrank();
// Alice 投支持票
vm.startPrank(alice);
dao.vote(1, true);
vm.stopPrank();
// Bob 投反对票
vm.startPrank(bob);
dao.vote(1, false);
vm.stopPrank();
// 快进 3 天,投票结束
vm.warp(block.timestamp + 3 days + 1);
// 进入 Timelock 队列
dao.queue(1);
// 立即执行应失败(需要 timelock)
vm.expectRevert();
dao.execute(1);
// 再快进 2 天
vm.warp(block.timestamp + 2 days);
// 执行提案
dao.execute(1);
// 验证目标合约的值已被修改
assertEq(target.value(), 42);
}
}执行测试:
➜ tutorial git:(main) ✗ forge test --match-path test/SimpleDAO.t.sol -vvv
[⠊] Compiling...
[⠒] Compiling 1 files with Solc 0.8.30
[⠑] Solc 0.8.30 finished in 551.36ms
Compiler run successful!
Ran 1 test for test/SimpleDAO.t.sol:SimpleDAOTest
[PASS] testProposalLifecycle() (gas: 416882)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.95ms (2.33ms CPU time)
Ran 1 test suite in 165.45ms (5.95ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)