|
| 1 | +# Governance Proposal Builder |
| 2 | + |
| 3 | +Helpers to build, validate, and fill DAO Governance proposals. |
| 4 | + |
| 5 | +## Prerequisites |
| 6 | + |
| 7 | +- Node.js v18+ |
| 8 | +- npm |
| 9 | + |
| 10 | +## Installation |
| 11 | + |
| 12 | +``` |
| 13 | +npm install |
| 14 | +``` |
| 15 | + |
| 16 | +## Configuration |
| 17 | + |
| 18 | +Create a `.env` file based on `.env.example`: |
| 19 | +``` |
| 20 | +cp .env.example .env |
| 21 | +``` |
| 22 | + |
| 23 | +## Available scripts |
| 24 | + |
| 25 | +Currently availabe scripts are: |
| 26 | +``` |
| 27 | +[*] fill-options-gateway-proposal |
| 28 | +[*] decode-options-gateway-proposal |
| 29 | +``` |
| 30 | + |
| 31 | +### fillOptionsGatewayProposal |
| 32 | + |
| 33 | +#### Workflow |
| 34 | + |
| 35 | +1. Edit `gateway-proposal-temp.json` (see one of the |
| 36 | + `gateway-proposal-temp.<network>-example.json` files as a starting point) to |
| 37 | + describe the proposal you want to send. Leave `arguments.options` set to |
| 38 | + `"0x"` — that field is what this script fills in. |
| 39 | +2. Run the fill script for the target network: |
| 40 | + |
| 41 | + ```bash |
| 42 | + npm run fill-options-gateway-proposal:mainnet |
| 43 | + # or |
| 44 | + npm run fill-options-gateway-proposal:testnet |
| 45 | + ``` |
| 46 | + |
| 47 | +3. The script writes two files in current directory: |
| 48 | + - `gateway-proposal-filled.json` — the validated proposal mirroring the |
| 49 | + input shape, with `arguments.options` filled in. Useful as a |
| 50 | + human-readable record of what was generated. |
| 51 | + - `aragonProposal.json` — the same call rendered as a single Aragon |
| 52 | + transaction (`[{ to, value, data }]`) where `data` is the ABI-encoded |
| 53 | + `sendRemoteProposal(...)` calldata. **This is the file to upload via the |
| 54 | + Aragon DAO front-end** when creating the proposal that calls |
| 55 | + `sendRemoteProposal` on the `GovernanceOAppSender` contract. |
| 56 | + |
| 57 | +#### What the script checks |
| 58 | + |
| 59 | +Before computing options, the script enforces that the input matches the |
| 60 | +canonical structure of `gateway-proposal-temp.json`: |
| 61 | + |
| 62 | +- Top-level keys are exactly `to`, `method`, `arguments`. |
| 63 | +- `arguments` keys are exactly `targets`, `values`, `functionSignatures`, |
| 64 | + `datas`, `operations`, `options`. |
| 65 | +- `targets`, `values`, `functionSignatures`, `datas`, `operations` are arrays |
| 66 | + of strings of identical length. |
| 67 | +- `options` is a string equal to `"0x"` : empty placeholder — **the whole point |
| 68 | + of the script is to fill it**. |
| 69 | +- `method` is exactly `"sendRemoteProposal"`. |
| 70 | +- `to` matches the canonical `GovernanceOAppSender` for the chosen network: |
| 71 | + - mainnet: `0x1c5D750D18917064915901048cdFb2dB815e0910` |
| 72 | + - testnet: `0x909692c2f4979ca3fa11B5859d499308A1ec4932` |
| 73 | +- currently it only allows `values` and `operations` to be arrays of `"0"`s. |
| 74 | +- each `targets[i]` has non-empty bytecode on the Gateway chain. |
| 75 | + |
| 76 | +#### How `options` is computed |
| 77 | + |
| 78 | +The script uses `@layerzerolabs/lz-v2-utilities` to build a LayerZero |
| 79 | +option containing a single executor `lzReceive` action. To do this it forks the Gateway chain and impersonates the `SafeProxy` account to estimate the gas needed, and adds some constant and proportional buffers. |
| 80 | + |
| 81 | +#### Output |
| 82 | + |
| 83 | +The script writes two files next to the input: |
| 84 | + |
| 85 | +``` |
| 86 | +./gateway-proposal-filled.json |
| 87 | +./aragonProposal.json |
| 88 | +``` |
| 89 | + |
| 90 | +It **refuses to overwrite** either file if it already exists, and exits |
| 91 | +with a non-zero status before writing anything. Delete the file(s) first if |
| 92 | +you want to regenerate. |
| 93 | + |
| 94 | +#### RECOMMENDED MANUAL STEP: Decoding individual `datas` entries |
| 95 | + |
| 96 | +Independently from this script, when doing a cross-chain proposal, it is highly recommended to always sanity-check what each `arguments.datas[i]` actually calls (using the matching |
| 97 | +`arguments.functionSignatures[i]`) — note that usually datas are encoded **without** |
| 98 | +the 4-byte selector, so use `cast abi-decode` and treat the bytes as the |
| 99 | +"return-value" tuple, for example: |
| 100 | + |
| 101 | +```bash |
| 102 | +DATA=0x00000000000000000000000012345678901234567890123456789012345678900000000000000000000000000000000000000000000000000000000000000002 |
| 103 | + |
| 104 | +cast abi-decode 'f()(address,uint256)' "$DATA" |
| 105 | +# 0x1234567890123456789012345678901234567890 |
| 106 | +# 2 |
| 107 | +``` |
| 108 | + |
| 109 | +The `f()` is just a placeholder; only the parameter-types tuple matters. Pass |
| 110 | +the same types as the matching `functionSignatures[i]`. |
| 111 | + |
| 112 | +### decodeOptionsGatewayProposal |
| 113 | + |
| 114 | +Reverse of the `computeLZOptions` step inside `fillOptionsGatewayProposal`: |
| 115 | +takes a LayerZero options hex string |
| 116 | +and prints the decoded `gasLimit` and `nativeValue`. Useful to sanity-check |
| 117 | +what's in `arguments.options` of a Gateway proposal and to use before voting on a pending DAO Gateway proposal. |
| 118 | + |
| 119 | +#### Usage |
| 120 | + |
| 121 | +```bash |
| 122 | +npm run decode-options-gateway-proposal -- --options 0x000301001101000000000000000000000000000493e0 |
| 123 | +``` |
| 124 | + |
| 125 | +The leading `--` is required so npm forwards the flag to the script instead |
| 126 | +of consuming it itself. `--options` is the only flag and is required. |
| 127 | + |
| 128 | +#### Output |
| 129 | + |
| 130 | +Prints (to stdout) the raw hex and the decoded fields: |
| 131 | + |
| 132 | +``` |
| 133 | +Options hex: 0x000301001101000000000000000000000000000493e0 |
| 134 | +gasLimit: 300000 |
| 135 | +nativeValue: 0 |
| 136 | +``` |
0 commit comments