Solidity Proposal Lifecycle
A proposal lifecycle is the set of states a proposal moves through from creation to execution. Each state allows only the next step that fits it.
A proposal starts pending and votes count while active
Starting moves the proposal from Pending to Active. Votes add to the for count or the against count.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract Proposal {
enum State { Pending, Active, Succeeded, Defeated, Queued, Executed }
State public state;
uint256 public forVotes;
uint256 public againstVotes;
uint256 public votingEnds;
uint256 public quorum;
constructor() {
state = State.Pending;
votingEnds = block.timestamp + 100;
quorum = 10;
}
function start() public {
require(state == State.Pending, "not pending");
state = State.Active;
}
function vote(bool support, uint256 weight) public {
require(state == State.Active, "not active");
if (support) {
forVotes += weight;
} else {
againstVotes += weight;
}
}
}
Finalizing after voting ends decides succeeded or defeated
Finalizing needs the voting time to have passed. A proposal needs both a majority and the quorum to succeed.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ProposalOutcome {
enum State { Pending, Active, Succeeded, Defeated }
State public state;
uint256 public forVotes;
uint256 public againstVotes;
uint256 public votingEnds;
uint256 public quorum;
constructor() {
state = State.Active;
votingEnds = block.timestamp + 100;
quorum = 10;
}
function vote(bool support, uint256 weight) public {
require(state == State.Active, "not active");
if (support) {
forVotes += weight;
} else {
againstVotes += weight;
}
}
function finalize() public {
require(state == State.Active, "not active");
require(block.timestamp >= votingEnds, "voting open");
if (forVotes > againstVotes && forVotes >= quorum) {
state = State.Succeeded;
} else {
state = State.Defeated;
}
}
}
Only a succeeded proposal can be queued and executed
Queueing moves a succeeded proposal one step forward. A defeated proposal cannot queue at all.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ProposalQueue {
enum State { Succeeded, Defeated, Queued, Executed }
State public state;
bool public executed;
constructor(bool passed) {
if (passed) {
state = State.Succeeded;
} else {
state = State.Defeated;
}
}
function queue() public {
require(state == State.Succeeded, "not succeeded");
state = State.Queued;
}
function execute() public {
require(state == State.Queued, "not queued");
state = State.Executed;
executed = true;
}
}
Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ProposalLifecycle {
enum State { Pending, Active, Succeeded, Defeated, Queued, Executed }
State public state;
uint256 public forVotes;
uint256 public againstVotes;
uint256 public votingEnds;
uint256 public executeAfter;
uint256 public quorum;
uint256 public value;
constructor() {
state = State.Pending;
votingEnds = block.timestamp + 100;
quorum = 10;
}
function start() public {
require(state == State.Pending, "not pending");
state = State.Active;
}
function vote(bool support, uint256 weight) public {
require(state == State.Active, "not active");
if (support) {
forVotes += weight;
} else {
againstVotes += weight;
}
}
function finalize() public {
require(state == State.Active, "not active");
require(block.timestamp >= votingEnds, "voting open");
if (forVotes > againstVotes && forVotes >= quorum) {
state = State.Succeeded;
} else {
state = State.Defeated;
}
}
function queue() public {
require(state == State.Succeeded, "not succeeded");
state = State.Queued;
executeAfter = block.timestamp + 50;
}
function execute(uint256 newValue) public {
require(state == State.Queued, "not queued");
require(block.timestamp >= executeAfter, "too early");
state = State.Executed;
value = newValue;
}
}
Key points
- Proposal: A proposal is a suggested action that voters accept or reject.
- State: A state is one named stage of the lifecycle such as Active or Defeated.
- Quorum: A quorum is the minimum amount of supporting votes a proposal needs to pass.
- Succeeded: A succeeded proposal won the vote and met the quorum so it may be queued.
- Defeated: A defeated proposal lost the vote or missed the quorum so it stops.
- Restatement: The lifecycle moves a proposal forward one stage at a time and a defeated proposal never reaches execution.
In this example, ProposalLifecycle starts a proposal, counts its votes, finalizes it after a warp, queues it with a further delay, and executes it after a second warp, while a defeated proposal stops at Defeated and cannot queue.
Note
Voting ends at a fixed time in this toy contract and the clock is moved by warping in the proof.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Remix IDE
Exercise
A proposal has 8 for votes, 2 against votes, and a quorum of 10. What happens when it finalizes?