Proposal life cycle

In the proposal life cycle, the initial step is its creation. We have previously discussed the creation of proposals that alter the state of system contracts, but it's important to note that the executor can be any contract. Suppose we have the UnknownToken token, and ownership of this token has been transferred to the DAO.

contract UnknownToken is ERC20, Ownable {
    constructor(string memory name, string memory symbol) ERC20(name, symbol) {}

    function mint(address to, uint256 amount) external onlyOwner {
        _mint(to, amount);
    }
}

Now, let's consider a scenario where we want to execute the mint function of this token via a proposal.

function createProposal(GovPool govPool, IERC20 token) external returns (uint256 proposalId) {
    address receiver = address(this);
    uint256 amount = 1 ether;

    IGovPool.ProposalAction[] memory actionsFor = new IGovPool.ProposalAction[](1);
    actionsFor[0] = IGovPool.ProposalAction({
        executor: address(token),
        value: 0,
        data: abi.encodeWithSelector(UnknownToken.mint.selector, receiver, amount)
    });

    IGovPool.ProposalAction[] memory actionsAgainst = new IGovPool.ProposalAction[](0);

    govPool.createProposal("Mint token", actionsFor, actionsAgainst);

    return GovPool(govPool).latestProposalId();
}

You can also add specific validations that will be passed each time you create a proposal with your contract functioning as the main executor. To achieve this, you should implement the IProposalValidator interface. In this particular case, the validation ensures that the only action within the proposal is the minting of some amount of tokens. If the validate method returns false, the creation of the proposal will be reverted.

contract UnknownToken is IProposalValidator {
    function validate(
        IGovPool.ProposalAction[] calldata actions
    ) external view override returns (bool valid) {
        return actions.length == 1 && bytes4(actions[0].data[0:4]) == UnknownToken.mint.selector;
    }
}

Once created, a proposal initially enters the Voting state, making it available for user to cast their votes. The voting period remains open as long as the quorum is not reached or the voting time hasn't elapsed. Users may also vote against proposals. If votes against the proposal reach a quorum, the status of the proposal becomes Defeated. Now we can pass the proposalId from the just-created proposal to the voting function and cast a vote in favor of it.

function vote(IGovPool govPool, uint256 proposalId) external {
    require(
        govPool.getProposalState(proposalId) == IGovPool.ProposalState.Voting,
        "Not voting state"
    );

    bool isVoteFor = true;
    uint256 amount = 1 ether;
    uint256[] memory nftIds = new uint256[](1);

    govPool.vote(proposalId, isVoteFor, amount, nftIds);
}

To save on gas, consider utilizing the createProposalAndVote function.

function createProposalAndVote(IGovPool govPool) external {
    // ...
    
    govPool.createProposalAndVote("MintToken", actionsFor, actionsAgainst, amount, nftIds);
}

It's possible to cancel your entire vote on the proposal, including the delegated power, by simply calling the cancelVote method. After that, your assets will no longer be locked and you can withdraw them.

function cancelVote(IGovPool govPool, uint256 proposalId) external {
    require(
        govPool.getProposalState(proposalId) == IGovPool.ProposalState.Voting,
        "Not voting state"
    );

    govPool.cancelVote(proposalId);
}

Assuming our proposal has reached the quorum and has exceeded duration if earlyCompletion is false, it can exist in one of three possible states. The first state is Locked in which case we only need to wait for the executionDelay. The second state is SucceededFor or SucceededAgainst indicating that it is ready for execution. In this case, we can simply call the execute method.

function execute(IGovPool govPool, uint256 proposalId) external {
    IGovPool.ProposalState proposalState = govPool.getProposalState(proposalId);

    require(
        proposalState == IGovPool.ProposalState.SucceededFor ||
            proposalState == IGovPool.ProposalState.SucceededAgainst,
        "Not succeeded state"
    );

    govPool.execute(proposalId);
}

The WaitingForVotingTransfer state, indicating that validatorsVote is true, signifies that the proposal is ready to be moved to the second stage of voting. To initiate this, the moveProposalToValidators method should be called by any user. This method triggers the creation of the corresponding external proposal in the GovValidators contract. External proposals for the GovValidators contract have the same ids as those on the GovPool contract.

function moveProposalToValidators(IGovPool govPool, uint256 proposalId) external {
    require(
        govPool.getProposalState(proposalId) ==
            IGovPool.ProposalState.WaitingForVotingTransfer,
        "Not waiting for transfer state"
    );

    govPool.moveProposalToValidators(proposalId);
}

Following the transition of the proposal to the validators, the validators will have the ability to cast their votes for it.

function validatorVote(IGovPool govPool, uint256 proposalId) external {
    (, , address govValidatorsAddress, , ) = govPool.getHelperContracts();
    IGovValidators govValidators = IGovValidators(govValidatorsAddress);

    bool isInternal = false;
    require(
        govValidators.getProposalState(proposalId, isInternal) ==
            IGovValidators.ProposalState.Voting,
        "Not voting state"
    );

    uint256 amount = 1 ether;
    bool isVoteFor = true;

    govValidators.voteExternalProposal(proposalId, amount, isVoteFor);
}

Once the external proposal has the Succeeded status on the GovValidators contract, it will also attain the SucceededFor or SucceededAgainst status on the GovPool contract, allowing it to be executed in the usual manner.

function executeAfterValidators(IGovPool govPool, uint256 proposalId) external {
    (, , address govValidatorsAddress, , ) = govPool.getHelperContracts();
    IGovValidators govValidators = IGovValidators(govValidatorsAddress);

    bool isInternal = false;
    require(
        govValidators.getProposalState(proposalId, isInternal) ==
            IGovValidators.ProposalState.Succeeded,
        "Not succeeded state"
    );

    IGovPool.ProposalState proposalState = govPool.getProposalState(proposalId);

    assert(
        proposalState == IGovPool.ProposalState.SucceededFor ||
            proposalState == IGovPool.ProposalState.SucceededAgainst
    );

    govPool.execute(proposalId);
}

Well done! When you call the execute method, the tokens are minted to the address specified in the proposal. You can verify this by simply calling the balanceOf method on the token contract.