🎓 A1Learner
🔍
0 of 45 lessons completed

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?