Solidity Project: Governance Treasury
A governance treasury is a contract that holds funds and spends them only when token holders vote for a proposal. This project combines token-weighted voting, delegation, a timelock, and treasury payouts into one working system.
Delegation moves vote power to another address
Delegation is handing your vote power to someone else to cast. The delegate votes with your tokens, and you stop voting with them yourself.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract GovToken {
string public name;
address public owner;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => uint256) public votes;
mapping(address => address) public delegation;
constructor(string memory tokenName) {
name = tokenName;
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function mint(address to, uint256 amount) public onlyOwner {
balanceOf[to] += amount;
totalSupply += amount;
address d = delegation[to];
if (d == address(0)) {
votes[to] += amount;
} else {
votes[d] += amount;
}
}
function delegate(address to) public {
address old = delegation[msg.sender];
address from = old == address(0) ? msg.sender : old;
votes[from] -= balanceOf[msg.sender];
delegation[msg.sender] = to;
votes[to] += balanceOf[msg.sender];
}
function votingPower(address account) public view returns (uint256) {
return votes[account];
}
}
contract Governor {
GovToken public token;
uint256 public delay;
uint256 public quorum;
uint256 public proposalCount;
struct Proposal {
address recipient;
uint256 amount;
uint256 forVotes;
uint256 againstVotes;
bool queued;
bool executed;
uint256 eta;
}
mapping(uint256 => Proposal) public proposals;
mapping(uint256 => mapping(address => bool)) public hasVoted;
constructor(GovToken token_) {
token = token_;
delay = 100;
quorum = 50;
}
receive() external payable {}
function propose(address recipient, uint256 amount) public returns (uint256) {
proposalCount += 1;
proposals[proposalCount] = Proposal(recipient, amount, 0, 0, false, false, 0);
return proposalCount;
}
function vote(uint256 proposalId, bool support) public {
require(!hasVoted[proposalId][msg.sender], "already voted");
hasVoted[proposalId][msg.sender] = true;
uint256 power = token.votingPower(msg.sender);
require(power > 0, "no votes");
if (support) {
proposals[proposalId].forVotes += power;
} else {
proposals[proposalId].againstVotes += power;
}
}
function queue(uint256 proposalId) public {
Proposal storage p = proposals[proposalId];
require(!p.queued, "already queued");
require(p.forVotes > p.againstVotes && p.forVotes >= quorum, "not passed");
p.queued = true;
p.eta = block.timestamp + delay;
}
function execute(uint256 proposalId) public {
Proposal storage p = proposals[proposalId];
require(p.queued, "not queued");
require(!p.executed, "already executed");
require(block.timestamp >= p.eta, "too early");
p.executed = true;
(bool ok, ) = p.recipient.call{value: p.amount}("");
require(ok, "payout failed");
}
}
A timelock delays execution after a vote passes
A timelock is a waiting period between queuing a proposal and executing it. Execution before the wait ends must revert.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract GovToken {
string public name;
address public owner;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => uint256) public votes;
mapping(address => address) public delegation;
constructor(string memory tokenName) {
name = tokenName;
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function mint(address to, uint256 amount) public onlyOwner {
balanceOf[to] += amount;
totalSupply += amount;
address d = delegation[to];
if (d == address(0)) {
votes[to] += amount;
} else {
votes[d] += amount;
}
}
function delegate(address to) public {
address old = delegation[msg.sender];
address from = old == address(0) ? msg.sender : old;
votes[from] -= balanceOf[msg.sender];
delegation[msg.sender] = to;
votes[to] += balanceOf[msg.sender];
}
function votingPower(address account) public view returns (uint256) {
return votes[account];
}
}
contract Governor {
GovToken public token;
uint256 public delay;
uint256 public quorum;
uint256 public proposalCount;
struct Proposal {
address recipient;
uint256 amount;
uint256 forVotes;
uint256 againstVotes;
bool queued;
bool executed;
uint256 eta;
}
mapping(uint256 => Proposal) public proposals;
mapping(uint256 => mapping(address => bool)) public hasVoted;
constructor(GovToken token_) {
token = token_;
delay = 100;
quorum = 50;
}
receive() external payable {}
function propose(address recipient, uint256 amount) public returns (uint256) {
proposalCount += 1;
proposals[proposalCount] = Proposal(recipient, amount, 0, 0, false, false, 0);
return proposalCount;
}
function vote(uint256 proposalId, bool support) public {
require(!hasVoted[proposalId][msg.sender], "already voted");
hasVoted[proposalId][msg.sender] = true;
uint256 power = token.votingPower(msg.sender);
require(power > 0, "no votes");
if (support) {
proposals[proposalId].forVotes += power;
} else {
proposals[proposalId].againstVotes += power;
}
}
function queue(uint256 proposalId) public {
Proposal storage p = proposals[proposalId];
require(!p.queued, "already queued");
require(p.forVotes > p.againstVotes && p.forVotes >= quorum, "not passed");
p.queued = true;
p.eta = block.timestamp + delay;
}
function execute(uint256 proposalId) public {
Proposal storage p = proposals[proposalId];
require(p.queued, "not queued");
require(!p.executed, "already executed");
require(block.timestamp >= p.eta, "too early");
p.executed = true;
(bool ok, ) = p.recipient.call{value: p.amount}("");
require(ok, "payout failed");
}
}
Treasury money moves only through an executed proposal
The treasury holds ether, but it has no direct payout function. A payout happens only when a passed proposal is queued and its delay has passed.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract GovToken {
string public name;
address public owner;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => uint256) public votes;
mapping(address => address) public delegation;
constructor(string memory tokenName) {
name = tokenName;
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function mint(address to, uint256 amount) public onlyOwner {
balanceOf[to] += amount;
totalSupply += amount;
address d = delegation[to];
if (d == address(0)) {
votes[to] += amount;
} else {
votes[d] += amount;
}
}
function delegate(address to) public {
address old = delegation[msg.sender];
address from = old == address(0) ? msg.sender : old;
votes[from] -= balanceOf[msg.sender];
delegation[msg.sender] = to;
votes[to] += balanceOf[msg.sender];
}
function votingPower(address account) public view returns (uint256) {
return votes[account];
}
}
contract Governor {
GovToken public token;
uint256 public delay;
uint256 public quorum;
uint256 public proposalCount;
struct Proposal {
address recipient;
uint256 amount;
uint256 forVotes;
uint256 againstVotes;
bool queued;
bool executed;
uint256 eta;
}
mapping(uint256 => Proposal) public proposals;
mapping(uint256 => mapping(address => bool)) public hasVoted;
constructor(GovToken token_) {
token = token_;
delay = 100;
quorum = 50;
}
receive() external payable {}
function propose(address recipient, uint256 amount) public returns (uint256) {
proposalCount += 1;
proposals[proposalCount] = Proposal(recipient, amount, 0, 0, false, false, 0);
return proposalCount;
}
function vote(uint256 proposalId, bool support) public {
require(!hasVoted[proposalId][msg.sender], "already voted");
hasVoted[proposalId][msg.sender] = true;
uint256 power = token.votingPower(msg.sender);
require(power > 0, "no votes");
if (support) {
proposals[proposalId].forVotes += power;
} else {
proposals[proposalId].againstVotes += power;
}
}
function queue(uint256 proposalId) public {
Proposal storage p = proposals[proposalId];
require(!p.queued, "already queued");
require(p.forVotes > p.againstVotes && p.forVotes >= quorum, "not passed");
p.queued = true;
p.eta = block.timestamp + delay;
}
function execute(uint256 proposalId) public {
Proposal storage p = proposals[proposalId];
require(p.queued, "not queued");
require(!p.executed, "already executed");
require(block.timestamp >= p.eta, "too early");
p.executed = true;
(bool ok, ) = p.recipient.call{value: p.amount}("");
require(ok, "payout failed");
}
}
Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract GovToken {
string public name;
address public owner;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => uint256) public votes;
mapping(address => address) public delegation;
constructor(string memory tokenName) {
name = tokenName;
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function mint(address to, uint256 amount) public onlyOwner {
balanceOf[to] += amount;
totalSupply += amount;
address d = delegation[to];
if (d == address(0)) {
votes[to] += amount;
} else {
votes[d] += amount;
}
}
function delegate(address to) public {
address old = delegation[msg.sender];
address from = old == address(0) ? msg.sender : old;
votes[from] -= balanceOf[msg.sender];
delegation[msg.sender] = to;
votes[to] += balanceOf[msg.sender];
}
function votingPower(address account) public view returns (uint256) {
return votes[account];
}
}
contract Governor {
GovToken public token;
uint256 public delay;
uint256 public quorum;
uint256 public proposalCount;
struct Proposal {
address recipient;
uint256 amount;
uint256 forVotes;
uint256 againstVotes;
bool queued;
bool executed;
uint256 eta;
}
mapping(uint256 => Proposal) public proposals;
mapping(uint256 => mapping(address => bool)) public hasVoted;
constructor(GovToken token_) {
token = token_;
delay = 100;
quorum = 50;
}
receive() external payable {}
function propose(address recipient, uint256 amount) public returns (uint256) {
proposalCount += 1;
proposals[proposalCount] = Proposal(recipient, amount, 0, 0, false, false, 0);
return proposalCount;
}
function vote(uint256 proposalId, bool support) public {
require(!hasVoted[proposalId][msg.sender], "already voted");
hasVoted[proposalId][msg.sender] = true;
uint256 power = token.votingPower(msg.sender);
require(power > 0, "no votes");
if (support) {
proposals[proposalId].forVotes += power;
} else {
proposals[proposalId].againstVotes += power;
}
}
function queue(uint256 proposalId) public {
Proposal storage p = proposals[proposalId];
require(!p.queued, "already queued");
require(p.forVotes > p.againstVotes && p.forVotes >= quorum, "not passed");
p.queued = true;
p.eta = block.timestamp + delay;
}
function execute(uint256 proposalId) public {
Proposal storage p = proposals[proposalId];
require(p.queued, "not queued");
require(!p.executed, "already executed");
require(block.timestamp >= p.eta, "too early");
p.executed = true;
(bool ok, ) = p.recipient.call{value: p.amount}("");
require(ok, "payout failed");
}
}
Key points
- Quorum: Quorum is the minimum number of supporting votes a proposal needs to pass.
- Delegation: Delegation hands a holder's vote power to a delegate who votes in their place.
- Timelock: A timelock is a fixed waiting period between queuing a proposal and executing it.
- Proposal lifecycle: A proposal moves from proposed to voted to queued to executed, and a defeated proposal stops at defeated.
- Treasury: The treasury is the pool of funds that only an executed proposal can spend.
In this example, the voter holds 100 tokens and receives 60 more votes by delegation, then proposes a 2 ether payout to ANA. The proposal passes with 160 votes, is queued, waits out the delay, and executes so the treasury pays exactly 2 ether and keeps 3 ether.
Note
Execution here is open to any caller once a proposal is queued and its delay has passed, because the vote and the delay are the guards.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Remix IDE
Exercise
A proposal has 40 votes for, 10 against, and the quorum is 50. What happens when someone tries to queue it?