Skip to content
16 changes: 16 additions & 0 deletions packages/protocol/contracts/layer1/mainnet/LibL1Addrs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,28 @@ pragma solidity ^0.8.24;
library LibL1Addrs {
address public constant DAO = 0x9CDf589C941ee81D75F34d3755671d614f7cf261;
address public constant DAO_SIGNER_LIST = 0x0F95E6968EC1B28c794CF1aD99609431de5179c2;
address public constant DAO_ENCRYPTION_REGISTRY = 0x2eFDb93a3B87b930E553d504db67Ee41c69C42d1;
address public constant DAO_STANDARD_MULTISIG = 0xD7dA1C25E915438720692bC55eb3a7170cA90321;
address public constant DAO_EMERGENCY_MULTISIG = 0x2AffADEb2ef5e1F2a7F58964ee191F1e88317ECd;
address public constant DAO_CONTROLLER = 0x75Ba76403b13b26AD1beC70D6eE937314eeaCD0a;
address public constant DAO_OPTIMISTIC_TOKEN_VOTING_PLUGIN =
0x989E348275b659d36f8751ea1c10D146211650BE;

// Security Council seat addresses (DAO_SIGNER_LIST membership before/after Proposal0020)
// Seats retained by Proposal0020
address public constant SC_TAIKO_LABS = 0xb47fE76aC588101BFBdA9E68F66433bA51E8029a;
address public constant SC_L2BEAT = 0xf1cF63589A1e012F9124182c9eAa36B5333e5f06;
address public constant SC_ARAGON = 0xb284810536C0dAB6A8e48153B58588A9B9e0F701;
address public constant SC_NETHERMIND = 0x5353c607e6eca6C63FEC5c6C0F5CC3a5348d5c95;
// Independent seat (EOA) added by Proposal0020
address public constant SC_GUSTAVO_GONZALEZ = 0xAC5898b0FFFd23F4Ef09F0E50Fa1bC4896eF7163;
// Seats removed by Proposal0020
address public constant SC_CHAINBOUND = 0x436a1075099A145417EBFc74BBaC9605e3e4f1A7;
address public constant SC_HALBORN = 0x0F40268Ec0Dc8D88CF2f22E227A29a0b478b6351;
address public constant SC_DREW_VAN_DER_WERFF = 0x25d3E89bAcE2040Ed3aF7c4c7B505cfBB72fD6f1;
address public constant SC_TONI_WAHRSTATTER = 0xa384E224A3F3D664F43eBE33395eF0DCcE67e894;
address public constant SC_GATTACA = 0x6268d189E011Aa53A2f09A1FE159445BeB3d878E;

address public constant FORCED_INCLUSION_STORE = 0x05d88855361808fA1d7fc28084Ef3fCa191c4e03;
// Kept for legacy governance scripts (e.g. Proposal0009)
address public constant TAIKO_WRAPPER = 0x9F9D2fC7abe74C79f86F0D1212107692430eef72;
Expand Down
262 changes: 262 additions & 0 deletions packages/protocol/script/layer1/governance/BuildDirectProposal.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "forge-std/src/Script.sol";
import { LibL1Addrs as L1 } from "src/layer1/mainnet/LibL1Addrs.sol";

/// @notice Base for DAO proposals whose actions the Aragon DAO executes directly — i.e.
/// changes to the governance stack itself (SignerList, multisig plugins,
/// EncryptionRegistry). Protocol changes executed via the DAO Controller use
/// `BuildProposal` instead; the controller holds no permissions on the Aragon contracts,
/// so its `Execute` path cannot carry these actions.
///
/// Such proposals are created on the Standard Multisig, usually via the Taiko DAO UI
/// (dao.taiko.xyz): the UI pins the metadata to IPFS and assembles the `createProposal`
/// call from actions pasted into its custom-action (calldata) form.
///
/// Modes (`MODE` env):
/// - `print`: write `Proposal$P.action.md` — the actions to paste into the DAO UI.
/// If `METADATA_URI` is set, also emit the full `createProposal` calldata
/// for direct submission (requires pre-pinned metadata).
/// - `l1dryrun`: on a fork — run `checkBaseline()`, create the proposal as `SENDER`
/// (`APPROVE=true` additionally approves at creation; default false),
/// run `simulatePreExecution()`, apply the actions as the DAO, then run
/// `checkPostState()`.
///
/// The print-mode direct-submission fallback always encodes `approveProposal = false`;
/// a direct submitter approves separately.
abstract contract BuildDirectProposal is Script {
/// @dev Mirrors Aragon OSx `IDAO.Action`.
struct Action {
address to;
uint256 value;
bytes data;
}

error MissingEnv(string name);
error CheckFailed(string what);

function run() external {
string memory mode = vm.envString("MODE");
if (keccak256(abi.encodePacked(mode)) == keccak256(abi.encodePacked("print"))) {
logProposalAction(vm.envString("P"));
} else if (keccak256(abi.encodePacked(mode)) == keccak256(abi.encodePacked("l1dryrun"))) {
dryrunL1();
} else {
console2.log("Error: Invalid mode. Must be one of: print, l1dryrun");
}
}

/// @dev The actions the DAO will execute, in order.
function buildDaoActions() internal view virtual returns (Action[] memory);

/// @dev Pre-creation assertions against the forked chain (dryrun only).
function checkBaseline() internal view virtual { }

/// @dev Out-of-band steps that must happen between proposal creation and DAO
/// execution, simulated on the fork (dryrun only). Default: none.
function simulatePreExecution() internal virtual { }

/// @dev Post-execution assertions against the forked chain (dryrun only).
function checkPostState() internal view virtual { }

// ---------------------------------------------------------------
// Mode implementations
// ---------------------------------------------------------------

function logProposalAction(string memory _proposalId) internal {
Action[] memory actions = buildDaoActions();

string memory fileName =
string.concat("./script/layer1/proposals/Proposal", _proposalId, ".action.md");
string memory fileContent = string.concat(
"# Proposal",
_proposalId,
"\n\nCreated via the Taiko DAO UI on the Standard Multisig `",
vm.toString(L1.DAO_STANDARD_MULTISIG),
"`.\nThe UI pins the metadata and assembles `createProposal`; paste each action",
" below into the\nUI's custom-action (calldata) form, in order. After creation,",
" run the `verify` mode\nagainst the new proposal id before approving.\n\n",
"- Destination plugin (set by the UI): `",
vm.toString(L1.DAO_OPTIMISTIC_TOKEN_VOTING_PLUGIN),
"`\n"
);

for (uint256 i; i < actions.length; ++i) {
fileContent = string.concat(
fileContent,
"\n## Action ",
vm.toString(i + 1),
"\n- To: `",
vm.toString(actions[i].to),
"` (",
nameOf(actions[i].to),
")\n- Value: `",
vm.toString(actions[i].value),
"`\n- Data: `",
vm.toString(actions[i].data),
"`\n"
);
}

// Fallback for submitting createProposal directly (not via the UI): requires a
// pre-pinned METADATA_URI, since nothing pins the metadata for you outside the UI.
string memory metadataURI = vm.envOr("METADATA_URI", string(""));
if (bytes(metadataURI).length > 0) {
fileContent = string.concat(
fileContent,
"\n## Direct submission fallback (bypassing the UI)\n- To (Standard Multisig): `",
vm.toString(L1.DAO_STANDARD_MULTISIG),
"`\n- Function: `createProposal`\n- Value: `0`\n- Metadata URI: `",
metadataURI,
"`\n- Calldata: `",
vm.toString(buildCreateProposalCalldata(metadataURI, false)),
"`\n"
);
}

vm.writeFile(fileName, fileContent);
console2.log(fileContent);
console2.log("Proposal action details written to", fileName);
}

function dryrunL1() internal {
checkBaseline();

address sender = vm.envOr("SENDER", address(0));
if (sender == address(0)) revert MissingEnv("SENDER");
string memory metadataURI = vm.envOr("METADATA_URI", string("ipfs://dryrun-placeholder"));

// Built before the prank: a subclass's buildDaoActions may make view calls, and
// an external call inside the argument expression would consume the prank.
bytes memory createCalldata =
buildCreateProposalCalldata(metadataURI, vm.envOr("APPROVE", false));
vm.prank(sender);
(bool ok, bytes memory ret) = L1.DAO_STANDARD_MULTISIG.call(createCalldata);
logIfReverted(ok, ret);
check(ok, "createProposal reverted");
console2.log("createProposal accepted, proposalId:", abi.decode(ret, (uint256)));

simulatePreExecution();

Action[] memory actions = buildDaoActions();
for (uint256 i; i < actions.length; ++i) {
check(
actions[i].to.code.length > 0,
string.concat("action ", vm.toString(i + 1), " target has no code")
);
vm.prank(L1.DAO);
(ok, ret) = actions[i].to.call{ value: actions[i].value }(actions[i].data);
logIfReverted(ok, ret);
check(ok, string.concat("action ", vm.toString(i + 1), " reverted"));
}

checkPostState();
console2.log("Dryrun OK");
}

// ---------------------------------------------------------------
// Helpers for subclasses
// ---------------------------------------------------------------

/// @dev The full `Multisig.createProposal` calldata (dryrun and direct-submission
/// fallback; the DAO UI assembles this call itself).
function buildCreateProposalCalldata(
string memory _metadataURI,
bool _approveProposal
)
internal
view
returns (bytes memory)
{
return abi.encodeWithSignature(
"createProposal(bytes,(address,uint256,bytes)[],address,bool)",
bytes(_metadataURI),
buildDaoActions(),
L1.DAO_OPTIMISTIC_TOKEN_VOTING_PLUGIN,
_approveProposal
);
}

function logIfReverted(bool _ok, bytes memory _returnData) internal pure {
if (!_ok) {
console2.log("revert data:");
console2.logBytes(_returnData);
}
}

function check(bool _ok, string memory _what) internal pure {
if (!_ok) revert CheckFailed(_what);
}

function readUint(address _target, string memory _sig) internal view returns (uint256) {
return abi.decode(readRaw(_target, abi.encodeWithSignature(_sig)), (uint256));
}

function readBool(
address _target,
string memory _sig,
address _arg
)
internal
view
returns (bool)
{
return abi.decode(readRaw(_target, abi.encodeWithSignature(_sig, _arg)), (bool));
}

function readAddr(
address _target,
string memory _sig,
address _arg
)
internal
view
returns (address)
{
return abi.decode(readRaw(_target, abi.encodeWithSignature(_sig, _arg)), (address));
}

/// @dev SignerList settings: (encryptionRegistry, minSignerListLength).
function readSignerListSettings() internal view returns (address, uint16) {
return abi.decode(
readRaw(L1.DAO_SIGNER_LIST, abi.encodeWithSignature("settings()")), (address, uint16)
);
}

/// @dev Standard Multisig settings:
/// (onlyListed, minApprovals, destinationProposalDuration, signerList, expirationPeriod).
function readStandardMultisigSettings()
internal
view
returns (bool, uint16, uint32, address, uint32)
{
return abi.decode(
readRaw(L1.DAO_STANDARD_MULTISIG, abi.encodeWithSignature("multisigSettings()")),
(bool, uint16, uint32, address, uint32)
);
}

/// @dev Emergency Multisig settings: (onlyListed, minApprovals, signerList, expirationPeriod).
function readEmergencyMultisigSettings() internal view returns (bool, uint16, address, uint32) {
return abi.decode(
readRaw(L1.DAO_EMERGENCY_MULTISIG, abi.encodeWithSignature("multisigSettings()")),
(bool, uint16, address, uint32)
);
}

function nameOf(address _target) internal pure returns (string memory) {
if (_target == L1.DAO_SIGNER_LIST) return "SignerList";
if (_target == L1.DAO_STANDARD_MULTISIG) return "Standard Multisig";
if (_target == L1.DAO_EMERGENCY_MULTISIG) return "Emergency Multisig";
if (_target == L1.DAO_ENCRYPTION_REGISTRY) return "EncryptionRegistry";
if (_target == L1.DAO) return "DAO";
return "?";
}

function readRaw(address _target, bytes memory _data) private view returns (bytes memory) {
(bool ok, bytes memory ret) = _target.staticcall(_data);
check(ok, "staticcall reverted");
return ret;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Proposal0020

Created via the Taiko DAO UI on the Standard Multisig `0xD7dA1C25E915438720692bC55eb3a7170cA90321`.
The UI pins the metadata and assembles `createProposal`; paste each action below into the
UI's custom-action (calldata) form, in order. After creation, run the `verify` mode
Comment thread
davidtaikocha marked this conversation as resolved.
Outdated
against the new proposal id before approving.

- Destination plugin (set by the UI): `0x989E348275b659d36f8751ea1c10D146211650BE`

## Action 1
- To: `0x0F95E6968EC1B28c794CF1aD99609431de5179c2` (SignerList)
- Value: `0`
- Data: `0xed19b9240000000000000000000000002efdb93a3b87b930e553d504db67ee41c69c42d10000000000000000000000000000000000000000000000000000000000000004`

## Action 2
- To: `0x0F95E6968EC1B28c794CF1aD99609431de5179c2` (SignerList)
- Value: `0`
- Data: `0xe8906a2d00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000ac5898b0fffd23f4ef09f0e50fa1bc4896ef7163`

## Action 3
- To: `0x0F95E6968EC1B28c794CF1aD99609431de5179c2` (SignerList)
- Value: `0`
- Data: `0x8d361e4300000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000005000000000000000000000000436a1075099a145417ebfc74bbac9605e3e4f1a70000000000000000000000000f40268ec0dc8d88cf2f22e227a29a0b478b635100000000000000000000000025d3e89bace2040ed3af7c4c7b505cfbb72fd6f1000000000000000000000000a384e224a3f3d664f43ebe33395ef0dcce67e8940000000000000000000000006268d189e011aa53a2f09a1fe159445beb3d878e`

## Action 4
- To: `0xD7dA1C25E915438720692bC55eb3a7170cA90321` (Standard Multisig)
- Value: `0`
- Data: `0xcf94a86d0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000d2f000000000000000000000000000f95e6968ec1b28c794cf1ad99609431de5179c20000000000000000000000000000000000000000000000000000000000127500`

## Action 5
- To: `0x2AffADEb2ef5e1F2a7F58964ee191F1e88317ECd` (Emergency Multisig)
- Value: `0`
- Data: `0x7dacfc00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000040000000000000000000000000f95e6968ec1b28c794cf1ad99609431de5179c20000000000000000000000000000000000000000000000000000000000127500`

## Action 6
- To: `0x2eFDb93a3B87b930E553d504db67Ee41c69C42d1` (EncryptionRegistry)
- Value: `0`
- Data: `0x3348803f`
Loading
Loading