Writing actions in solidity
Every proposal, standard or emergency, consists of a set of Actions that define the changes to be made to the protocol. Said actions can be manually defined in the DAO UI, or they can be defined in a smart contract script that can then be compiled and executed by the DAO.
This guide outlines how to construct such a script in Solidity, as well as compile it to calldata to be submitted in the DAO interface.
Building Actions
To ensure proposal actions can be easily verified by other Security Council members, we recommend writing readable Solidity code that defines the actions, then compiling it to generate the final calldata. This approach is preferable to decoding raw binary manually, as the code clearly conveys the logic and intent behind each action, making peer review more straightforward.
A sample proposal script is provided below, which demonstrates how to build actions for both L1 and L2 networks. The script uses the BuildProposal
base contract to define the actions. It can be found here in our mono repo.
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;
import "./BuildProposal.sol";
// To print the proposal action data: `P=0001 pnpm proposal`// To dryrun the proposal on L1: `P=0001 pnpm proposal:dryrun:l1`// To dryrun the proposal on L2: `P=0001 pnpm proposal:dryrun:l2`contract Proposal0001 is BuildProposal { address public constant L1_FOO_CONTRACT = 0x4c234082E57d7f82AB8326A338d8F17FAbEdbd97; address public constant L2_BAR_CONTRACT = 0x0e577Bb67d38c18E4B9508984DA36d6D316ade58; address public constant ALICE = address(0x1);
function buildL1Actions() internal pure override returns (Controller.Action[] memory actions) { actions = new Controller.Action[](1); actions[0] = Controller.Action({ target: L1_FOO_CONTRACT, value: 0, data: abi.encodeCall(Ownable.transferOwnership, (ALICE)) }); }
function buildL2Actions() internal pure override returns (uint64 l2ExecutionId, uint32 l2GasLimit, Controller.Action[] memory actions) { l2ExecutionId = 0; l2GasLimit = 1_000_000; actions = new Controller.Action[](1); actions[0] = Controller.Action({ target: L2_BAR_CONTRACT, value: 0, data: abi.encodeCall(Ownable.transferOwnership, (ALICE)) }); }}
To convert all actions into binary calldata for use in the DAO UI, run P=0001 pnpm proposal
in packages/protocol
(assuming the action script is named Proposal0001.s.sol). The generated calldata will be printed to the console and also saved in Proposal0001.action.md. The output will look like the following:
To confirm that the script functions as intended, you should dry run the proposal on both L1 and L2 networks. This can be done with the commands P=0001 pnpm proposal:dryrun:l1
and P=0001 pnpm proposal:dryrun:l2
, respectively. The dry run will simulate the execution of the proposal without making any actual changes to the protocol.
You should expect a DryrunSucceeded()
event in the console output as seen below, as well as a revert with that event. If it does not emit said event, it means the proposal actions would fail under the current blockchain state.
Once you confirm that the proposal actions are correct and the dry-runs are successful, you can proceed to submit the proposal in the DAO UI. Copy the generated calldata from the console output or from your ProposalX.action.md
file, and paste it into the Actions
-> Add raw calldata
field in the proposal building part of the DAO UI.
Access Restricted
This page requires a connected wallet with authorized access.
Please connect your wallet using the sidebar to continue.
If you require access, please get in touch with the DAO Coordinator.
Checking wallet access...