├── src ├── tasks │ ├── index.ts │ └── simulate-proposal.ts ├── config.ts └── utils.ts ├── test ├── abi │ ├── index.ts │ └── nouns-dao-executor.abi.ts ├── proposal-81.test.ts ├── proposal-82.test.ts └── proposal-37.test.ts ├── .gitignore ├── .env.sample ├── .prettierrc ├── reports ├── templates │ └── proposal-template.md ├── proposal-38.md ├── proposal-82.md ├── proposal-81.md ├── proposal-59.md ├── proposal-37.md ├── proposal-58.md └── proposal-50.md ├── hardhat.config.ts ├── package.json └── README.md /src/tasks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './simulate-proposal'; 2 | -------------------------------------------------------------------------------- /test/abi/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nouns-dao-executor.abi'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | cache 3 | .env 4 | tenderly.yaml 5 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | ETHEREUM_HTTP_URL= 2 | TENDERLY_PROJECT= 3 | TENDERLY_USERNAME= 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "arrowParens": "avoid", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /reports/templates/proposal-template.md: -------------------------------------------------------------------------------- 1 | # Proposal [ID]: [NAME] 2 | 3 | ## Overview 4 | 5 | 6 | 7 | ## Proposal Description Accuracy 8 | 9 | 10 | 11 | ## Execution Risks 12 | 13 | 14 | 15 | ## Audit Required 16 | 17 | 18 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { ETHEREUM_HTTP_URL, TENDERLY_PROJECT, TENDERLY_USERNAME } from './src/config'; 2 | import { HardhatUserConfig } from 'hardhat/config'; 3 | import '@tenderly/hardhat-tenderly'; 4 | import '@nomiclabs/hardhat-ethers'; 5 | import './src/tasks'; 6 | 7 | const config: HardhatUserConfig = { 8 | networks: { 9 | mainnet: { 10 | url: ETHEREUM_HTTP_URL, 11 | }, 12 | }, 13 | tenderly: { 14 | project: TENDERLY_PROJECT, 15 | username: TENDERLY_USERNAME, 16 | }, 17 | }; 18 | export default config; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nouns-diligence", 3 | "version": "0.1.0", 4 | "description": "Nouns On-Chain Proposal Simulation and Analysis", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "task:simulate-proposal": "npx hardhat simulate-proposal", 8 | "test:proposal": "sh -c 'npx hardhat test test/proposal-${0}.test.ts'", 9 | "format": "prettier --write '{src,test}/**/*.ts'" 10 | }, 11 | "license": "MIT", 12 | "devDependencies": { 13 | "@nomiclabs/hardhat-ethers": "^2.0.4", 14 | "@nouns/sdk": "^0.4.0", 15 | "@tenderly/hardhat-tenderly": "1.0.13", 16 | "@types/chai": "^4.3.0", 17 | "@types/mocha": "^9.1.0", 18 | "chai": "^4.3.6", 19 | "dotenv": "^14.3.2", 20 | "ethereum-waffle": "^3.4.0", 21 | "ethers": "^5.6.8", 22 | "hardhat": "2.9.0", 23 | "mocha": "^9.2.0", 24 | "prettier": "^2.5.1", 25 | "ts-node": "^10.4.0", 26 | "typescript": "^4.5.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | dotenv.config(); 4 | 5 | // Node Fields 6 | 7 | // Local fork node hostname 8 | export const NODE_HOSTNAME = '127.0.0.1'; 9 | 10 | // Local fork node port 11 | export const NODE_PORT = 8545; 12 | 13 | // Proposal Fields 14 | 15 | // 14 days in seconds 16 | export const GRACE_PERIOD = 60 * 60 * 24 * 14; 17 | 18 | // Voting period in blocks 19 | export const VOTING_PERIOD = 19710; 20 | 21 | // The index of `forVotes` in the Proposal struct 22 | export const FOR_VOTE_MEMBER_INDEX = 11; 23 | 24 | // The number of simulated for votes 25 | export const SIMULATED_VOTE_COUNT = 1_000; 26 | 27 | // The Ethereum RPC URL (Requires archive node for older proposals) 28 | export const ETHEREUM_HTTP_URL = process.env.ETHEREUM_HTTP_URL; 29 | 30 | // The Tenderly project name 31 | export const TENDERLY_PROJECT = process.env.TENDERLY_PROJECT!; 32 | 33 | // The Tenderly username 34 | export const TENDERLY_USERNAME = process.env.TENDERLY_USERNAME!; 35 | 36 | // Hardhat private key 37 | // prettier-ignore 38 | export const LOCAL_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'; 39 | -------------------------------------------------------------------------------- /reports/proposal-38.md: -------------------------------------------------------------------------------- 1 | # Proposal 38: Proposal to Re-Elect Nouns DAO Core Contributor 2 | 3 | ## Proposal Description Accuracy 4 | 5 | **The proposal description is as follows:** 6 | 7 | > The transactions embedded in this proposal will create a Sablier streaming payment to the contributor's address for 62.52414 ETH over 6 months (10.42069 ETH per month) from February 25 2022 to August 25 2022. 8 | 9 | **Is this proposal description accurate?** 10 | 11 | Yes, this proposal creates a streaming payment to `0x2548e6D6F1f0cd0d7f41355ee38066edE6A7eE0a` from February 25, 2022 2:02:45 AM UTC to August 25, 2022 12:00:00 AM UTC for an amount of 62.52414 WETH. 12 | 13 | ## Execution Risks 14 | 15 | ### Critical 16 | 17 | None. 18 | 19 | ### Major 20 | 21 | None. 22 | 23 | ### Minor 24 | 25 | - This proposal MUST be executed prior to February 25, 2022 2:02:45 AM UTC (Thursday, February 24, 2022 9:02:45 PM EST). Execution will revert if attempted after this date time. 26 | - Sablier smart contract risk, siloed to the 62.52414 WETH deposited to Sablier in this proposal. 27 | 28 | ## Audit Required 29 | 30 | **No**. This proposal does not introduce any new contracts. 31 | -------------------------------------------------------------------------------- /reports/proposal-82.md: -------------------------------------------------------------------------------- 1 | # Proposal 82: Sell Noun #253 to Lil Nouns DAO 2 | 3 | ## Overview 4 | 5 | Proposal 82 creates an on-chain listing of Noun 253, which is only fillable by the Lil Nouns treasury, for 69.42 ETH. 6 | 7 | The on-chain listing of Noun 253 involves 3 function calls: 8 | 9 | 1. `setApprovalForModule` - Allow the Zora 'Asks Private ETH' module to call the ERC721 transfer helper. 10 | 2. `approve` - Allow the ERC721 transfer helper to transfer Noun 253 from the Nouns DAO treasury. 11 | 3. `createAsk` - Create the on-chain, private listing of Noun 253 for 69.42 ETH. 12 | 13 | ## Proposal Description Accuracy 14 | 15 | The proposal description is accurate and does not attempt to deceive the DAO. 16 | 17 | ## Execution Risks 18 | 19 | ### Critical 20 | 21 | None. 22 | 23 | ### Major 24 | 25 | None. 26 | 27 | ### Minor 28 | 29 | This proposal shares the same trust assumptions and risks as [proposal 81](./proposal-81.md), adjusted for an ask amount of 69.42 ETH: 30 | 31 | - The 'Asks Private ETH' [module owner multisig](https://etherscan.io/address/0xd1d1D4e36117aB794ec5d4c78cBD3a8904E691D0) has the ability to set the module fee to 100%, taking all of the ETH provided by the Lil Nouns treasury in exchange for Noun 253. Zora would lose far more in reputational damage, so this would likely only occur if two or more of the multisig signers were compromised. 32 | - General smart contract risk, siloed to Noun 253. 33 | - No expiry of the on-chain, private offer. 34 | 35 | ## Audit Required 36 | 37 | **No** 38 | 39 | 40 | ## Validation 41 | 42 | Validation of proposal 82 can be found [here](../test/proposal-82.test.ts). 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nouns Diligence 2 | 3 | Nouns On-Chain Proposal Simulation and Analysis 4 | 5 | ## For Voters 6 | 7 | Technical reports for all reviewed proposals can be found in the [reports](./reports) folder. 8 | 9 | ## For Reviewers 10 | 11 | Simulation is a useful tool for inspecting execution of complex on-chain proposals, but it's not the only tool that should be used. Consult the report template for additional review steps. 12 | 13 | ### Setup 14 | 15 | 1. Install the [Tenderly CLI](https://github.com/tenderly/tenderly-cli#installation) 16 | 2. Login to the Tenderly CLI using `tenderly login` 17 | 3. Configure Tenderly exporting using `tenderly export init` 18 | 4. Copy the sample environment file to `.env` and populate 19 | 20 | ### Proposal Simulation & Analysis 21 | 22 | 1. Simulate proposal execution 23 | ``` 24 | yarn task:simulate-proposal --id [proposal_id] 25 | ``` 26 | 2. Export the execution transaction for inspection 27 | ``` 28 | tenderly export [transaction_id] 29 | ``` 30 | 31 | ### Simple Validation (Test Coverage) 32 | 33 | Proposal test coverage is useful when the proposal is of low to moderate complexity. Test coverage is not exhaustive. 34 | 35 | **Running Tests** 36 | 37 | Prior to running a proposal test suite, simulate the proposal using the the above command and leave the node running. 38 | 39 | Once simulation completes, use the following command to run the proposal test suite: 40 | 41 | ``` 42 | yarn test:proposal [id] 43 | ``` 44 | 45 | 46 | **Writing Tests** 47 | 48 | Use `test/proposal-37.test.ts` as an example. 49 | 50 | Note that the file name must be of format: `proposal-[id].test.ts`. 51 | -------------------------------------------------------------------------------- /reports/proposal-81.md: -------------------------------------------------------------------------------- 1 | # Proposal 81: Allocate Noun to Lil Nouns DAO - "treasure quest" edition 2 | 3 | ## Overview 4 | 5 | Proposal 81 creates an on-chain listing of Noun 253, which is only fillable by the Lil Nouns treasury, for 4.2069 ETH. 6 | 7 | The on-chain listing of Noun 253 involves 3 function calls: 8 | 9 | 1. `setApprovalForModule` - Allow the Zora 'Asks Private ETH' module to call the ERC721 transfer helper. 10 | 2. `approve` - Allow the ERC721 transfer helper to transfer Noun 253 from the Nouns DAO treasury. 11 | 3. `createAsk` - Create the on-chain, private listing of Noun 253 for 4.2069 ETH. 12 | 13 | ## Proposal Description Accuracy 14 | 15 | The proposal description is accurate and does not attempt to deceive the DAO. 16 | 17 | ## Execution Risks 18 | 19 | ### Critical 20 | 21 | None. 22 | 23 | ### Major 24 | 25 | - There is a conditional risk, dependent on the outcome of [proposal 82](./proposal-82.md). If proposal 82 succeeds and is executed after this proposal, it will overwrite the on-chain listing included in this proposal. 26 | 27 | ### Minor 28 | 29 | - The 'Asks Private ETH' [module owner multisig](https://etherscan.io/address/0xd1d1D4e36117aB794ec5d4c78cBD3a8904E691D0) has the ability to set the module fee to 100%, taking all of the ETH provided by the Lil Nouns treasury in exchange for Noun 253. Zora would lose far more in reputational damage, so this would likely only occur if two or more of the multisig signers were compromised. 30 | - General smart contract risk, siloed to Noun 253. 31 | - No expiry of the on-chain, private offer. 32 | 33 | ## Audit Required 34 | 35 | **No** 36 | 37 | 38 | ## Validation 39 | 40 | Validation of proposal 81 can be found [here](../test/proposal-81.test.ts). 41 | -------------------------------------------------------------------------------- /reports/proposal-59.md: -------------------------------------------------------------------------------- 1 | # Proposal 59: Gamify fundraising for Ukraine by donating 51 ETH through the Rescue Toadz project 2 | 3 | ## Overview 4 | 5 | Proposal 59 donates 51 ETH to [Unchain Ukraine](https://unchain.fund/) through the Rescue Toadz project. 6 | 7 | Rescue Toadz gamifies the donation process, enabling users to 'capture' minted Toadz by matching or surpassing the previous donation. 8 | 9 | The transaction included in this proposal attempts to 'capture' 7 Toadz, including one donation of 40 ETH, one donation of 10 ETH, and 5 donations of 0.2 ETH. 10 | 11 | **This proposal is a re-submission of [#50](./proposal-50.md) and includes fixes for all critical issues.** 12 | 13 | ## Proposal Description Accuracy 14 | 15 | The proposal description is accurate and does not attempt to deceive the DAO. The charity address is correct and cannot be updated. 16 | 17 | **Notes:** 18 | 19 | - It's possible that the DAO does not donate the full 51 ETH. ETH will be returned to the DAO if the amount being donated by capturing a Toad has been exceeded by another party. 20 | - The Nouns DAO executor does not call the RescueToadz contract directly. Instead, it calls [RescueToadzNounsExecutor](https://etherscan.io/address/0xe4380808F44ceced3aCDf7e85547c29F5cB69674), which acts as a pass-through contract to resolve the technical issues found in proposal 50. Received POAPs will be held in this pass-through contract and can be removed via a subsequent proposal if ever desired. 21 | 22 | ## Execution Risks 23 | 24 | ### Critical 25 | 26 | None. 27 | 28 | ### Major 29 | 30 | None. 31 | 32 | ### Minor 33 | 34 | - There is one possible, yet unlikely, revert condition. Details of this condition will be released at a later time. 35 | - Minor smart contract risk, siloed to the 51 ETH sent in the proposal. 36 | 37 | ## Audit Required 38 | 39 | **No** 40 | 41 | 42 | ## Validation 43 | 44 | Follow the [steps in the README](../README.md#for-reviewers) to simulate proposal 59. 45 | -------------------------------------------------------------------------------- /reports/proposal-37.md: -------------------------------------------------------------------------------- 1 | # Proposal 37: setProposalThresholdBPS(25) 2 | 3 | ## Overview 4 | 5 | Proposal 37 lowers the proposal threshold BPS (basis points) storage value in the Nouns DAO contract from 50 to 25. 6 | 7 | **Note**: One basis point is equal to 0.01%. 8 | 9 | If executed, 0.25% of the total Noun supply will be required to submit a proposal to the DAO. 10 | 11 | In practice, this change extends the ability to submit a proposal using a single vote until, and including, Noun 398 (supply of 399). As noted in the proposal description, two votes will be required to submit a proposal once Noun 199 is minted if no action is taken. 12 | 13 | Upon execution, you may unexpectedly notice that the proposal threshold returns a value of 0, rather than 1. This is because a proposer must have [more](https://github.com/nounsDAO/nouns-monorepo/blob/ca4dbe199e835706636776ef201ffbaecfde8774/packages/nouns-contracts/contracts/governance/NounsDAOLogicV1.sol#L188) votes than the proposal threshold. 14 | 15 | ## Proposal Description Accuracy 16 | 17 | The proposal description is as follows: 18 | 19 | > This change would enable Noun owners or delegates to submit Nouns DAO proposals using a single vote until, and including, Noun 399. Without this change, proposals created at Noun 200 and on will require 2 votes. 20 | 21 | While this proposal description is accurate, there's a small clarification to be made: 22 | 23 | The Noun supply is off by 1 compared to Noun IDs. This change would allow single vote holders to submit a proposal prior to the existence of the Noun with ID **399**, when the total Noun supply is 400. Without this change, proposals created following the minting of Noun **199** will require 2 votes, when the total Noun supply is **200**. 24 | 25 | ## Execution Risks 26 | 27 | ### Critical 28 | 29 | None. 30 | 31 | ### Major 32 | 33 | None. 34 | 35 | ### Minor 36 | 37 | Execution of this proposal could result in a greater number of low quality proposals as only one vote is required to put a proposal on-chain. It's important to note that this is an extension of the existing behavior. 38 | 39 | ## Audit Required 40 | 41 | **No**. This change is a configuration value update and does not introduce any new contracts. 42 | 43 | 44 | ## Validation 45 | 46 | Validation of proposal 37 can be found [here](../test/proposal-37.test.ts). 47 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber as EthersBN, Contract, providers, utils } from 'ethers'; 2 | import { solidityKeccak256 } from 'ethers/lib/utils'; 3 | import { FOR_VOTE_MEMBER_INDEX } from './config'; 4 | 5 | /** 6 | * Get the `forVotes` storage key for the passed proposal id 7 | * @param proposalId The proposal id 8 | */ 9 | export const getProposalForVoteStorageKey = (proposalId: number) => { 10 | const hash = solidityKeccak256(['uint256', 'uint256'], [proposalId, FOR_VOTE_MEMBER_INDEX]); 11 | return utils.hexStripZeros(EthersBN.from(hash).add(FOR_VOTE_MEMBER_INDEX).toHexString()); 12 | }; 13 | 14 | /** 15 | * Mint until the passed noun id is reached 16 | * @param nounsToken The Nouns token contract 17 | * @param targetNounId The target noun id 18 | * @param provider The Hardhat provider 19 | * @param minter The address that has the minter role 20 | */ 21 | export const mintTo = async ( 22 | nounsToken: Contract, 23 | targetNounId: number, 24 | provider: providers.JsonRpcProvider, 25 | minter: string, 26 | ) => { 27 | await provider.send('hardhat_impersonateAccount', [minter]); 28 | 29 | const signer = provider.getSigner(minter); 30 | const contract = nounsToken.connect(signer); 31 | 32 | while ((await contract.totalSupply()) < targetNounId) { 33 | await contract.mint(); 34 | } 35 | }; 36 | 37 | /** 38 | * Mine the Hardhat network to `targetBlock` 39 | * Multi-block mining has not been implemented yet: 40 | * https://github.com/nomiclabs/hardhat/issues/1112 41 | * @param targetBlock The target block number 42 | * @param provider The Hardhat provider 43 | */ 44 | export const mineTo = async (targetBlock: number, provider: providers.JsonRpcProvider) => { 45 | const latestBlock = await provider.getBlock('latest'); 46 | const blocksToMine = targetBlock - latestBlock.number; 47 | await provider.send('hardhat_mine', [utils.hexStripZeros(utils.hexlify(blocksToMine))]); 48 | }; 49 | 50 | /** 51 | * Delay for `seconds` 52 | * @param seconds The number of seconds to delay 53 | */ 54 | export const delay = (seconds: number) => { 55 | return new Promise(resolve => setTimeout(resolve, seconds * 1000)); 56 | }; 57 | 58 | /** 59 | * Wait for any keypress 60 | */ 61 | export const keypress = async () => { 62 | process.stdin.setRawMode(true); 63 | return new Promise(resolve => 64 | process.stdin.once('data', () => { 65 | process.stdin.setRawMode(false); 66 | resolve(true); 67 | }), 68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /src/tasks/simulate-proposal.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ETHEREUM_HTTP_URL, 3 | LOCAL_PRIVATE_KEY, 4 | NODE_HOSTNAME, 5 | NODE_PORT, 6 | SIMULATED_VOTE_COUNT, 7 | } from '../config'; 8 | import { delay, getProposalForVoteStorageKey, keypress, mineTo } from '../utils'; 9 | import { ChainId, getContractsForChainOrThrow } from '@nouns/sdk'; 10 | import { BigNumber as EthersBN, providers, utils, Wallet } from 'ethers'; 11 | import { TASK_NODE } from 'hardhat/builtin-tasks/task-names'; 12 | import { task, types } from 'hardhat/config'; 13 | 14 | task('simulate-proposal', 'Simulate a Nouns governance proposal') 15 | .addParam('id', 'The Nouns proposal ID', undefined, types.int) 16 | .setAction(async ({ id }, { run }) => { 17 | const provider = new providers.JsonRpcProvider(ETHEREUM_HTTP_URL); 18 | 19 | let { nounsDaoContract: dao } = getContractsForChainOrThrow( 20 | ChainId.Mainnet, 21 | new Wallet(LOCAL_PRIVATE_KEY, provider), 22 | ); 23 | 24 | const proposal = await dao.proposals(id); 25 | const latestBlock = await provider.getBlock('latest'); 26 | 27 | const isPendingOrActive = proposal.endBlock.gt(latestBlock.number); 28 | const endBlock = proposal.endBlock.toNumber(); 29 | 30 | // Start the fork node 31 | await Promise.race([ 32 | run(TASK_NODE, { 33 | port: NODE_PORT, 34 | hostname: NODE_HOSTNAME, 35 | fork: provider.connection.url, 36 | ...(!isPendingOrActive ? { forkBlockNumber: endBlock } : {}), 37 | }), 38 | delay(2), 39 | ]); 40 | 41 | // Connect to the fork node, mining until the proposal start block if necessary 42 | const fork = new providers.JsonRpcProvider(`http://${NODE_HOSTNAME}:${NODE_PORT}/`); 43 | if (isPendingOrActive) { 44 | await mineTo(endBlock, fork); 45 | } 46 | 47 | // Attach the DAO contract to the fork 48 | dao = dao.connect(dao.signer.connect(fork)); 49 | 50 | // Simulate `SIMULATED_VOTE_COUNT` for votes 51 | await fork.send('hardhat_setStorageAt', [ 52 | dao.address, 53 | getProposalForVoteStorageKey(id), 54 | utils.hexZeroPad(EthersBN.from(SIMULATED_VOTE_COUNT).toHexString(), 32), 55 | ]); 56 | 57 | // Queue the proposal 58 | await dao.queue(id); 59 | 60 | // Fast-forward & execute proposal 61 | const { eta } = await dao.proposals(id); 62 | 63 | await fork.send('evm_setNextBlockTimestamp', [eta.toNumber()]); 64 | 65 | const { hash } = await dao.execute(id); 66 | 67 | console.log(`Execution Transaction Hash: ${hash}`); 68 | console.log('Export this hash to Tenderly and press any key to exit.'); 69 | 70 | await keypress(); 71 | }); 72 | -------------------------------------------------------------------------------- /reports/proposal-58.md: -------------------------------------------------------------------------------- 1 | # Proposal 58: Increase Voting Delay and Voting Period 2 | 3 | ## Overview 4 | 5 | Proposal 58 increases the voting delay to 4 days and the voting period to 5 days. These values are currently set to roughly 2 days and 3 days, respectively. 6 | 7 | **Refresher:** 8 | 9 | - **Voting Delay** - The number of Ethereum blocks to wait before voting on a proposal may begin. This value is added to the current block number when a proposal is created. 10 | - **Voting Period** - The duration of voting on a proposal, in Ethereum blocks. 11 | 12 | **This proposal assumes 13 second blocks when doing the conversion to time.** 13 | 14 | ## Proposal Description Accuracy 15 | 16 | This proposal is accurate. The average Ethereum block time is [very close to 13 seconds](https://ycharts.com/indicators/ethereum_average_block_time) and the calculations match the time values described in the proposal: 17 | 18 | - **Voting Delay** - `(26585 * (13 / 60)) / (60 * 24) = 4.00005787037037 days` 19 | - **Voting Period** - `(33230 * (13 / 60)) / (60 * 24) = 4.99988425925926 days` 20 | 21 | ## Execution Risks 22 | 23 | ### Critical 24 | 25 | None. 26 | 27 | ### Major 28 | 29 | To normalize both quorum votes and the proposal threshold, votes are considered from the proposal creation block. This logic can be found in the [NounsDAOLogicV1](https://github.com/nounsDAO/nouns-monorepo/blob/master/packages/nouns-contracts/contracts/governance/NounsDAOLogicV1.sol#L508) contract. 30 | 31 | Note that `castVoteInternal` uses the current `votingDelay` to determine when the vote snapshot is taken, rather than a value that's cached at the time of proposal creation. 32 | 33 | This has a couple side effects: 34 | 35 | - Proposals that are **pending** at the time this proposal is executed will use a snapshot that's 13,445 blocks, or roughly 2.02 days, before the expected proposal creation snapshot. 36 | - Proposals that are **active** at the time this proposal is executed will begin to use the old snapshot. Unlike pending proposals, this could allow a voter to vote twice on the same proposal under the following circumstances: 37 | - The Noun owner or delegate already voted on the active proposal prior to the execution of this proposal AND they transferred or re-delegated their Nouns between the two snapshots. 38 | 39 | It is recommended that no changes to the `votingDelay` occur until the Nouns DAO logic contract is patched. This issue does NOT affect the `votingPeriod`, which can be updated prior to the patch. 40 | 41 | ### Minor 42 | 43 | None. 44 | 45 | ## Audit Required 46 | 47 | **No**. This change is a configuration value update and does not introduce any new contracts. 48 | -------------------------------------------------------------------------------- /test/proposal-81.test.ts: -------------------------------------------------------------------------------- 1 | import { solidity } from 'ethereum-waffle'; 2 | import { providers, utils, Signer, Contract } from 'ethers'; 3 | import { NounsDAOExecutorABI } from './abi'; 4 | import { NODE_HOSTNAME, NODE_PORT } from '../src/config'; 5 | import { ChainId, getContractsForChainOrThrow } from '@nouns/sdk'; 6 | import chai from 'chai'; 7 | 8 | chai.use(solidity); 9 | const { expect } = chai; 10 | 11 | /** 12 | * This suite validates that the on-chain private offer can be filled 13 | * by the Lil Nouns treasury. 14 | */ 15 | describe('Proposal 81', () => { 16 | const MOCK_ADMIN = '0x000000000000000000000000000000000000dEaD'; 17 | const NOUNS_TOKEN = '0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03'; 18 | const NOUNS_DAO_TREASURY = '0x0BC3807Ec262cB779b38D65b38158acC3bfedE10'; 19 | const LIL_NOUNS_DAO_TREASURY = '0xd5f279ff9EB21c6D40C8f345a66f2751C4eeA1fB'; 20 | const ASKS_PRIVATE_ETH_MODULE = '0xfbf87e6c2c242d0166E2Ddb60Db5A94cD4dAe00e'; 21 | 22 | const FILL_ASK_SIGNATURE = 'fillAsk(address,uint256)'; 23 | const OFFERED_NOUN_ID = 253; 24 | const PRICE = utils.parseEther('4.2069'); 25 | 26 | const provider = new providers.JsonRpcProvider(`http://${NODE_HOSTNAME}:${NODE_PORT}/`); 27 | 28 | let signer: Signer; 29 | let snapshotId: string; 30 | let lilNounsDAOTreasury: Contract; 31 | const { nounsTokenContract } = getContractsForChainOrThrow(ChainId.Mainnet, provider); 32 | 33 | before(async () => { 34 | await provider.send('hardhat_impersonateAccount', [MOCK_ADMIN]); 35 | 36 | signer = provider.getSigner(MOCK_ADMIN); 37 | lilNounsDAOTreasury = new Contract(LIL_NOUNS_DAO_TREASURY, NounsDAOExecutorABI, signer); 38 | 39 | // Overwrite the Lil Nouns treasury admin 40 | await provider.send('hardhat_setStorageAt', [ 41 | LIL_NOUNS_DAO_TREASURY, 42 | '0x0', // `admin` storage slot (0) 43 | utils.hexZeroPad(MOCK_ADMIN, 32), 44 | ]); 45 | }); 46 | 47 | beforeEach(async () => { 48 | snapshotId = await provider.send('evm_snapshot', []); 49 | }); 50 | 51 | afterEach(async () => { 52 | await provider.send('evm_revert', [snapshotId]); 53 | }); 54 | 55 | it('should allow the offer to be filled by the Lil Nouns DAO treasury', async () => { 56 | const { timestamp } = await provider.getBlock('latest'); 57 | const delay = 60 * 60 * 24 * 2; 58 | const buffer = 60 * 60; 59 | const eta = timestamp + delay + buffer; 60 | const args = [ 61 | ASKS_PRIVATE_ETH_MODULE, 62 | PRICE, 63 | FILL_ASK_SIGNATURE, 64 | utils.defaultAbiCoder.encode(['address', 'uint256'], [NOUNS_TOKEN, OFFERED_NOUN_ID]), 65 | eta, 66 | ]; 67 | 68 | const queue = await lilNounsDAOTreasury.queueTransaction(...args); 69 | await queue.wait(); 70 | 71 | await provider.send('evm_increaseTime', [delay + buffer + 1]); 72 | 73 | const nounsDAOBalanceBefore = await provider.getBalance(NOUNS_DAO_TREASURY); 74 | 75 | const execution = await lilNounsDAOTreasury.executeTransaction(...args); 76 | await execution.wait(); 77 | 78 | const nounsDAOBalanceAfter = await provider.getBalance(NOUNS_DAO_TREASURY); 79 | const noun253Owner = await nounsTokenContract.ownerOf(OFFERED_NOUN_ID); 80 | 81 | expect(nounsDAOBalanceAfter.sub(nounsDAOBalanceBefore)).to.equal(PRICE); 82 | expect(noun253Owner).to.equal(LIL_NOUNS_DAO_TREASURY); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/proposal-82.test.ts: -------------------------------------------------------------------------------- 1 | import { solidity } from 'ethereum-waffle'; 2 | import { providers, utils, Signer, Contract } from 'ethers'; 3 | import { NounsDAOExecutorABI } from './abi'; 4 | import { NODE_HOSTNAME, NODE_PORT } from '../src/config'; 5 | import { ChainId, getContractsForChainOrThrow } from '@nouns/sdk'; 6 | import chai from 'chai'; 7 | 8 | chai.use(solidity); 9 | const { expect } = chai; 10 | 11 | /** 12 | * This suite validates that the on-chain private offer can be filled 13 | * by the Lil Nouns treasury. 14 | */ 15 | describe('Proposal 82', () => { 16 | const MOCK_ADMIN = '0x000000000000000000000000000000000000dEaD'; 17 | const NOUNS_TOKEN = '0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03'; 18 | const NOUNS_DAO_TREASURY = '0x0BC3807Ec262cB779b38D65b38158acC3bfedE10'; 19 | const LIL_NOUNS_DAO_TREASURY = '0xd5f279ff9EB21c6D40C8f345a66f2751C4eeA1fB'; 20 | const ASKS_PRIVATE_ETH_MODULE = '0xfbf87e6c2c242d0166E2Ddb60Db5A94cD4dAe00e'; 21 | 22 | const FILL_ASK_SIGNATURE = 'fillAsk(address,uint256)'; 23 | const OFFERED_NOUN_ID = 253; 24 | const PRICE = utils.parseEther('69.42'); 25 | 26 | const provider = new providers.JsonRpcProvider(`http://${NODE_HOSTNAME}:${NODE_PORT}/`); 27 | 28 | let signer: Signer; 29 | let snapshotId: string; 30 | let lilNounsDAOTreasury: Contract; 31 | const { nounsTokenContract } = getContractsForChainOrThrow(ChainId.Mainnet, provider); 32 | 33 | before(async () => { 34 | await provider.send('hardhat_impersonateAccount', [MOCK_ADMIN]); 35 | 36 | signer = provider.getSigner(MOCK_ADMIN); 37 | lilNounsDAOTreasury = new Contract(LIL_NOUNS_DAO_TREASURY, NounsDAOExecutorABI, signer); 38 | 39 | // Overwrite the Lil Nouns treasury admin 40 | await provider.send('hardhat_setStorageAt', [ 41 | LIL_NOUNS_DAO_TREASURY, 42 | '0x0', // `admin` storage slot (0) 43 | utils.hexZeroPad(MOCK_ADMIN, 32), 44 | ]); 45 | }); 46 | 47 | beforeEach(async () => { 48 | snapshotId = await provider.send('evm_snapshot', []); 49 | }); 50 | 51 | afterEach(async () => { 52 | await provider.send('evm_revert', [snapshotId]); 53 | }); 54 | 55 | it('should allow the offer to be filled by the Lil Nouns DAO treasury', async () => { 56 | const { timestamp } = await provider.getBlock('latest'); 57 | const delay = 60 * 60 * 24 * 2; 58 | const buffer = 60 * 60; 59 | const eta = timestamp + delay + buffer; 60 | const args = [ 61 | ASKS_PRIVATE_ETH_MODULE, 62 | PRICE, 63 | FILL_ASK_SIGNATURE, 64 | utils.defaultAbiCoder.encode(['address', 'uint256'], [NOUNS_TOKEN, OFFERED_NOUN_ID]), 65 | eta, 66 | ]; 67 | 68 | const queue = await lilNounsDAOTreasury.queueTransaction(...args); 69 | await queue.wait(); 70 | 71 | await provider.send('evm_increaseTime', [delay + buffer + 1]); 72 | 73 | const nounsDAOBalanceBefore = await provider.getBalance(NOUNS_DAO_TREASURY); 74 | 75 | const execution = await lilNounsDAOTreasury.executeTransaction(...args); 76 | await execution.wait(); 77 | 78 | const nounsDAOBalanceAfter = await provider.getBalance(NOUNS_DAO_TREASURY); 79 | const noun253Owner = await nounsTokenContract.ownerOf(OFFERED_NOUN_ID); 80 | 81 | expect(nounsDAOBalanceAfter.sub(nounsDAOBalanceBefore)).to.equal(PRICE); 82 | expect(noun253Owner).to.equal(LIL_NOUNS_DAO_TREASURY); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /reports/proposal-50.md: -------------------------------------------------------------------------------- 1 | # Proposal 50: Donate 51 ETH for humanitarian aid to Ukraine through the Rescue Toadz project 2 | 3 | ## Overview 4 | 5 | Proposal 50 attempts to donate 51 ETH to [Unchain Ukraine](https://unchain.fund/) through the Rescue Toadz project. 6 | 7 | Rescue Toadz gamifies the donation process, enabling users to 'capture' minted Toadz by matching or surpassing the previous donation. 8 | 9 | The transaction included in this proposal attempts to 'capture' 7 Toadz, including one donation of 40 ETH, one donation of 10 ETH, and 5 donations of 0.2 ETH. 10 | 11 | 12 | ## Proposal Description Accuracy 13 | 14 | The proposal description is accurate and does not attempt to deceive the DAO. The charity address is correct and cannot be updated. 15 | 16 | **Note:** While there is no attempt to deceive the DAO, the proposal transaction will fail due to a technical oversight. See [Execution Risks](#execution-risks) for more information. 17 | 18 | 19 | ## Execution Risks 20 | 21 | ### Critical 22 | 23 | The transaction included in the proposal will fail to execute. While it only takes one `REVERT` to stop execution and revert state changes, there are multiple possible execution failures. 24 | 25 | Included among these are one unavoidable revert that **will** cause the transaction to fail with 100% certainty AND two **possible** reverts. 26 | 27 | #### Unavoidable REVERTs 28 | 29 | 1. The [capture](https://github.com/haltakov/rescue-toadz/blob/d544264/contract/contracts/RescueToadz.sol#L95) function calls [_safeTransferFrom](https://github.com/haltakov/rescue-toadz/blob/d544264/contract/contracts/RescueToadz.sol#L110) to transfer the Rescue Toad to the DAO. This call will revert because the Nouns DAO executor does not expose an [onERC1155Received](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/742e85be7c08dff21410ba4aa9c60f6a033befb8/contracts/token/ERC1155/ERC1155.sol#L470) function, which is called in the [_doSafeTransferAcceptanceCheck](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/742e85be7c08dff21410ba4aa9c60f6a033befb8/contracts/token/ERC1155/ERC1155.sol#L186). This cannot be remedied prior to execution. 30 | 31 | #### Possible REVERTs 32 | 33 | 1. At time of writing, Rescue Toad #15 does not exist. This has the potential to cause the first revert, but can be remedied prior to execution by minting Toad #15. 34 | - [Failure Condition](https://github.com/haltakov/rescue-toadz/blob/d544264/contract/contracts/RescueToadz.sol#L100) 35 | 2. Anyone can cause the proposal transaction to fail by donating an amount greater than the DAO's donation to any of the 7 Rescue Toadz listed in the proposal. 36 | - [Failure Condition](https://github.com/haltakov/rescue-toadz/blob/d544264/contract/contracts/RescueToadz.sol#L101-L104) 37 | 38 | ### Minor 39 | 40 | The owner of the Rescue Toadz contract has the ability to cause the proposal transaction to fail by pausing the contract prior to execution. 41 | - [Failure Condition](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4a9cc8b4918ef3736229a5cc5a310bdc17bf759f/contracts/security/Pausable.sol#L52) 42 | 43 | ## Audit Required 44 | 45 | **No** 46 | 47 | 48 | ## Validation 49 | 50 | Follow the [steps in the README](../README.md#for-reviewers) to simulate proposal 50. 51 | 52 | **Note:** If you would like to test the unavoidable revert, you must mint Toad #15 on the fork (if not yet minted). To accomplish this, paste the following code after the fork is created in [simulate-proposal.ts](../src/tasks/simulate-proposal.ts#L46): 53 | 54 | ```ts 55 | const FORK_MINTER_ADDRESS = '0x000000000000000000000000000000000000dead'; 56 | 57 | // Impersonate an account with ETH & mint Toad #15 58 | await fork.send('hardhat_impersonateAccount', [FORK_MINTER_ADDRESS]); 59 | const minter = fork.getSigner(FORK_MINTER_ADDRESS); 60 | await minter.sendTransaction({ 61 | to: '0x57605D3A2C7726e9A7801307AF0C893bA5199F66', 62 | value: EthersBN.from(10).pow(16), 63 | data: '0xa0712d68000000000000000000000000000000000000000000000000000000000000000F', // 15 64 | }); 65 | await fork.send('hardhat_stopImpersonatingAccount', [FORK_MINTER_ADDRESS]); 66 | ``` 67 | -------------------------------------------------------------------------------- /test/proposal-37.test.ts: -------------------------------------------------------------------------------- 1 | import { deployMockContract, MockContract, solidity } from 'ethereum-waffle'; 2 | import { ChainId, getContractsForChainOrThrow, NounsTokenABI } from '@nouns/sdk'; 3 | import { providers, utils, BigNumber as EthersBN, Signer, constants } from 'ethers'; 4 | import { NODE_HOSTNAME, NODE_PORT } from '../src/config'; 5 | import chai from 'chai'; 6 | 7 | chai.use(solidity); 8 | const { expect } = chai; 9 | 10 | /** 11 | * This suite runs several tests to validate the following: 12 | * - Noun owners or delegates can propose with a single vote until a supply 13 | * of 399 Nouns is reached. We assume the supply at the time of execution 14 | * to be 204, but it may be higher depending on when execution occurs. 15 | * - Noun owners or delegates must have at least two votes to propose once 16 | * a supply of 400 Nouns is reached. 17 | * 18 | * Note: `NounsDAOLogicV1` validates that the proposer has MORE votes than 19 | * the proposal threshold: https://bit.ly/3ghElRt. 20 | */ 21 | describe('Proposal 37', () => { 22 | const MOCK_DEPLOYER = '0x000000000000000000000000000000000000dEaD'; 23 | 24 | const provider = new providers.JsonRpcProvider(`http://${NODE_HOSTNAME}:${NODE_PORT}/`); 25 | let { nounsDaoContract } = getContractsForChainOrThrow(ChainId.Mainnet, provider); 26 | 27 | let signer: Signer; 28 | let snapshotId: string; 29 | let mockNounsToken: MockContract; 30 | 31 | before(async () => { 32 | await provider.send('hardhat_impersonateAccount', [MOCK_DEPLOYER]); 33 | 34 | signer = provider.getSigner(MOCK_DEPLOYER); 35 | mockNounsToken = await deployMockContract(signer, NounsTokenABI); 36 | 37 | nounsDaoContract = nounsDaoContract.connect(signer); 38 | 39 | // Point the DAO to the Nouns contract stub 40 | await provider.send('hardhat_setStorageAt', [ 41 | nounsDaoContract.address, 42 | utils.hexStripZeros(EthersBN.from(10).toHexString()), // `nouns` storage slot (10) 43 | utils.hexZeroPad(mockNounsToken.address, 32), 44 | ]); 45 | }); 46 | 47 | beforeEach(async () => { 48 | snapshotId = await provider.send('evm_snapshot', []); 49 | }); 50 | 51 | afterEach(async () => { 52 | await provider.send('evm_revert', [snapshotId]); 53 | }); 54 | 55 | it('should allow a Noun owner to propose with a single vote when the total supply is 204', async () => { 56 | await mockNounsToken.mock.getPriorVotes.returns(1); 57 | 58 | await mockNounsToken.mock.totalSupply.returns(204); 59 | 60 | const proposalThreshold = await nounsDaoContract.proposalThreshold(); 61 | expect(proposalThreshold).to.equal(0); 62 | 63 | await expect(nounsDaoContract.propose([constants.AddressZero], [0], [''], ['0x'], '')).to.emit( 64 | nounsDaoContract, 65 | 'ProposalCreated', 66 | ); 67 | }); 68 | 69 | it('should allow a Noun owner to propose with a single vote when the total supply is 399', async () => { 70 | await mockNounsToken.mock.getPriorVotes.returns(1); 71 | 72 | await mockNounsToken.mock.totalSupply.returns(399); 73 | 74 | const proposalThreshold = await nounsDaoContract.proposalThreshold(); 75 | expect(proposalThreshold).to.equal(0); 76 | 77 | await expect(nounsDaoContract.propose([constants.AddressZero], [0], [''], ['0x'], '')).to.emit( 78 | nounsDaoContract, 79 | 'ProposalCreated', 80 | ); 81 | }); 82 | 83 | it('should require two votes to propose once a supply of 400 Nouns is reached', async () => { 84 | await mockNounsToken.mock.totalSupply.returns(400); 85 | await mockNounsToken.mock.getPriorVotes.returns(1); 86 | 87 | const proposalThreshold = await nounsDaoContract.proposalThreshold(); 88 | expect(proposalThreshold).to.equal(1); 89 | 90 | await expect( 91 | nounsDaoContract.propose([constants.AddressZero], [0], [''], ['0x'], ''), 92 | ).to.be.revertedWith('NounsDAO::propose: proposer votes below proposal threshold'); 93 | 94 | await mockNounsToken.mock.getPriorVotes.returns(2); 95 | 96 | await expect(nounsDaoContract.propose([constants.AddressZero], [0], [''], ['0x'], '')).to.emit( 97 | nounsDaoContract, 98 | 'ProposalCreated', 99 | ); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /test/abi/nouns-dao-executor.abi.ts: -------------------------------------------------------------------------------- 1 | export const NounsDAOExecutorABI = [ 2 | { 3 | inputs: [ 4 | { internalType: 'address', name: 'admin_', type: 'address' }, 5 | { internalType: 'uint256', name: 'delay_', type: 'uint256' }, 6 | ], 7 | stateMutability: 'nonpayable', 8 | type: 'constructor', 9 | }, 10 | { 11 | anonymous: false, 12 | inputs: [ 13 | { indexed: true, internalType: 'bytes32', name: 'txHash', type: 'bytes32' }, 14 | { indexed: true, internalType: 'address', name: 'target', type: 'address' }, 15 | { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, 16 | { indexed: false, internalType: 'string', name: 'signature', type: 'string' }, 17 | { indexed: false, internalType: 'bytes', name: 'data', type: 'bytes' }, 18 | { indexed: false, internalType: 'uint256', name: 'eta', type: 'uint256' }, 19 | ], 20 | name: 'CancelTransaction', 21 | type: 'event', 22 | }, 23 | { 24 | anonymous: false, 25 | inputs: [ 26 | { indexed: true, internalType: 'bytes32', name: 'txHash', type: 'bytes32' }, 27 | { indexed: true, internalType: 'address', name: 'target', type: 'address' }, 28 | { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, 29 | { indexed: false, internalType: 'string', name: 'signature', type: 'string' }, 30 | { indexed: false, internalType: 'bytes', name: 'data', type: 'bytes' }, 31 | { indexed: false, internalType: 'uint256', name: 'eta', type: 'uint256' }, 32 | ], 33 | name: 'ExecuteTransaction', 34 | type: 'event', 35 | }, 36 | { 37 | anonymous: false, 38 | inputs: [{ indexed: true, internalType: 'address', name: 'newAdmin', type: 'address' }], 39 | name: 'NewAdmin', 40 | type: 'event', 41 | }, 42 | { 43 | anonymous: false, 44 | inputs: [{ indexed: true, internalType: 'uint256', name: 'newDelay', type: 'uint256' }], 45 | name: 'NewDelay', 46 | type: 'event', 47 | }, 48 | { 49 | anonymous: false, 50 | inputs: [{ indexed: true, internalType: 'address', name: 'newPendingAdmin', type: 'address' }], 51 | name: 'NewPendingAdmin', 52 | type: 'event', 53 | }, 54 | { 55 | anonymous: false, 56 | inputs: [ 57 | { indexed: true, internalType: 'bytes32', name: 'txHash', type: 'bytes32' }, 58 | { indexed: true, internalType: 'address', name: 'target', type: 'address' }, 59 | { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, 60 | { indexed: false, internalType: 'string', name: 'signature', type: 'string' }, 61 | { indexed: false, internalType: 'bytes', name: 'data', type: 'bytes' }, 62 | { indexed: false, internalType: 'uint256', name: 'eta', type: 'uint256' }, 63 | ], 64 | name: 'QueueTransaction', 65 | type: 'event', 66 | }, 67 | { stateMutability: 'payable', type: 'fallback' }, 68 | { 69 | inputs: [], 70 | name: 'GRACE_PERIOD', 71 | outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], 72 | stateMutability: 'view', 73 | type: 'function', 74 | }, 75 | { 76 | inputs: [], 77 | name: 'MAXIMUM_DELAY', 78 | outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], 79 | stateMutability: 'view', 80 | type: 'function', 81 | }, 82 | { 83 | inputs: [], 84 | name: 'MINIMUM_DELAY', 85 | outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], 86 | stateMutability: 'view', 87 | type: 'function', 88 | }, 89 | { inputs: [], name: 'acceptAdmin', outputs: [], stateMutability: 'nonpayable', type: 'function' }, 90 | { 91 | inputs: [], 92 | name: 'admin', 93 | outputs: [{ internalType: 'address', name: '', type: 'address' }], 94 | stateMutability: 'view', 95 | type: 'function', 96 | }, 97 | { 98 | inputs: [ 99 | { internalType: 'address', name: 'target', type: 'address' }, 100 | { internalType: 'uint256', name: 'value', type: 'uint256' }, 101 | { internalType: 'string', name: 'signature', type: 'string' }, 102 | { internalType: 'bytes', name: 'data', type: 'bytes' }, 103 | { internalType: 'uint256', name: 'eta', type: 'uint256' }, 104 | ], 105 | name: 'cancelTransaction', 106 | outputs: [], 107 | stateMutability: 'nonpayable', 108 | type: 'function', 109 | }, 110 | { 111 | inputs: [], 112 | name: 'delay', 113 | outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], 114 | stateMutability: 'view', 115 | type: 'function', 116 | }, 117 | { 118 | inputs: [ 119 | { internalType: 'address', name: 'target', type: 'address' }, 120 | { internalType: 'uint256', name: 'value', type: 'uint256' }, 121 | { internalType: 'string', name: 'signature', type: 'string' }, 122 | { internalType: 'bytes', name: 'data', type: 'bytes' }, 123 | { internalType: 'uint256', name: 'eta', type: 'uint256' }, 124 | ], 125 | name: 'executeTransaction', 126 | outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }], 127 | stateMutability: 'nonpayable', 128 | type: 'function', 129 | }, 130 | { 131 | inputs: [], 132 | name: 'pendingAdmin', 133 | outputs: [{ internalType: 'address', name: '', type: 'address' }], 134 | stateMutability: 'view', 135 | type: 'function', 136 | }, 137 | { 138 | inputs: [ 139 | { internalType: 'address', name: 'target', type: 'address' }, 140 | { internalType: 'uint256', name: 'value', type: 'uint256' }, 141 | { internalType: 'string', name: 'signature', type: 'string' }, 142 | { internalType: 'bytes', name: 'data', type: 'bytes' }, 143 | { internalType: 'uint256', name: 'eta', type: 'uint256' }, 144 | ], 145 | name: 'queueTransaction', 146 | outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], 147 | stateMutability: 'nonpayable', 148 | type: 'function', 149 | }, 150 | { 151 | inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], 152 | name: 'queuedTransactions', 153 | outputs: [{ internalType: 'bool', name: '', type: 'bool' }], 154 | stateMutability: 'view', 155 | type: 'function', 156 | }, 157 | { 158 | inputs: [{ internalType: 'uint256', name: 'delay_', type: 'uint256' }], 159 | name: 'setDelay', 160 | outputs: [], 161 | stateMutability: 'nonpayable', 162 | type: 'function', 163 | }, 164 | { 165 | inputs: [{ internalType: 'address', name: 'pendingAdmin_', type: 'address' }], 166 | name: 'setPendingAdmin', 167 | outputs: [], 168 | stateMutability: 'nonpayable', 169 | type: 'function', 170 | }, 171 | { stateMutability: 'payable', type: 'receive' }, 172 | ]; 173 | --------------------------------------------------------------------------------