diff --git a/.env.example b/.env.example index c506dcd..eb35073 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,24 @@ -# NETWORK AND DEPLOYMENT WALLET -DEPLOYMENT_PRIVATE_KEY="..." -ALCHEMY_API_KEY="..." -ETHERSCAN_API_KEY="..." -NETWORK="holesky" -DEPLOY_AS_PRODUCTION=true # With false, the script will deploy mock helpers +# NETWORK AND ACCOUNT(s) +DEPLOYMENT_PRIVATE_KEY="0x..." +REFUND_ADDRESS="0x..." + +# The name of the networks to use for test/production +TESTNET_NETWORK="holesky" +PRODNET_NETWORK="mainnet" + +# The RPC of the networks to use for test/production +TESTNET_RPC_URL="https://holesky.drpc.org" +PRODNET_RPC_URL="https://eth.drpc.org" + +# API Keys (optional) +# Note that having these active will slow down unit tests even when not needed +# So recommended to only activate when needed +# ALCHEMY_API_KEY="..." +# ETHERSCAN_API_KEY="..." + +# MULTISIG PARAMETERS +# define a list of multisig members - said multisig will be assigned administrator roles of the ve contracts +MULTISIG_MEMBERS_JSON_FILE_NAME="/script/multisig-members.json" # GOVERNANCE PARAMETERS MIN_VETO_RATIO="300000" # 30% (base 1_000_000) @@ -24,6 +39,7 @@ TAIKO_BRIDGE_ADDRESS="0xA098b76a3Dd499D3F6D58D8AcCaFC8efBFd06807" # Address of t DAO_FACTORY="0xE640Da5AD169630555A86D9b6b9C145B4961b1EB" PLUGIN_SETUP_PROCESSOR="0xCe0B4124dea6105bfB85fB4461c4D39f360E9ef3" PLUGIN_REPO_FACTORY="0x95D563382BeD5AcB458759EE05b27DF2CB019Cc7" + GOVERNANCE_ERC20_BASE="0xC24188a73dc09aA7C721f96Ad8857B469C01dC9f" GOVERNANCE_WRAPPED_ERC20_BASE="0x7a62da7B56fB3bfCdF70E900787010Bc4c9Ca42e" diff --git a/.env.test.example b/.env.test.example new file mode 100644 index 0000000..9b3e753 --- /dev/null +++ b/.env.test.example @@ -0,0 +1,14 @@ +# If deploying against a fork, pass the address of a large token holder. +# This address will be impersonated to distribute tokens to addresses inside test cases. +# The whale needs to hold at least 3000 tokens +TEST_TOKEN_WHALE="" + +# If you are testing with `make test-fork-factory-*`, you need to define the address of the +# existing factory to use. Otherwise, you should use `make test-fork-*` +FACTORY_ADDRESS="" + +# The block number to run test forks against +# If left empty, the live onchain state will be used, which may +# consume API calls or cause rate limits +FORK_TESTNET_BLOCK_NUMBER=2643743 +FORK_PRODNET_BLOCK_NUMBER=21084855 diff --git a/.gitignore b/.gitignore index e74f8df..80e3a6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Signer lists +script/multisig-*.json +!script/multisig-members.json + # Compiler files cache/ out/ @@ -10,8 +14,11 @@ docs/ # Dotenv file .env +.env.test lcov.info .DS_Store *.tree + +logs/* diff --git a/DEPLOYMENTS.md b/DEPLOYMENTS.md index 682d4d7..e45d5bf 100644 --- a/DEPLOYMENTS.md +++ b/DEPLOYMENTS.md @@ -4,6 +4,76 @@ ## Holesly +### November 18th 2024 + +Deployment for internal testing: +- Exit window of 7 days +- L2 disabled +- Using a pre-release voting token + +Deployment with the encryption registry available + +``` + Deploying from: 0x424797Ed6d902E17b9180BFcEF452658e148e0Ab + Chain ID: 17000 + Using test token settings + Minting test tokens for the multisig members and the bridge + + Factory: 0x7D3dA38E856f002f4623B0D32a494E358f72adC9 + + DAO: 0x8caD8b62769710233f319611d064462b633Bbb8C + Voting token: 0x18EE0C13EC97a60fc190bABB348FD87421368920 + Taiko Bridge: 0x0000000000000000000000000000001234567890 + + Plugins + - Multisig plugin: 0xc880dB28A9105e6D69d30E93d99E38eFE84c54CB + - Emergency multisig plugin: 0x3bE6294EB67A3501bF091fD229282F2A51c532d2 + - Optimistic token voting plugin: 0xBB249c027c5De908288104F665A605ceC88ad6CE + + Helpers + - Signer list 0x7716DcB9B83f5f9fB5266767841c3F29555cE2d5 + - Encryption registry 0x94224B656D7D174B2Aa97FFCB188A847E6EA4511 + - Delegation wall 0xb8D78b40014D36F83dDc8219c0621d35E8043167 + + Plugin repositories + - Multisig plugin repository: 0x2d870FCedF2C1204839C3b8bca2Bf6e632b4E602 + - Emergency multisig plugin repository: 0xe069Ae1DCB19A9DE9C516097FaC20ea070311D48 + - Optimistic token voting plugin repository: 0xd774b0976C67832C84848dC6fdadE6189B297A71 +``` + +### October 16th 2024 + +Deployment for internal testing: +- Exit window of 2h +- L2 disabled +- Using a pre-release voting token + +``` +Chain ID: 17000 +Deploying from: 0x424797Ed6d902E17b9180BFcEF452658e148e0Ab +Using production settings + +Factory: 0xFC84a8516Cc08F7cAB9633C900eB7E54811533Cd + +DAO: 0x7A1a8393678cFB7C72d9C3Ed0Db69F7A336224b7 +Voting token: 0x7dbcF74e44EFc5eC635f40c962d90F2EeD81069a +Taiko Bridge: 0xA098b76a3Dd499D3F6D58D8AcCaFC8efBFd06807 + +Plugins +- Multisig plugin: 0x3952b0de6537866d872331d529357C23427cf364 +- Emergency multisig plugin: 0x38aC34F55A0712C101697360118fEC35AeC777C9 +- Optimistic token voting plugin: 0xd0E3fC86DD0AdA97aC2a3432b75BE31b0e1E900F + +Plugin repositories +- Multisig plugin repository: 0xa77DDA30b1a0AbAa837212C458C46a1Ae8a60Cc6 +- Emergency multisig plugin repository: 0x875A8BBac6880c965844f4d3935fD892C8f3F931 +- Optimistic token voting plugin repository: 0xF03e700D8C08c8c50BB5e7C7165342858172E65a + +Helpers +- Encryption registry 0xD0D409d0048F998fb58a6b352Cf58239c5168d53 +- Delegation wall 0x0470d887b19cf877949A5Bc227042DFfAa3d7752 +``` + ### August 1st 2024 Deployment for internal testing, with L2 voting disabled and using a pre-release voting token. @@ -12,23 +82,23 @@ Deployment for internal testing, with L2 voting disabled and using a pre-release Chain ID: 17000 Deploying from: 0x424797Ed6d902E17b9180BFcEF452658e148e0Ab Using production settings - + Factory: 0xC06F1a08fBacF5895aDe3EFB137Dc2Cc2dA7b3B9 - + DAO: 0xC38fFd23688cF6f70b61C7FD6ca6D7D2C84Ef252 Voting token: 0x7dbcF74e44EFc5eC635f40c962d90F2EeD81069a Taiko Bridge: 0xA098b76a3Dd499D3F6D58D8AcCaFC8efBFd06807 - + Plugins - Multisig plugin: 0x038FdE3344EfFe37A4575cA1276f1982A43ce9dF - Emergency multisig plugin: 0x0fC611670228A61824c317926f30e8a2615aa1A3 - Optimistic token voting plugin: 0x619d6661eA06b917e26694f23c5Bb32fa0456773 - + Plugin repositories - Multisig plugin repository: 0xcba5780F2054BB9FAEA4f55047bdcD5828704829 - Emergency multisig plugin repository: 0x175749Dec3157ADFf45D20abF61F8Cf9c17D16Af - Optimistic token voting plugin repository: 0x8D762BdEb9582b782D2955C3C6701Fc1a89fe8FD - + Helpers - Public key registry 0x9695520e32F85eF403f6B18b8a94e44A90D5cBF0 - Delegation wall 0x15B379C5c9115e645Cdf1EF9fA03389586AfEa2A @@ -43,18 +113,18 @@ Deployment for internal testing, with L2 voting disabled and using a test voting Deploying from: 0x424797Ed6d902E17b9180BFcEF452658e148e0Ab Using internal testing settings Minting test tokens for the multisig members and the bridge - + Factory: 0xF9Be929F990F9C8bF9ed355Ddd29Af7bd9995890 - + DAO: 0xeB4586617089270Fe042F69Bf799590AF224807a Voting token: 0x12b2574840dB17C2278d9725a2679E97FE266075 Taiko Bridge: 0x0000000000000000000000000000001234567890 - + Plugins - Multisig plugin: 0xd8Fe1194Cf90eF38b54A110EcfeAE8F2AA5Dfe86 - Emergency multisig plugin: 0xeCBa720A8645B198b2637f6559B9155E4bc3B566 - Optimistic token voting plugin: 0xd9F6A2533efab98bA016Cb1D3001b6Ec1C246485 - + Plugin repositories - Multisig plugin repository: 0xa51B2d7b7847cFB666919301e03f48b596A15871 - Emergency multisig plugin repository: 0x2ce4e91D1a00c42736730B494Ab9BFfbfEDdF2ac @@ -74,23 +144,23 @@ Deployment for internal testing, targeting test dependencies. Deploying from: 0x424797Ed6d902E17b9180BFcEF452658e148e0Ab Using internal testing settings Minting test tokens for the multisig members and the bridge - + Factory: 0x151dB38A460F3c4F9F377cf040A5Ed5D9958940D - + DAO: 0x192206aA5807ADef5C6C32ffBA2C6dA8e4473e9e Voting token: 0xA8888c98205B146804798B4dA1411288B5E8bb1C Taiko Bridge: 0x0000000000000000000000000000001234567890 - + Plugins - Multisig plugin: 0xd3e68dB8B60120D79032E8eb84c620CE6D9D6258 - Emergency multisig plugin: 0x155f75684Ed220D78634432F892D61b8B7D592B5 - Optimistic token voting plugin: 0x4f438847492002FF84B3735e1da8E65fADD18271 - + Plugin repositories - Multisig plugin repository: 0xC16d70743046b3478728eE22Ca3110515Fa05718 - Emergency multisig plugin repository: 0x20235f476181a8C3b5121e36EAb13e4Bf6A65cD4 - Optimistic token voting plugin repository: 0xa03ef51E9cCBe245BF2A7bF431eE0A81908d1e84 - + Helpers - Public key registry 0xB96057cC9A2bb13C837d88d10370A804Efe68396 - Delegation wall 0xE1A79CCd6d5Dda5dCfCC4B2aaCfE458A82B2F914 @@ -105,23 +175,23 @@ Deployment for internal testing. Targetting Taiko's deployment. Chain ID: 17000 Deploying from: 0x424797Ed6d902E17b9180BFcEF452658e148e0Ab Using production settings - + Factory: 0x30435F686dA174f5B646E75684A0795F6A06d0C8 - + DAO: 0xcB10AB2E59Ac73e202adE31531462F7a75cfe74C Voting token: 0x6490E12d480549D333499236fF2Ba6676C296011 Taiko Bridge: 0xA098b76a3Dd499D3F6D58D8AcCaFC8efBFd06807 - + Plugins - Multisig plugin: 0x9d2f62109CE2fDb3FaE58f14D2c1CedFdc7939f9 - Emergency multisig plugin: 0x2198F07F02b2D7365C7Df8C488741B43EE076f83 - Optimistic token voting plugin: 0x799A3D93DB762A838F41Dd956857463AC9D245d7 - + Plugin repositories - Multisig plugin repository: 0xA16B5FD427EA11f171104945B6360793C801766B - Emergency multisig plugin repository: 0x5644C0B88a571B35C0AaA2F9378A06F60f04A927 - Optimistic token voting plugin repository: 0x48309dCFc32eBB1CB6DbA9169F8259f35d4fE993 - + Helpers - Public key registry 0x054098E107FCd07d1C3D0F97Ba8217CE85AaC3ca - Delegation wall 0x9A118b78dE4b3c91706f45Bb8686f678d5600500 @@ -135,23 +205,23 @@ Deployment intended for staging purposes. Chain ID: 17000 Deploying from: 0x424797Ed6d902E17b9180BFcEF452658e148e0Ab Minting test tokens for the multisig members and the bridge - + Factory: 0x2799EBD75fA793b93c4feBdb134b3b6Cbbb32124 - + DAO: 0xa0FDC6b2bf9FFd48D4F86b697761F13b32D0b7A1 Voting token: 0x01aeE1a16C8807DF52f2DA9191Cec8058e747F4A Taiko Bridge: 0x0000000000000000000000000000001234567890 - + Plugins - Multisig plugin: 0x284F47A42f1Eb96f0F1540931F8Ef04F4243Fb33 - Emergency multisig plugin: 0x0E09bFDA087cf60Bd03A767A03bf88e9E3824c39 - Optimistic token voting plugin: 0xf52B4681F1eB88C5b028510a3F365b5d04fa3295 - + Plugin repositories - Multisig plugin repository: 0x00fD4E0093a885F20208308C996461dbD93d3604 - Emergency multisig plugin repository: 0xb17469b843Ec56Bd75b118b461C07BA520f792d1 - Optimistic token voting plugin repository: 0xd49028E41E941296A48e5b1733bBDA857509FD1b - + Helpers - Public key registry 0x3b1a9c9198eF98d987A6361219FC59c3F805537d - Delegation wall 0xfdFd89FA33B92Cd1c49A2Ae452294Bc2C89f810D @@ -166,21 +236,21 @@ Used for internal development, using a different Taiko Bridge address. Chain ID: 17000 Deploying from: 0x424797Ed6d902E17b9180BFcEF452658e148e0Ab Minting test tokens for the multisig members and the bridge - + Factory contract: 0x57B11BfBEEc6935b307abF8a9c8Ce0DE8DB1868C DAO contract: 0xfCb5AC35C8Ab27c8f6B277a2963e7352f71ca993 Voting token: 0xD2275fEdcE5defbCccA4C29EE058455288248F84 Taiko Bridge: 0x0000000000000000000000000000001234567890 - + - Multisig plugin: 0x9cBDcae87CBE9bdbb9A882A551F4A3F20D007033 - Emergency multisig plugin: 0x456349f1F6621604536E99dB591EBD94e00d94F6 - Optimistic token voting plugin: 0xF9b68bD4a57281f3Ae8FE9A4600BD516fc7938c5 - + - Multisig plugin repository: 0xF5625F767D06814Becd2e4d224629dBA589c905E - Emergency multisig plugin repository: 0x920adce1a42A07E6A167A39a94194739e7602e55 - Optimistic token voting plugin repository: 0xd26d960b2BbfD0efcC16659f804A636c6B46bBce - + Helpers: - Public key registry 0x71D886c82694828f223136d6db18A3603ed8110e - Delegation wall 0xdeb0377b711DbA11d4f6B90EC2153256B8E17fd8 @@ -191,22 +261,22 @@ Used for internal development. ``` Chain ID: 17000 - + Deploying from: 0x424797Ed6d902E17b9180BFcEF452658e148e0Ab Minting test tokens for the multisig members and the bridge Test voting token: 0x53bbA0e878a73013AA0B1Dc6e6c4ea9691182E04 Factory contract: 0x06D323915f7057e32B0560b95A298c5a2Fe80C8d - + DAO contract: 0xC373851C8a42D0c9120f5bd6c218693CFED068C1 - + - Multisig plugin: 0x754C929002d09d09610831F81263Bb5A43Ea0865 - Emergency multisig plugin: 0x21B1eeb7A9ff58e4422eB2a06A8b2b2ceb0aC581 - Optimistic token voting plugin: 0x14DCBE5aAF3Ce2998E93f98DcFAB1cbd198D1257 - + - Multisig plugin repository: 0x494d47d419c2b48e3f888066FAf210DD32BFA1b6 - Emergency multisig plugin repository: 0xcA7404c1dDD5cb817E94F970256972b277F82f80 - Optimistic token voting plugin repository: 0xAe66318a5941712A80eA7B6e2F96C23B071816E5 - + Public key registry 0x683C6B9c550870423cEc58f6cedd78BCE36Fd7f1 Delegation wall 0x291aAE5fCAbBbD19A1b64F93338B71343E2AD740 ``` @@ -216,22 +286,22 @@ Used as a staging deployment. ``` Chain ID: 17000 - + Deploying from: 0x424797Ed6d902E17b9180BFcEF452658e148e0Ab Minting test tokens for the multisig members and the bridge Test voting token: 0xa95BADd91beB92F364905187eCB08B80220d5FA3 Factory contract: 0xFbA94606d10e807Bf6542C19a68DfEa815a4eeC3 - + DAO contract: 0xdA69Bd97278c409574AdC39295465A848C82CD16 - + - Multisig plugin: 0x2a22Fc29dE8944E62227bf75C89cA2e8CE9BA274 - Emergency multisig plugin: 0x7C36a0F03c27880C23f5704296Bc18Bfc33A7f59 - Optimistic token voting plugin: 0x40CD85d43B883C83290ed5D18400C640176A9679 - + - Multisig plugin repository: 0x307d009483C1b8Ef3C91F6ae748385Bf0936C59e - Emergency multisig plugin repository: 0x8181da2e9b1a428a4cF60fF6CEFc0098c1298aaA - Optimistic token voting plugin repository: 0x0847F2531e070353297fc3D7fFDB4656C1664c6d - + Public key registry 0x7A9577A02608446022F52984435ce1ca632BA629 Delegation wall 0xE917426E10a54FbF22FDAF32A4151c90550e1cA5 ``` diff --git a/Makefile b/Makefile index 805ace6..2cf2ab1 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,81 @@ .DEFAULT_TARGET: help +# Import the .env files and export their values (ignore any error if missing) +-include .env +-include .env.test + +# RULE SPECIFIC ENV VARS [optional] + +# Override the verifier and block explorer parameters (network dependent) +deploy-testnet: export ETHERSCAN_API_KEY_PARAM = --etherscan-api-key $(ETHERSCAN_API_KEY) +deploy-prodnet: export ETHERSCAN_API_KEY_PARAM = --etherscan-api-key $(ETHERSCAN_API_KEY) +# deploy-testnet: export VERIFIER_TYPE_PARAM = --verifier blockscout +# deploy-testnet: export VERIFIER_URL_PARAM = --verifier-url "https://server/api\?" + +# CONSTANTS + +TEST_COVERAGE_SRC_FILES:=$(wildcard test/*.sol test/**/*.sol script/*.sol script/**/*.sol src/escrow/increasing/delegation/*.sol src/libs/ProxyLib.sol) +DEPLOY_SCRIPT:=script/Deploy.s.sol:Deploy +VERBOSITY:=-vvv +SHELL:=/bin/bash + SOLIDITY_VERSION=0.8.17 +TEST_TREE_MARKDOWN=TEST_TREE.md SOURCE_FILES=$(wildcard test/*.t.yaml test/integration/*.t.yaml) TREE_FILES = $(SOURCE_FILES:.t.yaml=.tree) TARGET_TEST_FILES = $(SOURCE_FILES:.tree=.t.sol) MAKE_TEST_TREE=deno run ./test/script/make-test-tree.ts -TEST_TREE_MARKDOWN=TEST_TREE.md +MAKEFILE=Makefile + +# TARGETS .PHONY: help help: @echo "Available targets:" - @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ - | sed -n 's/^\(.*\): \(.*\)##\(.*\)/- make \1 \3/p' + @grep -E '^[a-zA-Z0-9_-]*:.*?## .*$$' Makefile \ + | sed -n 's/^\(.*\): \(.*\)##\(.*\)/- make \1 \3/p' \ + | sed 's/^- make $$//g' -all: sync markdown ## Builds all tree files and updates the test tree markdown +: ## -sync: $(TREE_FILES) ## Scaffold or sync tree files into solidity tests +.PHONY: init +init: .env .env.test ## Check the dependencies and prompt to install if needed + @which deno > /dev/null && echo "Deno is available" || echo "Install Deno: curl -fsSL https://deno.land/install.sh | sh" + @which bulloak > /dev/null && echo "bulloak is available" || echo "Install bulloak: cargo install bulloak" + + @which forge > /dev/null || curl -L https://foundry.paradigm.xyz | bash + @forge build + @which lcov > /dev/null || echo "Note: lcov can be installed by running 'sudo apt install lcov'" + +.PHONY: clean +clean: ## Clean the build artifacts + rm -f $(TREE_FILES) + rm -f $(TEST_TREE_MARKDOWN) + rm -Rf ./out/* lcov.info* ./report/* + +: ## + +.PHONY: test +test: ## Run unit tests, locally + forge test $(VERBOSITY) +# forge test --no-match-path $(FORK_TEST_WILDCARD) $(VERBOSITY) + +test-coverage: report/index.html ## Generate an HTML coverage report under ./report + @which open > /dev/null && open report/index.html || echo -n + @which xdg-open > /dev/null && xdg-open report/index.html || echo -n + +report/index.html: lcov.info.pruned + genhtml $^ -o report --branch-coverage + +lcov.info.pruned: lcov.info + lcov --remove ./$< -o ./$@ $^ + +lcov.info: $(TEST_COVERAGE_SRC_FILES) + forge coverage --no-match-path $(FORK_TEST_WILDCARD) --report lcov + +: ## + +sync-tests: $(TREE_FILES) ## Scaffold or sync tree files into solidity tests @for file in $^; do \ if [ ! -f $${file%.tree}.t.sol ]; then \ echo "[Scaffold] $${file%.tree}.t.sol" ; \ @@ -26,10 +86,10 @@ sync: $(TREE_FILES) ## Scaffold or sync tree files into solidity tests fi \ done -check: $(TREE_FILES) ## Checks if solidity files are out of sync +check-tests: $(TREE_FILES) ## Checks if solidity files are out of sync bulloak check $^ -markdown: $(TEST_TREE_MARKDOWN) ## Generates a markdown file with the test definitions rendered as a tree +markdown-tests: $(TEST_TREE_MARKDOWN) ## Generates a markdown file with the test definitions rendered as a tree # Internal targets @@ -54,18 +114,138 @@ $(TREE_FILES): $(SOURCE_FILES) %.tree: %.t.yaml @for file in $^; do \ - echo "[Convert] $$file -> $${file%.t.yaml}.tree" ; \ + echo "[Convert] $$file -> $${file%.t.yaml}.tree" ; \ cat $$file | $(MAKE_TEST_TREE) > $${file%.t.yaml}.tree ; \ done -# Global +# Copy the .env files if not present +.env: + cp .env.example .env + @echo "NOTE: Edit the correct values of .env before you continue" -.PHONY: init -init: ## Check the dependencies and prompt to install if needed - @which deno > /dev/null && echo "Deno is available" || echo "Install Deno: curl -fsSL https://deno.land/install.sh | sh" - @which bulloak > /dev/null && echo "bulloak is available" || echo "Install bulloak: cargo install bulloak" +.env.test: + cp .env.test.example .env.test + @echo "NOTE: Edit the correct values of .env.test before you continue" -.PHONY: clean -clean: ## Clean the intermediary tree files - rm -f $(TREE_FILES) - rm -f $(TEST_TREE_MARKDOWN) +# : ## + +# #### Fork testing #### + +# test-fork-mint-testnet: export MINT_TEST_TOKENS = true +# test-fork-mint-prodnet: export MINT_TEST_TOKENS = true + +# test-fork-mint-testnet: test-fork-testnet ## Clean fork test, minting test tokens (testnet) +# test-fork-mint-prodnet: test-fork-prodnet ## Clean fork test, minting test tokens (production network) + +# : ## + +# test-fork-testnet: export RPC_URL = $(TESTNET_RPC_URL) +# test-fork-prodnet: export RPC_URL = $(PRODNET_RPC_URL) +# test-fork-testnet: export FORK_BLOCK_NUMBER = $(FORK_TESTNET_BLOCK_NUMBER) +# test-fork-prodnet: export FORK_BLOCK_NUMBER = $(FORK_PRODNET_BLOCK_NUMBER) + +# test-fork-testnet: test-fork ## Fork test using the existing token(s), new factory (testnet) +# test-fork-prodnet: test-fork ## Fork test using the existing token(s), new factory (production network) + +# : ## + +# # Override the fork test mode (existing factory) +# test-fork-factory-testnet: export FORK_TEST_MODE = existing-factory +# test-fork-factory-prodnet: export FORK_TEST_MODE = existing-factory + +# test-fork-factory-testnet: test-fork-testnet ## Fork test using an existing factory (testnet) +# test-fork-factory-prodnet: test-fork-prodnet ## Fork test using an existing factory (production network) + +# .PHONY: test-fork +# test-fork: +# @if [ -z "$(strip $(FORK_BLOCK_NUMBER))" ] ; then \ +# forge test --match-contract $(E2E_TEST_NAME) \ +# --rpc-url $(RPC_URL) \ +# $(VERBOSITY) ; \ +# else \ +# forge test --match-contract $(E2E_TEST_NAME) \ +# --rpc-url $(RPC_URL) \ +# --fork-block-number $(FORK_BLOCK_NUMBER) \ +# $(VERBOSITY) ; \ +# fi + +: ## + +#### Deployment targets #### + +pre-deploy-mint-testnet: export MINT_TEST_TOKENS = true +pre-deploy-testnet: export RPC_URL = $(TESTNET_RPC_URL) +pre-deploy-testnet: export NETWORK = $(TESTNET_NETWORK) +pre-deploy-prodnet: export RPC_URL = $(PRODNET_RPC_URL) +pre-deploy-prodnet: export NETWORK = $(PRODNET_NETWORK) + +pre-deploy-mint-testnet: pre-deploy-testnet ## Simulate a deployment to the testnet, minting test token(s) +pre-deploy-testnet: pre-deploy ## Simulate a deployment to the testnet +pre-deploy-prodnet: pre-deploy ## Simulate a deployment to the production network + +: ## + +deploy-mint-testnet: export MINT_TEST_TOKENS = true +deploy-testnet: export RPC_URL = $(TESTNET_RPC_URL) +deploy-testnet: export NETWORK = $(TESTNET_NETWORK) +deploy-prodnet: export RPC_URL = $(PRODNET_RPC_URL) +deploy-prodnet: export NETWORK = $(PRODNET_NETWORK) + +deploy-testnet: export DEPLOYMENT_LOG_FILE=./deployment-$(patsubst "%",%,$(TESTNET_NETWORK))-$(shell date +"%y-%m-%d-%H-%M").log +deploy-prodnet: export DEPLOYMENT_LOG_FILE=./deployment-$(patsubst "%",%,$(PRODNET_NETWORK))-$(shell date +"%y-%m-%d-%H-%M").log + +deploy-mint-testnet: deploy-testnet ## Deploy to the testnet, mint a token and verify +deploy-testnet: deploy ## Deploy to the testnet and verify +deploy-prodnet: deploy ## Deploy to the production network and verify + +.PHONY: pre-deploy +pre-deploy: + @echo "Simulating the deployment" + forge script $(DEPLOY_SCRIPT) \ + --chain $(NETWORK) \ + --rpc-url $(RPC_URL) \ + $(VERBOSITY) + +.PHONY: deploy +deploy: test + @echo "Starting the deployment" + @mkdir -p logs/ + forge script $(DEPLOY_SCRIPT) \ + --chain $(NETWORK) \ + --rpc-url $(RPC_URL) \ + --broadcast \ + --verify \ + $(VERIFIER_TYPE_PARAM) \ + $(VERIFIER_URL_PARAM) \ + $(ETHERSCAN_API_KEY_PARAM) \ + $(VERBOSITY) | tee logs/$(DEPLOYMENT_LOG_FILE) + +: ## + +refund: export DEPLOYMENT_ADDRESS = $(shell cast wallet address --private-key $(DEPLOYMENT_PRIVATE_KEY)) + +.PHONY: refund +refund: ## Refund the remaining balance left on the deployment account + @echo "Refunding the remaining balance on $(DEPLOYMENT_ADDRESS)" + @if [ -z $(REFUND_ADDRESS) -o $(REFUND_ADDRESS) = "0x0000000000000000000000000000000000000000" ]; then \ + echo "- The refund address is empty" ; \ + exit 1; \ + fi + @BALANCE=$(shell cast balance $(DEPLOYMENT_ADDRESS) --rpc-url $(PRODNET_RPC_URL)) && \ + GAS_PRICE=$(shell cast gas-price --rpc-url $(PRODNET_RPC_URL)) && \ + REMAINING=$$(echo "$$BALANCE - $$GAS_PRICE * 21000" | bc) && \ + \ + ENOUGH_BALANCE=$$(echo "$$REMAINING > 0" | bc) && \ + if [ "$$ENOUGH_BALANCE" = "0" ]; then \ + echo -e "- No balance can be refunded: $$BALANCE wei\n- Minimum balance: $${REMAINING:1} wei" ; \ + exit 1; \ + fi ; \ + echo -n -e "Summary:\n- Refunding: $$REMAINING (wei)\n- Recipient: $(REFUND_ADDRESS)\n\nContinue? (y/N) " && \ + \ + read CONFIRM && \ + if [ "$$CONFIRM" != "y" ]; then echo "Aborting" ; exit 1; fi ; \ + \ + cast send --private-key $(DEPLOYMENT_PRIVATE_KEY) \ + --rpc-url $(PRODNET_RPC_URL) \ + --value $$REMAINING \ + $(REFUND_ADDRESS) diff --git a/README.md b/README.md index 5d756b4..75bdfae 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ Another key difference is that the Emergency Multisig is designed such in a way See [Deploying the DAO](#deploying-the-dao) below and check out the [contract deployments](./DEPLOYMENTS.md). -## Optimistic Token Voting plugin +## Contracts overview + +### Optimistic Token Voting plugin This plugin is an adapted version of Aragon's [Optimistic Token Voting plugin](https://github.com/aragon/optimistic-token-voting-plugin). @@ -26,33 +28,35 @@ Proposals can only be executed when the veto threshold hasn't been reached after The governance settings need to be defined when the plugin is installed but the DAO can update them at any time. -### Permissions +#### Permissions - Only proposers can create proposals on the plugin - The plugin can execute actions on the DAO - The DAO can update the plugin settings - The DAO can upgrade the plugin -## Multisig (standard flow) +### Multisig (standard flow) It allows the Security Council members to create and approve proposals. After a certain minimum of approvals is met, proposals can be relayed to the [Optimistic Token Voting plugin](#optimistic-token-voting-plugin) only. +The list of signers for this plugin is taken from SignerList contract. Any changes on it will effect both plugin instances. + The ability to relay proposals to the [Optimistic Token Voting plugin](#optimistic-token-voting-plugin) is restricted by a [permission condition](src/conditions/StandardProposalCondition.sol), which ensures that a minimum veto period is defined as part of the parameters. ![Standard proposal flow](./img/std-proposal-flow.png) -### Permissions +#### Permissions - Only members can create proposals - Only members can approve - The plugin can only create proposals on the [Optimistic Token Voting plugin](#optimistic-token-voting-plugin) provided that the `duration` is equal or greater than the minimum defined - The DAO can update the plugin settings -## Emergency Multisig +### Emergency Multisig Like before, this plugin allows Security Council members to create and approve proposals. If a super majority approves, proposals can be relayed to the [Optimistic Token Voting plugin](#optimistic-token-voting-plugin) with a delay period of potentially 0. This is, being executed immediately. -The address list of this plugin is taken from the standard Multisig plugin. Any changes on the former will effect both plugin instances. +The list of signers for this plugin is taken from SignerList contract. Any changes on it will effect both plugin instances. There are two key differences with the standard Multisig: 1. The proposal's metadata and the actions to execute are encrypted, only the Security Council members have the means to decrypt them @@ -60,7 +64,7 @@ There are two key differences with the standard Multisig: ![Emergency proposal flow](./img/emergency-proposal-flow.png) -### Permissions +#### Permissions The Emergency Multisig settings are the same as for the standard Multisig. @@ -69,27 +73,53 @@ The Emergency Multisig settings are the same as for the standard Multisig. - The plugin can only create proposals on the [Optimistic Token Voting plugin](#optimistic-token-voting-plugin) provided that the `duration` is equal or greater than the minimum defined - The DAO can update the plugin settings -## Public Key Registry +### Signer List + +Both multisigs relate to this contract to determine if an address was listed at a certain block. It allows to read the state and manage the address list given that the appropriate permissions are granted. + +It also plays an important role regarding encryption, this is why it is coupled with the Encryption Registry (see below). + +It offers convenience methods to determine 3 potential states for a given address: +- An address was a listed signer at a given past block (owner) +- An address is appointed by another address, listed at a past block (appointed) +- An address not listed or appointed + +### The encryption challenge + +Smart wallets cannot possibly generate a private key, which means that encryption and decryption is unviable. To this end, the [EncryptionRegistry](#encryption-registry) (see below) allows listed signers to **appoint** an EOA to act on behalf of them. + +This means that the Security Council could include a member who was an organization, and such organiation could have a smart wallet. This smart wallet would then appoint one of its members' EOA, so that emergency proposals could be reviewed, approved and eventually executed. -This is a helper contract that allows Security Council members to register the public key of their deterministic ephemeral wallet. The available public keys will be used to encrypt the proposal metadata and actions. Refer to the UI repository for the encryption details. +If at any point, the member's EOA became compromised or the member left the team, the smart wallet could then appoint a new EOA and continue without impacting the rest of the Security Council. -NOTE: A published public key cannot be changed once published. +What it means: +- Owners (listed signers) + - Can always create emergency multisig proposals + - Can only approve if they are not appointing another address +- Addresses appointed by a listed signer + - Can create emergency proposals + - Can approve + - Can execute (they can decrypt the actions and the metadata) -- A wallet can only generate one derived key pair. -- Public key registration is an automated process. No human error should be possible. -- Altering an encryption key is a strange edge case of which the rest of signers should be aware of. +### Encryption Registry -## Delegation Wall +This is a helper contract that allows Security Council members ([SignerList](#signer-list) addresses) to register the public key of their deterministic ephemeral wallet. The available public keys will be used to encrypt the proposal metadata and actions. + +Given that smart contracts cannot possibly sign or decrypt data, the encryption registry allows to appoint an EOA as the end target for encryption purposes. This is useful for organizations not wanting to rely on just a single wallet. + +Refer to the UI repository for the encryption details. + +### Delegation Wall This is a very simple contract that serves the purpose of storing the IPFS URI's pointing to the delegation profile posted by all candidates. Profiles can be updated by the owner and read by everyone. -## Installing plugins to the DAO +### Installing plugins to the DAO -### Installing the initial set of plugins on the DAO +#### Installing the initial set of plugins on the DAO This is taken care by the [TaikoDAOFactory](src/factory/TaikoDaoFactory.sol) contract. It is invoked by [scripts/Deploy.s.sol](script/Deploy.s.sol) and it creates a holistic, immutable DAO deployment, given some settings. To create a new DAO with new settings, a new factory needs to be deployed. -### Installing plugins on the existing DAO +#### Installing plugins on the existing DAO Plugin changes need a proposal to be passed when the DAO already exists. @@ -105,6 +135,171 @@ See [OptimisticTokenVotingPluginSetup](src/setup/OptimisticTokenVotingPluginSetu [Learn more about plugin setup's](https://devs.aragon.org/docs/osx/how-it-works/framework/plugin-management/plugin-setup/) and [preparing installations](https://devs.aragon.org/docs/sdk/examples/client/prepare-installation). + +## Setup + +To get started, ensure that [Foundry](https://getfoundry.sh/) and [Make](https://www.gnu.org/software/make/) are installed on your computer. + +### Using the Makefile + +The `Makefile` is the target launcher of the project. It's the recommended way to work with it. It manages the env variables of common tasks and executes only the steps that require being run. + +``` +$ make +Available targets: + +- make init Check the dependencies and prompt to install if needed +- make clean Clean the build artifacts + +- make test Run unit tests, locally +- make test-coverage Generate an HTML coverage report under ./report + +- make sync-tests Scaffold or sync tree files into solidity tests +- make check-tests Checks if solidity files are out of sync +- make markdown-tests Generates a markdown file with the test definitions rendered as a tree + +- make pre-deploy-mint-testnet Simulate a deployment to the testnet, minting test token(s) +- make pre-deploy-testnet Simulate a deployment to the testnet +- make pre-deploy-prodnet Simulate a deployment to the production network + +- make deploy-testnet Deploy to the testnet and verify +- make deploy-prodnet Deploy to the production network and verify + +- make refund Refund the remaining balance left on the deployment account +``` + +Run `make init`: +- It ensures that Foundry is installed +- It runs a first compilation of the project +- It copies `.env.example` into `.env` and `.env.test.example` into `.env.test` + +Next, customize the values of `.env` and optionally `.env.test`. + +### Understanding `.env.example` + +The env.example file contains descriptions for all the initial settings. You don't need all of these right away but should review prior to fork tests and deployments + +## Deployment + +Deployments are done using the deployment factory. This is a singleton contract that will: + +- Deploy all contracts +- Set permissions +- Transfer ownership to a freshly deployed multisig +- Store the addresses of the deployment in a single source of truth that can be queried at any time. + +Check the available make targets to simulate and deploy the smart contracts: + +``` +- make pre-deploy-testnet Simulate a deployment to the defined testnet +- make pre-deploy-prodnet Simulate a deployment to the defined production network +- make deploy-testnet Deploy to the defined testnet network and verify +- make deploy-prodnet Deploy to the production network and verify +``` + +### Deployment Checklist + +- [ ] I have cloned the official repository on my computer and I have checked out the corresponding branch +- [ ] I am using the latest official docker engine, running a Debian Linux (stable) image + - [ ] I have run `docker run --rm -it -v .:/deployment debian:bookworm-slim` + - [ ] I have run `apt update && apt install -y make curl git vim neovim bc` + - [ ] I have run `curl -L https://foundry.paradigm.xyz | bash` + - [ ] I have run `source /root/.bashrc && foundryup` + - [ ] I have run `cd /deployment` + - [ ] I have run `make init` + - [ ] I have printed the contents of `.env` and `.env.test` on the screen +- [ ] I am opening an editor on the `/deployment` folder, within the Docker container +- [ ] The `.env` file contains the correct parameters for the deployment + - [ ] I have created a brand new burner wallet with `cast wallet new` and copied the private key to `DEPLOYMENT_PRIVATE_KEY` within `.env` + - [ ] I have reviewed the target network and RPC URL + - [ ] I have checked that the JSON file under `MULTISIG_MEMBERS_JSON_FILE_NAME` contains the correct list of signers + - [ ] I have ensured all multisig members have undergone a proper security review and are aware of the security implications of being on said multisig + - [ ] I have checked that `MIN_STD_APPROVALS`, `MIN_EMERGENCY_APPROVALS` and `MULTISIG_PROPOSAL_EXPIRATION_PERIOD` are correct + - [ ] I have verified that `TOKEN_ADDRESS` corresponds to an ERC20 contract on the target chain + - [ ] I have checked that `TAIKO_L1_ADDRESS` and `TAIKO_BRIDGE_ADDRESS` are correct + - The plugin ENS subdomain + - [ ] Contains a meaningful and unique value + - The given OSx addresses: + - [ ] Exist on the target network + - [ ] Contain the latest stable official version of the OSx DAO implementation, the Plugin Setup Processor and the Plugin Repo Factory + - [ ] I have verified the values on https://www.npmjs.com/package/@aragon/osx-commons-configs?activeTab=code > `/@aragon/osx-commons-configs/dist/deployments/json/` +- [ ] All my unit tests pass (`make test`) +- **Target test network** + - [ ] I have run a preview deployment on the testnet + - `make pre-deploy-mint-testnet` + - [ ] I have deployed my contracts successfully to the target testnet + - `make deploy-testnet` + - [ ] I have tested that these contracts work successfully on a UI +- **Target production network** + - [ ] I have updated `TOKEN_ADDRESS` to have the address of the testnet token deployed above + - [ ] I have checked that `TAIKO_L1_ADDRESS` and `TAIKO_BRIDGE_ADDRESS` are correct +- [ ] My deployment wallet is a newly created account, ready for safe production deploys. +- My computer: + - [ ] Is running in a safe physical location and a trusted network + - [ ] It exposes no services or ports + - [ ] The wifi or wired network used does does not have open ports to a WAN +- [ ] I have previewed my deploy without any errors + - `make pre-deploy-prodnet` +- [ ] My wallet has sufficient native token for gas + - At least, 15% more than the estimated simulation +- [ ] Unit tests still run clean +- [ ] I have run `git status` and it reports no local changes +- [ ] The current local git branch corresponds to its counterpart on `origin` + - [ ] I confirm that the rest of members of the ceremony pulled the last commit of my branch and reported the same commit hash as my output for `git log -n 1` +- [ ] I have initiated the production deployment with `make deploy-prodnet` + +### Post deployment checklist + +- [ ] The deployment process completed with no errors +- [ ] The deployed factory was deployed by the deployment address +- [ ] The reported contracts have been created created by the newly deployed factory +- [ ] The smart contracts are correctly verified on Etherscan or the corresponding block explorer +- [ ] The output of the latest `deployment-*.log` file corresponds to the console output +- [ ] I have transferred the remaining funds of the deployment wallet to the address that originally funded it + - `make refund` + +### Manual from the command line + +You can of course run all commands from the command line: + +```sh +# Load the env vars +source .env +``` + +```sh +# run unit tests +forge test --no-match-path "test/fork/**/*.sol" +``` + +```sh +# Set the right RPC URL +RPC_URL="https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}" +``` + +```sh +# Run the deployment script + +# If using Etherscan +forge script --chain "$NETWORK" script/DeployGauges.s.sol:Deploy --rpc-url "$RPC_URL" --broadcast --verify + +# If using BlockScout +forge script --chain "$NETWORK" script/DeployGauges.s.sol:Deploy --rpc-url "$RPC_URL" --broadcast --verify --verifier blockscout --verifier-url "https://sepolia.explorer.mode.network/api\?" +``` + +If you get the error Failed to get EIP-1559 fees, add `--legacy` to the command: + +```sh +forge script --chain "$NETWORK" script/DeployGauges.s.sol:Deploy --rpc-url "$RPC_URL" --broadcast --verify --legacy +``` + +If some contracts fail to verify on Etherscan, retry with this command: + +```sh +forge script --chain "$NETWORK" script/DeployGauges.s.sol:Deploy --rpc-url "$RPC_URL" --verify --legacy --private-key "$DEPLOYMENT_PRIVATE_KEY" --resume +``` + + ## OSx protocol overview OSx [DAO's](https://github.com/aragon/osx/blob/develop/packages/contracts/src/core/dao/DAO.sol) are designed to hold all the assets and rights by themselves. On the other hand, plugins are custom opt-in pieces of logic that can implement any type of governance. They are meant to eventually make the DAO execute a certain set of actions. @@ -308,7 +503,6 @@ Then use `make` to automatically sync the described branches into solidity test ```sh $ make Available targets: -Available targets: - make all Builds all tree files and updates the test tree markdown - make sync Scaffold or sync tree files into solidity tests - make check Checks if solidity files are out of sync diff --git a/TEST_TREE.md b/TEST_TREE.md index bf268b3..f3a233f 100644 --- a/TEST_TREE.md +++ b/TEST_TREE.md @@ -1,3 +1,563 @@ # Test tree definitions Below is the graphical definition of the contract tests implemented on [the test folder](./test) + +``` +EmergencyMultisigTest +├── Given a newly deployed contract +│ └── Given calling initialize +│ ├── It should initialize the first time +│ ├── It should refuse to initialize again +│ ├── It should set the DAO address +│ ├── It should set the minApprovals +│ ├── It should set onlyListed +│ ├── It should set signerList +│ ├── It should set proposalExpirationPeriod +│ ├── It should emit MultisigSettingsUpdated +│ ├── When minApprovals is greater than signerList length on initialize +│ │ ├── It should revert +│ │ ├── It should revert (with onlyListed false) +│ │ └── It should not revert otherwise +│ ├── When minApprovals is zero on initialize +│ │ ├── It should revert +│ │ ├── It should revert (with onlyListed false) +│ │ └── It should not revert otherwise +│ └── When signerList is invalid on initialize +│ └── It should revert +├── When calling upgradeTo +│ ├── It should revert when called without the permission +│ └── It should work when called with the permission +├── When calling upgradeToAndCall +│ ├── It should revert when called without the permission +│ └── It should work when called with the permission +├── When calling supportsInterface +│ ├── It does not support the empty interface +│ ├── It supports IERC165Upgradeable +│ ├── It supports IPlugin +│ ├── It supports IProposal +│ └── It supports IEmergencyMultisig +├── When calling updateSettings +│ ├── Given caller has no permission +│ │ ├── It should revert +│ │ └── It otherwise it should just work +│ ├── It should set the minApprovals +│ ├── It should set onlyListed +│ ├── It should set signerList +│ ├── It should set proposalExpirationPeriod +│ ├── It should emit MultisigSettingsUpdated +│ ├── When minApprovals is greater than signerList length on updateSettings +│ │ ├── It should revert +│ │ ├── It should revert (with onlyListed false) +│ │ └── It should not revert otherwise +│ ├── When minApprovals is zero on updateSettings +│ │ ├── It should revert +│ │ ├── It should revert (with onlyListed false) +│ │ └── It should not revert otherwise +│ └── When signerList is invalid on updateSettings +│ └── It should revert +├── When calling createProposal +│ ├── It increments the proposal counter +│ ├── It creates and return unique proposal IDs +│ ├── It emits the ProposalCreated event +│ ├── It creates a proposal with the given values +│ ├── Given settings changed on the same block +│ │ ├── It reverts +│ │ └── It does not revert otherwise +│ ├── Given onlyListed is false +│ │ └── It allows anyone to create +│ ├── Given onlyListed is true +│ │ ├── Given creation caller is not listed or appointed +│ │ │ ├── It reverts +│ │ │ └── It reverts if listed before but not now +│ │ ├── Given creation caller is appointed by a former signer +│ │ │ └── It reverts +│ │ ├── Given creation caller is listed and self appointed +│ │ │ └── It creates the proposal +│ │ ├── Given creation caller is listed appointing someone else now +│ │ │ └── It creates the proposal +│ │ └── Given creation caller is appointed by a current signer +│ │ └── It creates the proposal +│ ├── Given approveProposal is true +│ │ └── It creates and calls approval in one go +│ └── Given approveProposal is false +│ └── It only creates the proposal +├── When calling hashActions +│ ├── It returns the right result +│ ├── It reacts to any of the values changing +│ └── It same input produces the same output +├── Given The proposal is not created +│ ├── When calling getProposal being uncreated +│ │ └── It should return empty values +│ ├── When calling canApprove or approve being uncreated +│ │ ├── It canApprove should return false (when listed and self appointed) +│ │ ├── It approve should revert (when listed and self appointed) +│ │ ├── It canApprove should return false (when listed, appointing someone else now) +│ │ ├── It approve should revert (when listed, appointing someone else now) +│ │ ├── It canApprove should return false (when appointed by a listed signer) +│ │ ├── It approve should revert (when appointed by a listed signer) +│ │ ├── It canApprove should return false (when unlisted and unappointed) +│ │ └── It approve should revert (when unlisted and unappointed) +│ ├── When calling hasApproved being uncreated +│ │ └── It hasApproved should always return false +│ └── When calling canExecute or execute being uncreated +│ └── It canExecute should always return false +├── Given The proposal is open +│ ├── When calling getProposal being open +│ │ └── It should return the right values +│ ├── When calling canApprove or approve being open +│ │ ├── It canApprove should return true (when listed on creation, self appointed now) +│ │ ├── It approve should work (when listed on creation, self appointed now) +│ │ ├── It approve should emit an event (when listed on creation, self appointed now) +│ │ ├── It canApprove should return false (when listed on creation, appointing someone else now) +│ │ ├── It approve should revert (when listed on creation, appointing someone else now) +│ │ ├── It canApprove should return true (when currently appointed by a signer listed on creation) +│ │ ├── It approve should work (when currently appointed by a signer listed on creation) +│ │ ├── It approve should emit an event (when currently appointed by a signer listed on creation) +│ │ ├── It canApprove should return false (when unlisted on creation, unappointed now) +│ │ └── It approve should revert (when unlisted on creation, unappointed now) +│ ├── When calling hasApproved being open +│ │ └── It hasApproved should return false until approved +│ └── When calling canExecute or execute being open +│ ├── It canExecute should return false (when listed on creation, self appointed now) +│ ├── It execute should revert (when listed on creation, self appointed now) +│ ├── It canExecute should return false (when listed on creation, appointing someone else now) +│ ├── It execute should revert (when listed on creation, appointing someone else now) +│ ├── It canExecute should return false (when currently appointed by a signer listed on creation) +│ ├── It execute should revert (when currently appointed by a signer listed on creation) +│ ├── It canExecute should return false (when unlisted on creation, unappointed now) +│ └── It execute should revert (when unlisted on creation, unappointed now) +├── Given The proposal was approved by the address +│ ├── When calling getProposal being approved +│ │ └── It should return the right values +│ ├── When calling canApprove or approve being approved +│ │ ├── It canApprove should return false (when listed on creation, self appointed now) +│ │ ├── It approve should revert (when listed on creation, self appointed now) +│ │ ├── It canApprove should return false (when currently appointed by a signer listed on creation) +│ │ └── It approve should revert (when currently appointed by a signer listed on creation) +│ ├── When calling hasApproved being approved +│ │ └── It hasApproved should return false until approved +│ └── When calling canExecute or execute being approved +│ ├── It canExecute should return false (when listed on creation, self appointed now) +│ ├── It execute should revert (when listed on creation, self appointed now) +│ ├── It canExecute should return false (when currently appointed by a signer listed on creation) +│ └── It execute should revert (when currently appointed by a signer listed on creation) +├── Given The proposal passed +│ ├── When calling getProposal being passed +│ │ └── It should return the right values +│ ├── When calling canApprove or approve being passed +│ │ ├── It canApprove should return false (when listed on creation, self appointed now) +│ │ ├── It approve should revert (when listed on creation, self appointed now) +│ │ ├── It canApprove should return false (when listed on creation, appointing someone else now) +│ │ ├── It approve should revert (when listed on creation, appointing someone else now) +│ │ ├── It canApprove should return false (when currently appointed by a signer listed on creation) +│ │ ├── It approve should revert (when currently appointed by a signer listed on creation) +│ │ ├── It canApprove should return false (when unlisted on creation, unappointed now) +│ │ └── It approve should revert (when unlisted on creation, unappointed now) +│ ├── When calling hasApproved being passed +│ │ └── It hasApproved should return false until approved +│ ├── When calling canExecute or execute with modified data being passed +│ │ ├── It execute should revert with modified metadata +│ │ ├── It execute should revert with modified actions +│ │ └── It execute should work with matching data +│ ├── When calling canExecute or execute being passed +│ │ ├── It canExecute should return true, always +│ │ ├── It execute should work, when called by anyone with the actions +│ │ ├── It execute should emit an event, when called by anyone with the actions +│ │ ├── It execute recreates the proposal on the destination plugin +│ │ ├── It The parameters of the recreated proposal match the hash of the executed one +│ │ ├── It A ProposalCreated event is emitted on the destination plugin +│ │ └── It Execution is immediate on the destination plugin +│ └── Given TaikoL1 is incompatible +│ └── It executes successfully, regardless +├── Given The proposal is already executed +│ ├── When calling getProposal being executed +│ │ └── It should return the right values +│ ├── When calling canApprove or approve being executed +│ │ ├── It canApprove should return false (when listed on creation, self appointed now) +│ │ ├── It approve should revert (when listed on creation, self appointed now) +│ │ ├── It canApprove should return false (when listed on creation, appointing someone else now) +│ │ ├── It approve should revert (when listed on creation, appointing someone else now) +│ │ ├── It canApprove should return false (when currently appointed by a signer listed on creation) +│ │ ├── It approve should revert (when currently appointed by a signer listed on creation) +│ │ ├── It canApprove should return false (when unlisted on creation, unappointed now) +│ │ └── It approve should revert (when unlisted on creation, unappointed now) +│ ├── When calling hasApproved being executed +│ │ └── It hasApproved should return false until approved +│ └── When calling canExecute or execute being executed +│ ├── It canExecute should return false (when listed on creation, self appointed now) +│ ├── It execute should revert (when listed on creation, self appointed now) +│ ├── It canExecute should return false (when listed on creation, appointing someone else now) +│ ├── It execute should revert (when listed on creation, appointing someone else now) +│ ├── It canExecute should return false (when currently appointed by a signer listed on creation) +│ ├── It execute should revert (when currently appointed by a signer listed on creation) +│ ├── It canExecute should return false (when unlisted on creation, unappointed now) +│ └── It execute should revert (when unlisted on creation, unappointed now) +└── Given The proposal expired + ├── When calling getProposal being expired + │ └── It should return the right values + ├── When calling canApprove or approve being expired + │ ├── It canApprove should return false (when listed on creation, self appointed now) + │ ├── It approve should revert (when listed on creation, self appointed now) + │ ├── It canApprove should return false (when listed on creation, appointing someone else now) + │ ├── It approve should revert (when listed on creation, appointing someone else now) + │ ├── It canApprove should return false (when currently appointed by a signer listed on creation) + │ ├── It approve should revert (when currently appointed by a signer listed on creation) + │ ├── It canApprove should return false (when unlisted on creation, unappointed now) + │ └── It approve should revert (when unlisted on creation, unappointed now) + ├── When calling hasApproved being expired + │ └── It hasApproved should return false until approved + └── When calling canExecute or execute being expired + ├── It canExecute should return false (when listed on creation, self appointed now) + ├── It execute should revert (when listed on creation, self appointed now) + ├── It canExecute should return false (when listed on creation, appointing someone else now) + ├── It execute should revert (when listed on creation, appointing someone else now) + ├── It canExecute should return false (when currently appointed by a signer listed on creation) + ├── It execute should revert (when currently appointed by a signer listed on creation) + ├── It canExecute should return false (when unlisted on creation, unappointed now) + └── It execute should revert (when unlisted on creation, unappointed now) +``` + +``` +MultisigTest +├── Given a newly deployed contract +│ └── Given calling initialize +│ ├── It should initialize the first time +│ ├── It should refuse to initialize again +│ ├── It should set the DAO address +│ ├── It should set the minApprovals +│ ├── It should set onlyListed +│ ├── It should set signerList +│ ├── It should set destinationProposalDuration +│ ├── It should set proposalExpirationPeriod +│ ├── It should emit MultisigSettingsUpdated +│ ├── When minApprovals is greater than signerList length on initialize +│ │ ├── It should revert +│ │ ├── It should revert (with onlyListed false) +│ │ └── It should not revert otherwise +│ ├── When minApprovals is zero on initialize +│ │ ├── It should revert +│ │ ├── It should revert (with onlyListed false) +│ │ └── It should not revert otherwise +│ └── When signerList is invalid on initialize +│ └── It should revert +├── When calling upgradeTo +│ ├── It should revert when called without the permission +│ └── It should work when called with the permission +├── When calling upgradeToAndCall +│ ├── It should revert when called without the permission +│ └── It should work when called with the permission +├── When calling supportsInterface +│ ├── It does not support the empty interface +│ ├── It supports IERC165Upgradeable +│ ├── It supports IPlugin +│ ├── It supports IProposal +│ └── It supports IMultisig +├── When calling updateSettings +│ ├── Given caller has no permission +│ │ ├── It should revert +│ │ └── It otherwise it should just work +│ ├── It should set the minApprovals +│ ├── It should set onlyListed +│ ├── It should set signerList +│ ├── It should set destinationProposalDuration +│ ├── It should set proposalExpirationPeriod +│ ├── It should emit MultisigSettingsUpdated +│ ├── When minApprovals is greater than signerList length on updateSettings +│ │ ├── It should revert +│ │ ├── It should revert (with onlyListed false) +│ │ └── It should not revert otherwise +│ ├── When minApprovals is zero on updateSettings +│ │ ├── It should revert +│ │ ├── It should revert (with onlyListed false) +│ │ └── It should not revert otherwise +│ └── When signerList is invalid on updateSettings +│ └── It should revert +├── When calling createProposal +│ ├── It increments the proposal counter +│ ├── It creates and return unique proposal IDs +│ ├── It emits the ProposalCreated event +│ ├── It creates a proposal with the given values +│ ├── Given settings changed on the same block +│ │ ├── It reverts +│ │ └── It does not revert otherwise +│ ├── Given onlyListed is false +│ │ └── It allows anyone to create +│ ├── Given onlyListed is true +│ │ ├── Given creation caller is not listed or appointed +│ │ │ ├── It reverts +│ │ │ └── It reverts if listed before but not now +│ │ ├── Given creation caller is appointed by a former signer +│ │ │ └── It reverts +│ │ ├── Given creation caller is listed and self appointed +│ │ │ └── It creates the proposal +│ │ ├── Given creation caller is listed appointing someone else now +│ │ │ └── It creates the proposal +│ │ └── Given creation caller is appointed by a current signer +│ │ └── It creates the proposal +│ ├── Given approveProposal is true +│ │ └── It creates and calls approval in one go +│ └── Given approveProposal is false +│ └── It only creates the proposal +├── Given The proposal is not created +│ ├── When calling getProposal being uncreated +│ │ └── It should return empty values +│ ├── When calling canApprove or approve being uncreated +│ │ ├── It canApprove should return false (when listed and self appointed) +│ │ ├── It approve should revert (when listed and self appointed) +│ │ ├── It canApprove should return false (when listed, appointing someone else now) +│ │ ├── It approve should revert (when listed, appointing someone else now) +│ │ ├── It canApprove should return false (when appointed by a listed signer) +│ │ ├── It approve should revert (when appointed by a listed signer) +│ │ ├── It canApprove should return false (when unlisted and unappointed) +│ │ └── It approve should revert (when unlisted and unappointed) +│ ├── When calling hasApproved being uncreated +│ │ └── It hasApproved should always return false +│ └── When calling canExecute or execute being uncreated +│ ├── It canExecute should return false (when listed and self appointed) +│ ├── It execute should revert (when listed and self appointed) +│ ├── It canExecute should return false (when listed, appointing someone else now) +│ ├── It execute should revert (when listed, appointing someone else now) +│ ├── It canExecute should return false (when appointed by a listed signer) +│ ├── It execute should revert (when appointed by a listed signer) +│ ├── It canExecute should return false (when unlisted and unappointed) +│ └── It execute should revert (when unlisted and unappointed) +├── Given The proposal is open +│ ├── When calling getProposal being open +│ │ └── It should return the right values +│ ├── When calling canApprove or approve being open +│ │ ├── It canApprove should return true (when listed on creation, self appointed now) +│ │ ├── It approve should work (when listed on creation, self appointed now) +│ │ ├── It approve should emit an event (when listed on creation, self appointed now) +│ │ ├── It canApprove should return false (when listed on creation, appointing someone else now) +│ │ ├── It approve should revert (when listed on creation, appointing someone else now) +│ │ ├── It canApprove should return true (when currently appointed by a signer listed on creation) +│ │ ├── It approve should work (when currently appointed by a signer listed on creation) +│ │ ├── It approve should emit an event (when currently appointed by a signer listed on creation) +│ │ ├── It canApprove should return false (when unlisted on creation, unappointed now) +│ │ └── It approve should revert (when unlisted on creation, unappointed now) +│ ├── When calling approve with tryExecution and almost passed being open +│ │ ├── It approve should also execute the proposal +│ │ ├── It approve should emit an Executed event +│ │ ├── It approve recreates the proposal on the destination plugin +│ │ ├── It The parameters of the recreated proposal match those of the approved one +│ │ └── It A ProposalCreated event is emitted on the destination plugin +│ ├── When calling hasApproved being open +│ │ └── It hasApproved should return false until approved +│ └── When calling canExecute or execute being open +│ ├── It canExecute should return false (when listed on creation, self appointed now) +│ ├── It execute should revert (when listed on creation, self appointed now) +│ ├── It canExecute should return false (when listed on creation, appointing someone else now) +│ ├── It execute should revert (when listed on creation, appointing someone else now) +│ ├── It canExecute should return false (when currently appointed by a signer listed on creation) +│ ├── It execute should revert (when currently appointed by a signer listed on creation) +│ ├── It canExecute should return false (when unlisted on creation, unappointed now) +│ └── It execute should revert (when unlisted on creation, unappointed now) +├── Given The proposal was approved by the address +│ ├── When calling getProposal being approved +│ │ └── It should return the right values +│ ├── When calling canApprove or approve being approved +│ │ ├── It canApprove should return false (when listed on creation, self appointed now) +│ │ ├── It approve should revert (when listed on creation, self appointed now) +│ │ ├── It canApprove should return false (when currently appointed by a signer listed on creation) +│ │ └── It approve should revert (when currently appointed by a signer listed on creation) +│ ├── When calling hasApproved being approved +│ │ └── It hasApproved should return false until approved +│ └── When calling canExecute or execute being approved +│ ├── It canExecute should return false (when listed on creation, self appointed now) +│ ├── It execute should revert (when listed on creation, self appointed now) +│ ├── It canExecute should return false (when currently appointed by a signer listed on creation) +│ └── It execute should revert (when currently appointed by a signer listed on creation) +├── Given The proposal passed +│ ├── When calling getProposal being passed +│ │ └── It should return the right values +│ ├── When calling canApprove or approve being passed +│ │ ├── It canApprove should return false (when listed on creation, self appointed now) +│ │ ├── It approve should revert (when listed on creation, self appointed now) +│ │ ├── It canApprove should return false (when listed on creation, appointing someone else now) +│ │ ├── It approve should revert (when listed on creation, appointing someone else now) +│ │ ├── It canApprove should return false (when currently appointed by a signer listed on creation) +│ │ ├── It approve should revert (when currently appointed by a signer listed on creation) +│ │ ├── It canApprove should return false (when unlisted on creation, unappointed now) +│ │ └── It approve should revert (when unlisted on creation, unappointed now) +│ ├── When calling hasApproved being passed +│ │ └── It hasApproved should return false until approved +│ ├── When calling canExecute or execute being passed +│ │ ├── It canExecute should return true, always +│ │ ├── It execute should work, when called by anyone +│ │ ├── It execute should emit an event, when called by anyone +│ │ ├── It execute recreates the proposal on the destination plugin +│ │ ├── It The parameters of the recreated proposal match those of the executed one +│ │ ├── It The proposal duration on the destination plugin matches the multisig settings +│ │ └── It A ProposalCreated event is emitted on the destination plugin +│ └── Given TaikoL1 is incompatible +│ └── It executes successfully, regardless +├── Given The proposal is already executed +│ ├── When calling getProposal being executed +│ │ └── It should return the right values +│ ├── When calling canApprove or approve being executed +│ │ ├── It canApprove should return false (when listed on creation, self appointed now) +│ │ ├── It approve should revert (when listed on creation, self appointed now) +│ │ ├── It canApprove should return false (when listed on creation, appointing someone else now) +│ │ ├── It approve should revert (when listed on creation, appointing someone else now) +│ │ ├── It canApprove should return false (when currently appointed by a signer listed on creation) +│ │ ├── It approve should revert (when currently appointed by a signer listed on creation) +│ │ ├── It canApprove should return false (when unlisted on creation, unappointed now) +│ │ └── It approve should revert (when unlisted on creation, unappointed now) +│ ├── When calling hasApproved being executed +│ │ └── It hasApproved should return false until approved +│ └── When calling canExecute or execute being executed +│ ├── It canExecute should return false (when listed on creation, self appointed now) +│ ├── It execute should revert (when listed on creation, self appointed now) +│ ├── It canExecute should return false (when listed on creation, appointing someone else now) +│ ├── It execute should revert (when listed on creation, appointing someone else now) +│ ├── It canExecute should return false (when currently appointed by a signer listed on creation) +│ ├── It execute should revert (when currently appointed by a signer listed on creation) +│ ├── It canExecute should return false (when unlisted on creation, unappointed now) +│ └── It execute should revert (when unlisted on creation, unappointed now) +└── Given The proposal expired + ├── When calling getProposal being expired + │ └── It should return the right values + ├── When calling canApprove or approve being expired + │ ├── It canApprove should return false (when listed on creation, self appointed now) + │ ├── It approve should revert (when listed on creation, self appointed now) + │ ├── It canApprove should return false (when listed on creation, appointing someone else now) + │ ├── It approve should revert (when listed on creation, appointing someone else now) + │ ├── It canApprove should return false (when currently appointed by a signer listed on creation) + │ ├── It approve should revert (when currently appointed by a signer listed on creation) + │ ├── It canApprove should return false (when unlisted on creation, unappointed now) + │ └── It approve should revert (when unlisted on creation, unappointed now) + ├── When calling hasApproved being expired + │ └── It hasApproved should return false until approved + └── When calling canExecute or execute being expired + ├── It canExecute should return false (when listed on creation, self appointed now) + ├── It execute should revert (when listed on creation, self appointed now) + ├── It canExecute should return false (when listed on creation, appointing someone else now) + ├── It execute should revert (when listed on creation, appointing someone else now) + ├── It canExecute should return false (when currently appointed by a signer listed on creation) + ├── It execute should revert (when currently appointed by a signer listed on creation) + ├── It canExecute should return false (when unlisted on creation, unappointed now) + └── It execute should revert (when unlisted on creation, unappointed now) +``` + +``` +SignerListTest +├── When deploying the contract +│ └── It should initialize normally +├── Given a deployed contract +│ └── It should refuse to initialize again +├── Given a new instance +│ └── Given calling initialize +│ ├── It should set the DAO address +│ ├── Given passing more addresses than supported on initialize +│ │ └── It should revert +│ ├── Given duplicate addresses on initialize +│ │ └── It should revert +│ ├── It should append the new addresses to the list +│ ├── It should return true on isListed +│ ├── It should emit the SignersAdded event +│ ├── It the encryption registry should be empty +│ └── It minSignerListLength should be zero +├── When calling updateSettings +│ ├── When updateSettings without the permission +│ │ └── It should revert +│ ├── When encryptionRegistry is not compatible +│ │ └── It should revert +│ ├── When minSignerListLength is bigger than the list size +│ │ └── It should revert +│ ├── It sets the new encryption registry +│ ├── It sets the new minSignerListLength +│ └── It should emit a SignerListSettingsUpdated event +├── When calling supportsInterface +│ ├── It does not support the empty interface +│ ├── It supports IERC165Upgradeable +│ ├── It supports Addresslist +│ └── It supports ISignerList +├── When calling addSigners +│ ├── When adding without the permission +│ │ └── It should revert +│ ├── Given passing more addresses than supported on addSigners +│ │ └── It should revert +│ ├── Given duplicate addresses on addSigners +│ │ └── It should revert +│ ├── It should append the new addresses to the list +│ ├── It should return true on isListed +│ └── It should emit the SignersAdded event +├── When calling removeSigners +│ ├── When removing without the permission +│ │ └── It should revert +│ ├── When removing an unlisted address +│ │ └── It should revert +│ ├── Given removing too many addresses // The new list will be smaller than minSignerListLength +│ │ └── It should revert +│ ├── It should remove the given addresses +│ ├── It should return false on isListed +│ └── It should emit the SignersRemoved event +├── When calling isListed +│ ├── Given the member is listed +│ │ └── It returns true +│ └── Given the member is not listed +│ └── It returns false +├── When calling isListedAtBlock +│ ├── Given the member was listed +│ │ ├── Given the member is not listed now +│ │ │ └── It returns true +│ │ └── Given the member is listed now +│ │ └── It returns true +│ └── Given the member was not listed +│ ├── Given the member is delisted now +│ │ └── It returns false +│ └── Given the member is enlisted now +│ └── It returns false +├── When calling isListedOrAppointedByListed +│ ├── Given the caller is a listed signer +│ │ └── It listedOrAppointedByListed should be true +│ ├── Given the caller is appointed by a signer +│ │ └── It listedOrAppointedByListed should be true +│ └── Given the caller is not listed or appointed +│ └── It listedOrAppointedByListed should be false +├── When calling getListedEncryptionOwnerAtBlock +│ ├── Given the resolved owner is listed on getListedEncryptionOwnerAtBlock +│ │ ├── When the given address is the owner +│ │ │ └── It should return the given address +│ │ └── When the given address is appointed by the owner +│ │ └── It should return the resolved owner +│ ├── Given the resolved owner was listed on getListedEncryptionOwnerAtBlock +│ │ ├── When the given address is the owner 2 +│ │ │ └── It should return the given address +│ │ └── When the given address is appointed by the owner 2 +│ │ └── It should return the resolved owner +│ └── Given the resolved owner was not listed on getListedEncryptionOwnerAtBlock +│ └── It should return a zero value +├── When calling resolveEncryptionAccountAtBlock +│ ├── Given the resolved owner is listed on resolveEncryptionAccountAtBlock +│ │ ├── When the given address is owner +│ │ │ ├── It owner should be itself +│ │ │ └── It votingWallet should be the appointed address +│ │ └── When the given address is appointed +│ │ ├── It owner should be the resolved owner +│ │ └── It votingWallet should be the given address +│ ├── Given the resolved owner was listed on resolveEncryptionAccountAtBlock +│ │ ├── When the given address is owner 2 +│ │ │ ├── It owner should be itself +│ │ │ └── It votingWallet should be the appointed address +│ │ └── When the given address is appointed 2 +│ │ ├── It owner should be the resolved owner +│ │ └── It votingWallet should be the given address +│ └── Given the resolved owner was not listed on resolveEncryptionAccountAtBlock +│ ├── It should return a zero owner +│ └── It should return a zero appointedWallet +└── When calling getEncryptionRecipients + ├── Given the encryption registry has no accounts + │ ├── It returns an empty list, even with signers + │ └── It returns an empty list, without signers + └── Given the encryption registry has accounts + ├── Given no overlap between registry and signerList // Some are on the encryption registry only and some are on the signerList only + │ └── It returns an empty list + └── Given some addresses are registered everywhere + ├── It returns a list containing the overlapping addresses + ├── It the result has the correct resolved addresses // appointed wallets are present, not the owner + ├── It result does not contain unregistered addresses + ├── It result does not contain unlisted addresses + └── It result does not contain non appointed addresses +``` + diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 43bfaee..1ec9cff 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -21,11 +21,22 @@ contract Deploy is Script { EmergencyMultisigPluginSetup emergencyMultisigPluginSetup; OptimisticTokenVotingPluginSetup optimisticTokenVotingPluginSetup; - function run() public { - vm.startBroadcast(vm.envUint("DEPLOYMENT_PRIVATE_KEY")); + /// @dev Thrown when attempting to deploy a multisig with no members + error EmptyMultisig(); + modifier broadcast() { + uint256 privKey = vm.envUint("DEPLOYMENT_PRIVATE_KEY"); + vm.startBroadcast(privKey); + console.log("Deploying from:", vm.addr(privKey)); + + _; + + vm.stopBroadcast(); + } + + function run() public broadcast { // NOTE: Deploying the plugin setup's separately because of the code size limit - // PublicKeyRegistry and DelegationWall are deployed by the TaikoDaoFactory + // EncryptionRegistry and DelegationWall are deployed by the TaikoDaoFactory // Deploy the plugin setup's multisigPluginSetup = new MultisigPluginSetup(); @@ -36,49 +47,23 @@ contract Deploy is Script { ); console.log("Chain ID:", block.chainid); - console.log("Deploying from:", vm.addr(vm.envUint("DEPLOYMENT_PRIVATE_KEY"))); TaikoDaoFactory.DeploymentSettings memory settings; - if (vm.envBool("DEPLOY_AS_PRODUCTION")) { - settings = getProductionSettings(); + if (vm.envOr("MINT_TEST_TOKENS", false)) { + settings = getTestTokenSettings(); } else { - settings = getInternalTestingSettings(); + settings = getProductionSettings(); } console.log(""); TaikoDaoFactory factory = new TaikoDaoFactory(settings); factory.deployOnce(); - TaikoDaoFactory.Deployment memory daoDeployment = factory.getDeployment(); - address delegationWall = address(new DelegationWall()); - vm.stopBroadcast(); - - // Print summary - console.log("Factory:", address(factory)); - console.log(""); - console.log("DAO:", address(daoDeployment.dao)); - console.log("Voting token:", address(settings.tokenAddress)); - console.log("Taiko Bridge:", settings.taikoBridgeAddress); - console.log(""); - - console.log("Plugins"); - console.log("- Multisig plugin:", address(daoDeployment.multisigPlugin)); - console.log("- Emergency multisig plugin:", address(daoDeployment.emergencyMultisigPlugin)); - console.log("- Optimistic token voting plugin:", address(daoDeployment.optimisticTokenVotingPlugin)); - console.log(""); - - console.log("Plugin repositories"); - console.log("- Multisig plugin repository:", address(daoDeployment.multisigPluginRepo)); - console.log("- Emergency multisig plugin repository:", address(daoDeployment.emergencyMultisigPluginRepo)); - console.log( - "- Optimistic token voting plugin repository:", address(daoDeployment.optimisticTokenVotingPluginRepo) - ); - console.log(""); + address delegationWall = address(new DelegationWall()); - console.log("Helpers"); - console.log("- Public key registry", address(daoDeployment.publicKeyRegistry)); - console.log("- Delegation wall", address(delegationWall)); + // Done + printDeploymentSummary(factory, delegationWall); } function getProductionSettings() internal view returns (TaikoDaoFactory.DeploymentSettings memory settings) { @@ -115,8 +100,8 @@ contract Deploy is Script { }); } - function getInternalTestingSettings() internal returns (TaikoDaoFactory.DeploymentSettings memory settings) { - console.log("Using internal testing settings"); + function getTestTokenSettings() internal returns (TaikoDaoFactory.DeploymentSettings memory settings) { + console.log("Using test token settings"); address taikoBridgeAddress = address(0x1234567890); address[] memory multisigMembers = readMultisigMembers(); @@ -153,11 +138,50 @@ contract Deploy is Script { }); } - function readMultisigMembers() internal view returns (address[] memory) { + function printDeploymentSummary(TaikoDaoFactory factory, address delegationWall) internal view { + TaikoDaoFactory.DeploymentSettings memory settings = factory.getSettings(); + TaikoDaoFactory.Deployment memory daoDeployment = factory.getDeployment(); + + console.log("Factory:", address(factory)); + console.log(""); + console.log("DAO:", address(daoDeployment.dao)); + console.log("Voting token:", address(settings.tokenAddress)); + console.log("Taiko Bridge:", settings.taikoBridgeAddress); + console.log(""); + + console.log("Plugins"); + console.log("- Multisig plugin:", address(daoDeployment.multisigPlugin)); + console.log("- Emergency multisig plugin:", address(daoDeployment.emergencyMultisigPlugin)); + console.log("- Optimistic token voting plugin:", address(daoDeployment.optimisticTokenVotingPlugin)); + console.log(""); + + console.log("Helpers"); + console.log("- Signer list", address(daoDeployment.signerList)); + console.log("- Encryption registry", address(daoDeployment.encryptionRegistry)); + console.log("- Delegation wall", address(delegationWall)); + + console.log(""); + + console.log("Plugin repositories"); + console.log("- Multisig plugin repository:", address(daoDeployment.multisigPluginRepo)); + console.log("- Emergency multisig plugin repository:", address(daoDeployment.emergencyMultisigPluginRepo)); + console.log( + "- Optimistic token voting plugin repository:", address(daoDeployment.optimisticTokenVotingPluginRepo) + ); + } + + function readMultisigMembers() public view returns (address[] memory result) { // JSON list of members - string memory path = string.concat(vm.projectRoot(), "/script/multisig-members.json"); - string memory json = vm.readFile(path); - return vm.parseJsonAddressArray(json, "$.members"); + string memory membersFilePath = vm.envString("MULTISIG_MEMBERS_JSON_FILE_NAME"); + string memory path = string.concat(vm.projectRoot(), membersFilePath); + string memory strJson = vm.readFile(path); + + bool exists = vm.keyExistsJson(strJson, "$.members"); + if (!exists) revert EmptyMultisig(); + + result = vm.parseJsonAddressArray(strJson, "$.members"); + + if (result.length == 0) revert EmptyMultisig(); } function createTestToken(address[] memory members, address taikoBridge) internal returns (address) { diff --git a/script/multisig-members.json b/script/multisig-members.json index 22ec7da..88c3329 100644 --- a/script/multisig-members.json +++ b/script/multisig-members.json @@ -1,14 +1,3 @@ { - "members": [ - "0x0123456789012345678901234567890123456789", - "0x1234567890123456789012345678901234567890", - "0x2345678901234567890123456789012345678901", - "0x3456789012345678901234567890123456789012", - "0x4567890123456789012345678901234567890123", - "0x5678901234567890123456789012345678901234", - "0x6789012345678901234567890123456789012345", - "0x7890123456789012345678901234567890123456", - "0x8901234567890123456789012345678901234567", - "0x9012345678901234567890123456789012345678" - ] + "members": [] } diff --git a/src/EmergencyMultisig.sol b/src/EmergencyMultisig.sol index eabf88d..6f26bf8 100644 --- a/src/EmergencyMultisig.sol +++ b/src/EmergencyMultisig.sol @@ -5,19 +5,18 @@ pragma solidity ^0.8.17; import {SafeCastUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; -import {IMembership} from "@aragon/osx/core/plugin/membership/IMembership.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {PluginUUPSUpgradeable} from "@aragon/osx/core/plugin/PluginUUPSUpgradeable.sol"; - import {ProposalUpgradeable} from "@aragon/osx/core/plugin/proposal/ProposalUpgradeable.sol"; -import {Addresslist} from "@aragon/osx/plugins/utils/Addresslist.sol"; import {IEmergencyMultisig} from "./interfaces/IEmergencyMultisig.sol"; import {OptimisticTokenVotingPlugin} from "./OptimisticTokenVotingPlugin.sol"; -import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {SignerList} from "./SignerList.sol"; +import {ISignerList} from "./interfaces/ISignerList.sol"; /// @title Multisig - Release 1, Build 1 /// @author Aragon Association - 2022-2024 /// @notice The on-chain multisig governance plugin in which a proposal passes if X out of Y approvals are met. -contract EmergencyMultisig is IEmergencyMultisig, IMembership, PluginUUPSUpgradeable, ProposalUpgradeable { +contract EmergencyMultisig is IEmergencyMultisig, PluginUUPSUpgradeable, ProposalUpgradeable { using SafeCastUpgradeable for uint256; /// @notice A container for proposal-related information. @@ -53,12 +52,12 @@ contract EmergencyMultisig is IEmergencyMultisig, IMembership, PluginUUPSUpgrade /// @notice A container for the plugin settings. /// @param onlyListed Whether only listed addresses can create a proposal or not. /// @param minApprovals The minimal number of approvals required for a proposal to pass. - /// @param addresslistSource The contract where the list of signers is defined. + /// @param signerList The contract defining who is a member and/or who is appointed as a decryption wallet /// @param proposalExpirationPeriod The amount of seconds after which a non executed proposal expires. struct MultisigSettings { bool onlyListed; uint16 minApprovals; - Addresslist addresslistSource; + SignerList signerList; uint64 proposalExpirationPeriod; } @@ -99,15 +98,15 @@ contract EmergencyMultisig is IEmergencyMultisig, IMembership, PluginUUPSUpgrade /// @param proposalId The ID of the proposal. error InvalidMetadataUri(uint256 proposalId); + /// @notice Thrown if the SignerList contract is not compatible. + /// @param signerList The given address + error InvalidSignerList(SignerList signerList); + /// @notice Thrown if the minimal approvals value is out of bounds (less than 1 or greater than the number of members in the address list). /// @param limit The maximal value. /// @param actual The actual value. error MinApprovalsOutOfBounds(uint16 limit, uint16 actual); - /// @notice Thrown if the address list source is empty. - /// @param givenContract The received address that doesn't conform to Addresslist. - error InvalidAddressListSource(address givenContract); - /// @notice Emitted when a proposal is created. /// @param proposalId The ID of the proposal. /// @param creator The creator of the proposal. @@ -126,10 +125,10 @@ contract EmergencyMultisig is IEmergencyMultisig, IMembership, PluginUUPSUpgrade /// @notice Emitted when the plugin settings are set. /// @param onlyListed Whether only listed addresses can create a proposal. /// @param minApprovals The minimum amount of approvals needed to pass a proposal. - /// @param addresslistSource The address of the contract holding the address list to use. + /// @param signerList The contract defining who is a member and/or who is appointed as a decryption wallet /// @param proposalExpirationPeriod The amount of seconds after which a non executed proposal expires. event MultisigSettingsUpdated( - bool onlyListed, uint16 indexed minApprovals, Addresslist addresslistSource, uint64 proposalExpirationPeriod + bool onlyListed, uint16 indexed minApprovals, SignerList signerList, uint64 proposalExpirationPeriod ); /// @notice Initializes Release 1, Build 1. @@ -152,8 +151,7 @@ contract EmergencyMultisig is IEmergencyMultisig, IMembership, PluginUUPSUpgrade override(PluginUUPSUpgradeable, ProposalUpgradeable) returns (bool) { - return _interfaceId == type(IEmergencyMultisig).interfaceId || _interfaceId == type(IMembership).interfaceId - || super.supportsInterface(_interfaceId); + return _interfaceId == type(IEmergencyMultisig).interfaceId || super.supportsInterface(_interfaceId); } /// @notice Updates the plugin settings. @@ -179,8 +177,13 @@ contract EmergencyMultisig is IEmergencyMultisig, IMembership, PluginUUPSUpgrade OptimisticTokenVotingPlugin _destinationPlugin, bool _approveProposal ) external returns (uint256 proposalId) { - if (multisigSettings.onlyListed && !multisigSettings.addresslistSource.isListed(msg.sender)) { - revert ProposalCreationForbidden(msg.sender); + if (multisigSettings.onlyListed) { + bool _listedOrAppointedByListed = multisigSettings.signerList.isListedOrAppointedByListed(msg.sender); + + // Only the account or its appointed address may create proposals + if (!_listedOrAppointedByListed) { + revert ProposalCreationForbidden(msg.sender); + } } uint64 snapshotBlock; @@ -221,9 +224,9 @@ contract EmergencyMultisig is IEmergencyMultisig, IMembership, PluginUUPSUpgrade /// @inheritdoc IEmergencyMultisig function approve(uint256 _proposalId) public { - address approver = msg.sender; - if (!_canApprove(_proposalId, approver)) { - revert ApprovalCastForbidden(_proposalId, approver); + address _sender = msg.sender; + if (!_canApprove(_proposalId, _sender)) { + revert ApprovalCastForbidden(_proposalId, _sender); } Proposal storage proposal_ = proposals[_proposalId]; @@ -234,9 +237,13 @@ contract EmergencyMultisig is IEmergencyMultisig, IMembership, PluginUUPSUpgrade proposal_.approvals += 1; } - proposal_.approvers[approver] = true; + // Register the approval as being made by the owner + address _owner = + multisigSettings.signerList.getListedEncryptionOwnerAtBlock(_sender, proposal_.parameters.snapshotBlock); + proposal_.approvers[_owner] = true; - emit Approved({proposalId: _proposalId, approver: approver}); + // We emit the event as the owner's approval + emit Approved({proposalId: _proposalId, approver: _owner}); // Automatic execution is intentionally omitted in order to prevent // private actions from accidentally leaving the local computer before being executed @@ -287,7 +294,11 @@ contract EmergencyMultisig is IEmergencyMultisig, IMembership, PluginUUPSUpgrade /// @inheritdoc IEmergencyMultisig function hasApproved(uint256 _proposalId, address _account) public view returns (bool) { - return proposals[_proposalId].approvers[_account]; + Proposal storage proposal_ = proposals[_proposalId]; + address _owner = + multisigSettings.signerList.getListedEncryptionOwnerAtBlock(_account, proposal_.parameters.snapshotBlock); + + return proposals[_proposalId].approvers[_owner]; } /// @inheritdoc IEmergencyMultisig @@ -316,11 +327,6 @@ contract EmergencyMultisig is IEmergencyMultisig, IMembership, PluginUUPSUpgrade actionsHash = keccak256(abi.encode(_actions)); } - /// @inheritdoc IMembership - function isMember(address _account) external view returns (bool) { - return multisigSettings.addresslistSource.isListed(_account); - } - /// @notice Internal function to execute a vote. It assumes the queried proposal exists. /// @param _proposalId The ID of the proposal. function _execute(uint256 _proposalId, bytes memory _metadataUri, IDAO.Action[] calldata _actions) internal { @@ -339,9 +345,9 @@ contract EmergencyMultisig is IEmergencyMultisig, IMembership, PluginUUPSUpgrade /// @notice Internal function to check if an account can approve. It assumes the queried proposal exists. /// @param _proposalId The ID of the proposal. - /// @param _account The account to check. + /// @param _approver The account to check. /// @return Returns `true` if the given account can approve on a certain proposal and `false` otherwise. - function _canApprove(uint256 _proposalId, address _account) internal view returns (bool) { + function _canApprove(uint256 _proposalId, address _approver) internal view returns (bool) { Proposal storage proposal_ = proposals[_proposalId]; if (!_isProposalOpen(proposal_)) { @@ -349,13 +355,20 @@ contract EmergencyMultisig is IEmergencyMultisig, IMembership, PluginUUPSUpgrade return false; } - if (!multisigSettings.addresslistSource.isListedAtBlock(_account, proposal_.parameters.snapshotBlock)) { - // The approver has no voting power. + // This internally calls `isListedAtBlock`. + // If not listed or resolved, it returns address(0) + (address _resolvedOwner, address _resolvedVoter) = + multisigSettings.signerList.resolveEncryptionAccountAtBlock(_approver, proposal_.parameters.snapshotBlock); + if (_resolvedOwner == address(0) || _resolvedVoter == address(0)) { + // Not listedAtBlock() nor appointed by a listed owner + return false; + } else if (_approver != _resolvedVoter) { + // Only the voter account can vote (owners who appointed, can't) return false; } - if (proposal_.approvers[_account]) { - // The approver has already approved + if (proposal_.approvers[_resolvedOwner]) { + // The account already approved return false; } @@ -387,16 +400,16 @@ contract EmergencyMultisig is IEmergencyMultisig, IMembership, PluginUUPSUpgrade /// @notice Internal function to update the plugin settings. /// @param _multisigSettings The new settings. function _updateMultisigSettings(MultisigSettings calldata _multisigSettings) internal { - if (!IERC165(address(_multisigSettings.addresslistSource)).supportsInterface(type(Addresslist).interfaceId)) { - revert InvalidAddressListSource(address(_multisigSettings.addresslistSource)); - } else if (_multisigSettings.minApprovals < 1) { - revert MinApprovalsOutOfBounds({limit: 1, actual: _multisigSettings.minApprovals}); + if (!IERC165(address(_multisigSettings.signerList)).supportsInterface(type(ISignerList).interfaceId)) { + revert InvalidSignerList(_multisigSettings.signerList); } - uint16 addresslistLength_ = uint16(_multisigSettings.addresslistSource.addresslistLength()); + uint16 addresslistLength_ = uint16(_multisigSettings.signerList.addresslistLength()); if (_multisigSettings.minApprovals > addresslistLength_) { revert MinApprovalsOutOfBounds({limit: addresslistLength_, actual: _multisigSettings.minApprovals}); + } else if (_multisigSettings.minApprovals < 1) { + revert MinApprovalsOutOfBounds({limit: 1, actual: _multisigSettings.minApprovals}); } multisigSettings = _multisigSettings; @@ -405,8 +418,8 @@ contract EmergencyMultisig is IEmergencyMultisig, IMembership, PluginUUPSUpgrade emit MultisigSettingsUpdated({ onlyListed: _multisigSettings.onlyListed, minApprovals: _multisigSettings.minApprovals, - addresslistSource: _multisigSettings.addresslistSource, - proposalExpirationPeriod: _multisigSettings.proposalExpirationPeriod + proposalExpirationPeriod: _multisigSettings.proposalExpirationPeriod, + signerList: _multisigSettings.signerList }); } diff --git a/src/EncryptionRegistry.sol b/src/EncryptionRegistry.sol new file mode 100644 index 0000000..58e9b79 --- /dev/null +++ b/src/EncryptionRegistry.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.17; + +import {Addresslist} from "@aragon/osx/plugins/utils/Addresslist.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {IEncryptionRegistry} from "./interfaces/IEncryptionRegistry.sol"; + +/// @title EncryptionRegistry - Release 1, Build 1 +/// @author Aragon Association - 2024 +/// @notice A smart contract where accounts can register their libsodium public key for encryption purposes, as well as appointing an EOA +contract EncryptionRegistry is IEncryptionRegistry, ERC165 { + struct AccountEntry { + address appointedWallet; + bytes32 publicKey; + } + + /// @notice Allows to enumerate the addresses on the registry + address[] public registeredAccounts; + + /// @notice The database of appointed wallets and their public key + mapping(address => AccountEntry) public accounts; + + /// @notice A reference to the account that appointed each wallet + mapping(address => address) public appointerOf; + + /// @dev The contract to check whether the caller is a multisig member + Addresslist addresslist; + + constructor(Addresslist _addresslist) { + if (!IERC165(address(_addresslist)).supportsInterface(type(Addresslist).interfaceId)) { + revert InvalidAddressList(); + } + + addresslist = _addresslist; + } + + /// @inheritdoc IEncryptionRegistry + function appointWallet(address _newWallet) public { + // Appointing ourselves is the same as unappointing + if (_newWallet == msg.sender) _newWallet = address(0); + + if (!addresslist.isListed(msg.sender)) { + revert MustBeListed(); + } else if (Address.isContract(_newWallet)) { + revert CannotAppointContracts(); + } else if (addresslist.isListed(_newWallet)) { + // Appointing an already listed signer is not allowed, as votes would be locked + revert AlreadyListed(); + } else if (_newWallet == accounts[msg.sender].appointedWallet) { + return; // done + } else if (appointerOf[_newWallet] != address(0)) { + revert AlreadyAppointed(); + } + + bool exists; + for (uint256 i = 0; i < registeredAccounts.length;) { + if (registeredAccounts[i] == msg.sender) { + exists = true; + break; + } + unchecked { + i++; + } + } + + // New account? + if (!exists) { + registeredAccounts.push(msg.sender); + } + // Existing account + else { + // Clear the current appointerOf[], if needed + if (accounts[msg.sender].appointedWallet != address(0)) { + appointerOf[accounts[msg.sender].appointedWallet] = address(0); + } + // Clear the current public key, if needed + if (accounts[msg.sender].publicKey != bytes32(0)) { + // The old appointed wallet should no longer be able to see new content + accounts[msg.sender].publicKey = bytes32(0); + } + } + + accounts[msg.sender].appointedWallet = _newWallet; + if (_newWallet != address(0)) { + appointerOf[_newWallet] = msg.sender; + } + emit WalletAppointed(msg.sender, _newWallet); + } + + /// @inheritdoc IEncryptionRegistry + function setOwnPublicKey(bytes32 _publicKey) public { + if (!addresslist.isListed(msg.sender)) { + revert MustBeListed(); + } + // If someone else if appointed, the public key cannot be overriden. + // The appointed value should be set to address(0) or msg.sender first. + else if ( + accounts[msg.sender].appointedWallet != address(0) && accounts[msg.sender].appointedWallet != msg.sender + ) { + revert MustResetAppointment(); + } + + _setPublicKey(msg.sender, _publicKey); + emit PublicKeySet(msg.sender, _publicKey); + } + + /// @inheritdoc IEncryptionRegistry + function setPublicKey(address _accountOwner, bytes32 _publicKey) public { + if (!addresslist.isListed(_accountOwner)) { + revert MustBeListed(); + } else if (accounts[_accountOwner].appointedWallet != msg.sender) { + revert MustBeAppointed(); + } + + _setPublicKey(_accountOwner, _publicKey); + emit PublicKeySet(_accountOwner, _publicKey); + } + + /// @inheritdoc IEncryptionRegistry + function getRegisteredAccounts() public view returns (address[] memory) { + return registeredAccounts; + } + + /// @inheritdoc IEncryptionRegistry + function getAppointedWallet(address _member) public view returns (address) { + if (accounts[_member].appointedWallet != address(0)) { + return accounts[_member].appointedWallet; + } + return _member; + } + + /// @notice Checks if this or the parent contract supports an interface by its ID. + /// @param _interfaceId The ID of the interface. + /// @return Returns `true` if the interface is supported. + function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { + return _interfaceId == type(IEncryptionRegistry).interfaceId || super.supportsInterface(_interfaceId); + } + + // Internal helpers + + function _setPublicKey(address _account, bytes32 _publicKey) internal { + bool exists; + for (uint256 i = 0; i < registeredAccounts.length;) { + if (registeredAccounts[i] == _account) { + exists = true; + break; + } + unchecked { + i++; + } + } + if (!exists) { + // New account + registeredAccounts.push(_account); + } + + accounts[_account].publicKey = _publicKey; + } +} diff --git a/src/Multisig.sol b/src/Multisig.sol index c19c8dc..27c529e 100644 --- a/src/Multisig.sol +++ b/src/Multisig.sol @@ -9,14 +9,16 @@ import {IMembership} from "@aragon/osx/core/plugin/membership/IMembership.sol"; import {PluginUUPSUpgradeable} from "@aragon/osx/core/plugin/PluginUUPSUpgradeable.sol"; import {ProposalUpgradeable} from "@aragon/osx/core/plugin/proposal/ProposalUpgradeable.sol"; -import {Addresslist} from "@aragon/osx/plugins/utils/Addresslist.sol"; import {IMultisig} from "./interfaces/IMultisig.sol"; +import {SignerList} from "./SignerList.sol"; +import {ISignerList} from "./interfaces/ISignerList.sol"; import {OptimisticTokenVotingPlugin} from "./OptimisticTokenVotingPlugin.sol"; +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; /// @title Multisig - Release 1, Build 1 /// @author Aragon Association - 2022-2024 /// @notice The on-chain multisig governance plugin in which a proposal passes if X out of Y approvals are met. -contract Multisig is IMultisig, IMembership, PluginUUPSUpgradeable, ProposalUpgradeable, Addresslist { +contract Multisig is IMultisig, PluginUUPSUpgradeable, ProposalUpgradeable { using SafeCastUpgradeable for uint256; /// @notice A container for proposal-related information. @@ -50,12 +52,15 @@ contract Multisig is IMultisig, IMembership, PluginUUPSUpgradeable, ProposalUpgr /// @notice A container for the plugin settings. /// @param onlyListed Whether only listed addresses can create a proposal or not. /// @param minApprovals The minimal number of approvals required for a proposal to pass. + /// @param addresslistSource The contract where the list of signers is defined. /// @param destinationProposalDuration The minimum duration that the destination plugin will enforce. + /// @param signerList The contract defining who is a member and/or who is appointed as a decryption wallet /// @param proposalExpirationPeriod The amount of seconds after which a non executed proposal expires. struct MultisigSettings { bool onlyListed; uint16 minApprovals; uint64 destinationProposalDuration; + SignerList signerList; uint64 proposalExpirationPeriod; } @@ -88,15 +93,18 @@ contract Multisig is IMultisig, IMembership, PluginUUPSUpgradeable, ProposalUpgr /// @param proposalId The ID of the proposal. error ProposalExecutionForbidden(uint256 proposalId); + /// @notice Thrown if the SignerList contract is not compatible. + /// @param signerList The given address + error InvalidSignerList(SignerList signerList); + /// @notice Thrown if the minimal approvals value is out of bounds (less than 1 or greater than the number of members in the address list). /// @param limit The maximal value. /// @param actual The actual value. error MinApprovalsOutOfBounds(uint16 limit, uint16 actual); - /// @notice Thrown if the address list length is out of bounds. - /// @param limit The limit value. - /// @param actual The actual value. - error AddresslistLengthOutOfBounds(uint16 limit, uint256 actual); + /// @notice Thrown if the address list source is empty. + /// @param givenContract The received address that doesn't conform to Addresslist. + error InvalidAddressListSource(address givenContract); /// @notice Emitted when a proposal is approve by an approver. /// @param proposalId The ID of the proposal. @@ -110,33 +118,24 @@ contract Multisig is IMultisig, IMembership, PluginUUPSUpgradeable, ProposalUpgr /// @notice Emitted when the plugin settings are set. /// @param onlyListed Whether only listed addresses can create a proposal. /// @param minApprovals The minimum amount of approvals needed to pass a proposal. + /// @param signerList The contract defining who is a member and/or who is appointed as a decryption wallet /// @param destinationProposalDuration The minimum duration (in seconds) that will be required on the destination plugin /// @param proposalExpirationPeriod The amount of seconds after which a non executed proposal expires. event MultisigSettingsUpdated( bool onlyListed, uint16 indexed minApprovals, uint64 destinationProposalDuration, + SignerList signerList, uint64 proposalExpirationPeriod ); /// @notice Initializes Release 1, Build 1. /// @dev This method is required to support [ERC-1822](https://eips.ethereum.org/EIPS/eip-1822). /// @param _dao The IDAO interface of the associated DAO. - /// @param _members The addresses of the initial members to be added. /// @param _multisigSettings The multisig settings. - function initialize(IDAO _dao, address[] calldata _members, MultisigSettings calldata _multisigSettings) - external - initializer - { + function initialize(IDAO _dao, MultisigSettings calldata _multisigSettings) external initializer { __PluginUUPSUpgradeable_init(_dao); - if (_members.length > type(uint16).max) { - revert AddresslistLengthOutOfBounds({limit: type(uint16).max, actual: _members.length}); - } - - _addAddresses(_members); - emit MembersAdded({members: _members}); - _updateMultisigSettings(_multisigSettings); } @@ -150,36 +149,8 @@ contract Multisig is IMultisig, IMembership, PluginUUPSUpgradeable, ProposalUpgr override(PluginUUPSUpgradeable, ProposalUpgradeable) returns (bool) { - return _interfaceId == type(IMultisig).interfaceId || _interfaceId == type(Addresslist).interfaceId - || _interfaceId == type(IMembership).interfaceId || super.supportsInterface(_interfaceId); - } - - /// @inheritdoc IMultisig - function addAddresses(address[] calldata _members) external auth(UPDATE_MULTISIG_SETTINGS_PERMISSION_ID) { - uint256 newAddresslistLength = addresslistLength() + _members.length; - - // Check if the new address list length would be greater than `type(uint16).max`, the maximal number of approvals. - if (newAddresslistLength > type(uint16).max) { - revert AddresslistLengthOutOfBounds({limit: type(uint16).max, actual: newAddresslistLength}); - } - - _addAddresses(_members); - - emit MembersAdded({members: _members}); - } - - /// @inheritdoc IMultisig - function removeAddresses(address[] calldata _members) external auth(UPDATE_MULTISIG_SETTINGS_PERMISSION_ID) { - uint16 newAddresslistLength = uint16(addresslistLength() - _members.length); - - // Check if the new address list length would become less than the current minimum number of approvals required. - if (newAddresslistLength < multisigSettings.minApprovals) { - revert MinApprovalsOutOfBounds({limit: multisigSettings.minApprovals, actual: newAddresslistLength}); - } - - _removeAddresses(_members); - - emit MembersRemoved({members: _members}); + return _interfaceId == type(IMultisig).interfaceId || _interfaceId == type(IMembership).interfaceId + || super.supportsInterface(_interfaceId); } /// @notice Updates the plugin settings. @@ -203,8 +174,13 @@ contract Multisig is IMultisig, IMembership, PluginUUPSUpgradeable, ProposalUpgr OptimisticTokenVotingPlugin _destinationPlugin, bool _approveProposal ) external returns (uint256 proposalId) { - if (multisigSettings.onlyListed && !isListed(msg.sender)) { - revert ProposalCreationForbidden(msg.sender); + if (multisigSettings.onlyListed) { + bool _listedOrAppointedByListed = multisigSettings.signerList.isListedOrAppointedByListed(msg.sender); + + // Only the account or its appointed address may create proposals + if (!_listedOrAppointedByListed) { + revert ProposalCreationForbidden(msg.sender); + } } uint64 snapshotBlock; @@ -219,6 +195,7 @@ contract Multisig is IMultisig, IMembership, PluginUUPSUpgradeable, ProposalUpgr } uint64 _expirationDate = block.timestamp.toUint64() + multisigSettings.proposalExpirationPeriod; + proposalId = _createProposal({ _creator: msg.sender, _metadata: _metadataURI, @@ -251,9 +228,9 @@ contract Multisig is IMultisig, IMembership, PluginUUPSUpgradeable, ProposalUpgr /// @inheritdoc IMultisig function approve(uint256 _proposalId, bool _tryExecution) public { - address approver = msg.sender; - if (!_canApprove(_proposalId, approver)) { - revert ApprovalCastForbidden(_proposalId, approver); + address _sender = msg.sender; + if (!_canApprove(_proposalId, _sender)) { + revert ApprovalCastForbidden(_proposalId, _sender); } Proposal storage proposal_ = proposals[_proposalId]; @@ -264,9 +241,13 @@ contract Multisig is IMultisig, IMembership, PluginUUPSUpgradeable, ProposalUpgr proposal_.approvals += 1; } - proposal_.approvers[approver] = true; + // Register the approval as being made by the owner. isListedAtBlock() relates to it + address _owner = + multisigSettings.signerList.getListedEncryptionOwnerAtBlock(_sender, proposal_.parameters.snapshotBlock); + proposal_.approvers[_owner] = true; - emit Approved({proposalId: _proposalId, approver: approver}); + // We emit the event as the owner's approval + emit Approved({proposalId: _proposalId, approver: _owner}); if (_tryExecution && _canExecute(_proposalId)) { _execute(_proposalId); @@ -315,7 +296,11 @@ contract Multisig is IMultisig, IMembership, PluginUUPSUpgradeable, ProposalUpgr /// @inheritdoc IMultisig function hasApproved(uint256 _proposalId, address _account) public view returns (bool) { - return proposals[_proposalId].approvers[_account]; + Proposal storage proposal_ = proposals[_proposalId]; + address _owner = + multisigSettings.signerList.getListedEncryptionOwnerAtBlock(_account, proposal_.parameters.snapshotBlock); + + return proposals[_proposalId].approvers[_owner]; } /// @inheritdoc IMultisig @@ -327,11 +312,6 @@ contract Multisig is IMultisig, IMembership, PluginUUPSUpgradeable, ProposalUpgr _execute(_proposalId); } - /// @inheritdoc IMembership - function isMember(address _account) external view returns (bool) { - return isListed(_account); - } - /// @notice Internal function to execute a vote. It assumes the queried proposal exists. /// @param _proposalId The ID of the proposal. function _execute(uint256 _proposalId) internal { @@ -350,9 +330,9 @@ contract Multisig is IMultisig, IMembership, PluginUUPSUpgradeable, ProposalUpgr /// @notice Internal function to check if an account can approve. It assumes the queried proposal exists. /// @param _proposalId The ID of the proposal. - /// @param _account The account to check. + /// @param _approver The account to check. /// @return Returns `true` if the given account can approve on a certain proposal and `false` otherwise. - function _canApprove(uint256 _proposalId, address _account) internal view returns (bool) { + function _canApprove(uint256 _proposalId, address _approver) internal view returns (bool) { Proposal storage proposal_ = proposals[_proposalId]; if (!_isProposalOpen(proposal_)) { @@ -360,13 +340,20 @@ contract Multisig is IMultisig, IMembership, PluginUUPSUpgradeable, ProposalUpgr return false; } - if (!isListedAtBlock(_account, proposal_.parameters.snapshotBlock)) { - // The approver has no voting power. + // This internally calls `isListedAtBlock`. + // If not listed or resolved, it returns address(0) + (address _resolvedOwner, address _resolvedVoter) = + multisigSettings.signerList.resolveEncryptionAccountAtBlock(_approver, proposal_.parameters.snapshotBlock); + if (_resolvedOwner == address(0) || _resolvedVoter == address(0)) { + // Not listedAtBlock() nor appointed by a listed owner + return false; + } else if (_approver != _resolvedVoter) { + // Only the voter account can vote (owners who appointed, can't) return false; } - if (proposal_.approvers[_account]) { - // The approver has already approved + if (proposal_.approvers[_resolvedOwner]) { + // The account already approved return false; } @@ -398,13 +385,15 @@ contract Multisig is IMultisig, IMembership, PluginUUPSUpgradeable, ProposalUpgr /// @notice Internal function to update the plugin settings. /// @param _multisigSettings The new settings. function _updateMultisigSettings(MultisigSettings calldata _multisigSettings) internal { - uint16 addresslistLength_ = uint16(addresslistLength()); + if (!IERC165(address(_multisigSettings.signerList)).supportsInterface(type(ISignerList).interfaceId)) { + revert InvalidSignerList(_multisigSettings.signerList); + } + + uint16 addresslistLength_ = uint16(_multisigSettings.signerList.addresslistLength()); if (_multisigSettings.minApprovals > addresslistLength_) { revert MinApprovalsOutOfBounds({limit: addresslistLength_, actual: _multisigSettings.minApprovals}); - } - - if (_multisigSettings.minApprovals < 1) { + } else if (_multisigSettings.minApprovals < 1) { revert MinApprovalsOutOfBounds({limit: 1, actual: _multisigSettings.minApprovals}); } @@ -414,6 +403,7 @@ contract Multisig is IMultisig, IMembership, PluginUUPSUpgradeable, ProposalUpgr emit MultisigSettingsUpdated({ onlyListed: _multisigSettings.onlyListed, minApprovals: _multisigSettings.minApprovals, + signerList: _multisigSettings.signerList, destinationProposalDuration: _multisigSettings.destinationProposalDuration, proposalExpirationPeriod: _multisigSettings.proposalExpirationPeriod }); @@ -422,5 +412,5 @@ contract Multisig is IMultisig, IMembership, PluginUUPSUpgradeable, ProposalUpgr /// @dev This empty reserved space is put in place to allow future versions to add new /// variables without shifting down storage in the inheritance chain. /// https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps - uint256[47] private __gap; + uint256[46] private __gap; } diff --git a/src/PublicKeyRegistry.sol b/src/PublicKeyRegistry.sol deleted file mode 100644 index 25ecdbd..0000000 --- a/src/PublicKeyRegistry.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -pragma solidity ^0.8.17; - -import {Addresslist} from "@aragon/osx/plugins/utils/Addresslist.sol"; -import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; - -/// @title PublicKeyRegistry - Release 1, Build 1 -/// @author Aragon Association - 2024 -/// @notice A smart contract where any wallet can register its own libsodium public key for encryption purposes -contract PublicKeyRegistry { - mapping(address => bytes32) public publicKeys; - - /// @dev Allows to enumerate the wallets that have a public key registered - address[] public registeredWallets; - - /// @dev The contract to check whether the caller is a multisig member - Addresslist addresslistSource; - - /// @notice Emitted when a public key is registered - event PublicKeyRegistered(address wallet, bytes32 publicKey); - - /// @notice Raised when the public key of the given user has already been set - error AlreadySet(); - - /// @notice Raised when the caller is not a multisig member - error RegistrationForbidden(); - - /// @notice Raised when the caller is not a multisig member - error InvalidAddressList(); - - constructor(Addresslist _addresslistSource) { - if (!IERC165(address(_addresslistSource)).supportsInterface(type(Addresslist).interfaceId)) { - revert InvalidAddressList(); - } - - addresslistSource = _addresslistSource; - } - - function setPublicKey(bytes32 _publicKey) public { - if (publicKeys[msg.sender] != 0) revert AlreadySet(); - else if (!addresslistSource.isListed(msg.sender)) revert RegistrationForbidden(); - - publicKeys[msg.sender] = _publicKey; - emit PublicKeyRegistered(msg.sender, _publicKey); - registeredWallets.push(msg.sender); - } - - /// @notice Returns the list of wallets that have registered a public key - /// @dev Use this function to get all addresses in a single call. You can still call registeredWallets[idx] to resolve them one by one. - function getRegisteredWallets() public view returns (address[] memory) { - return registeredWallets; - } - - /// @notice Returns the number of publicKey entries available - function registeredWalletCount() public view returns (uint256) { - return registeredWallets.length; - } -} diff --git a/src/SignerList.sol b/src/SignerList.sol new file mode 100644 index 0000000..1904e1c --- /dev/null +++ b/src/SignerList.sol @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.17; + +import {Addresslist} from "@aragon/osx/plugins/utils/Addresslist.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {ISignerList} from "./interfaces/ISignerList.sol"; +import {EncryptionRegistry} from "./EncryptionRegistry.sol"; +import {DaoAuthorizableUpgradeable} from "@aragon/osx/core/plugin/dao-authorizable/DaoAuthorizableUpgradeable.sol"; +import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; +import {IEncryptionRegistry} from "./interfaces/IEncryptionRegistry.sol"; + +// ID of the permission required to call the `addAddresses` and `removeAddresses` functions. +bytes32 constant UPDATE_SIGNER_LIST_PERMISSION_ID = keccak256("UPDATE_SIGNER_LIST_PERMISSION"); + +// ID of the permission required to update the SignerList settings. +bytes32 constant UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID = keccak256("UPDATE_SIGNER_LIST_SETTINGS_PERMISSION"); + +/// @title SignerList - Release 1, Build 1 +/// @author Aragon Association - 2024 +/// @notice A smart contract acting as the source of truth for multisig censuses, as well as defining who is appointed as an EOA for decryption purposes. +contract SignerList is ISignerList, Addresslist, ERC165Upgradeable, DaoAuthorizableUpgradeable { + /// @notice Thrown if the signer list length is out of bounds. + /// @param limit The limit value. + /// @param actual The actual value. + error SignerListLengthOutOfBounds(uint16 limit, uint256 actual); + + /// @notice Thrown when attempting to define an invalid EncryptionRegistry + error InvalidEncryptionRegitry(address givenAddress); + + /// @notice Emitted when the SignerList settings are updated + event SignerListSettingsUpdated(EncryptionRegistry encryptionRegistry, uint16 minSignerListLength); + + struct Settings { + /// @notice The contract where current signers can appoint wallets for decryption purposes + EncryptionRegistry encryptionRegistry; + /// @notice The minimum amount of addresses required. + /// @notice Set this value to at least the `minApprovals` of the EmergencyMultisig contract. + uint16 minSignerListLength; + } + + Settings public settings; + + /// @notice Disables the initializers on the implementation contract to prevent it from being left uninitialized. + constructor() { + _disableInitializers(); + } + + /// @notice Initializes Release 1, Build 1 without any settings yet. + /// @dev This method is required to support [ERC-1822](https://eips.ethereum.org/EIPS/eip-1822). + /// @dev updateSettings() must be called after the EncryptionRegistry has been deployed. + /// @param _dao The IDAO interface of the associated DAO. + /// @param _signers The addresses of the initial signers to be added. + function initialize(IDAO _dao, address[] calldata _signers) external initializer { + __DaoAuthorizableUpgradeable_init(_dao); + + // Validating _signers[] + if (_signers.length > type(uint16).max) { + revert SignerListLengthOutOfBounds({limit: type(uint16).max, actual: _signers.length}); + } + + _addAddresses(_signers); + emit SignersAdded({signers: _signers}); + } + + /// @inheritdoc ISignerList + function addSigners(address[] calldata _signers) external auth(UPDATE_SIGNER_LIST_PERMISSION_ID) { + uint256 newAddresslistLength = addresslistLength() + _signers.length; + + // Check if the new address list length would be greater than `type(uint16).max`, the maximal number of approvals. + if (newAddresslistLength > type(uint16).max) { + revert SignerListLengthOutOfBounds({limit: type(uint16).max, actual: newAddresslistLength}); + } + + _addAddresses(_signers); + emit SignersAdded({signers: _signers}); + } + + /// @inheritdoc ISignerList + function removeSigners(address[] calldata _signers) external auth(UPDATE_SIGNER_LIST_PERMISSION_ID) { + uint16 newAddresslistLength = uint16(addresslistLength() - _signers.length); + + // Check if the new address list length would become less than the current minimum number of approvals required. + if (newAddresslistLength < settings.minSignerListLength) { + revert SignerListLengthOutOfBounds({limit: settings.minSignerListLength, actual: newAddresslistLength}); + } + + _removeAddresses(_signers); + emit SignersRemoved({signers: _signers}); + } + + /// @notice Updates the plugin settings. + /// @param _newSettings The new settings. + function updateSettings(Settings calldata _newSettings) external auth(UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID) { + // Values validated within _updateSettings + _updateSettings(_newSettings); + + emit SignerListSettingsUpdated({ + encryptionRegistry: _newSettings.encryptionRegistry, + minSignerListLength: _newSettings.minSignerListLength + }); + } + + /// @inheritdoc ISignerList + function isListedOrAppointedByListed(address _address) public view returns (bool listedOrAppointedByListed) { + if (isListed(_address)) { + return true; + } else if (isListed(settings.encryptionRegistry.appointerOf(_address))) { + return true; + } + + // Not found, return blank (false) + } + + /// @inheritdoc ISignerList + function getListedEncryptionOwnerAtBlock(address _address, uint256 _blockNumber) + public + view + returns (address _owner) + { + if (isListedAtBlock(_address, _blockNumber)) { + return _address; + } + address _appointer = settings.encryptionRegistry.appointerOf(_address); + if (isListedAtBlock(_appointer, _blockNumber)) { + return _appointer; + } + + // Not found, return a blank address + } + + /// @inheritdoc ISignerList + function resolveEncryptionAccountAtBlock(address _address, uint256 _blockNumber) + public + view + returns (address _owner, address _voter) + { + if (isListedAtBlock(_address, _blockNumber)) { + // The owner + the voter + return (_address, settings.encryptionRegistry.getAppointedWallet(_address)); + } + + address _appointer = settings.encryptionRegistry.appointerOf(_address); + if (this.isListedAtBlock(_appointer, _blockNumber)) { + // The appointed wallet votes + return (_appointer, _address); + } + + // Not found, returning empty addresses + } + + /// @inheritdoc ISignerList + function getEncryptionRecipients() external view returns (address[] memory result) { + address[] memory _encryptionAccounts = settings.encryptionRegistry.getRegisteredAccounts(); + + // Allocating the full length. + // If any member is no longer listed, the size will be decreased. + result = new address[](_encryptionAccounts.length); + + uint256 rIdx; // Result iterator. Will never be greater than erIdx. + uint256 erIdx; // EncryptionRegistry iterator + address appointed; + for (erIdx = 0; erIdx < _encryptionAccounts.length;) { + if (isListed(_encryptionAccounts[erIdx])) { + // Add it to the result array if listed + appointed = settings.encryptionRegistry.getAppointedWallet(_encryptionAccounts[erIdx]); + // Use the appointed address if non-zero + if (appointed != address(0)) { + result[rIdx] = appointed; + } else { + result[rIdx] = _encryptionAccounts[erIdx]; + } + + unchecked { + rIdx++; + } + } + // Skip non-listed accounts othersise + + unchecked { + erIdx++; + } + } + + if (rIdx < erIdx) { + // Decrease the array size to return listed accounts without blank entries + uint256 diff = erIdx - rIdx; + assembly { + mstore(result, sub(mload(result), diff)) + } + } + } + + /// @notice Checks if this or the parent contract supports an interface by its ID. + /// @param _interfaceId The ID of the interface. + /// @return Returns `true` if the interface is supported. + function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { + return _interfaceId == type(ISignerList).interfaceId || _interfaceId == type(Addresslist).interfaceId + || super.supportsInterface(_interfaceId); + } + + // Internal helpers + + /// @notice Internal function to update the plugin settings. + /// @param _newSettings The new settings. + function _updateSettings(Settings calldata _newSettings) internal { + // Avoid writing if not needed + if ( + _newSettings.encryptionRegistry == settings.encryptionRegistry + && _newSettings.minSignerListLength == settings.minSignerListLength + ) { + return; + } else if (!_newSettings.encryptionRegistry.supportsInterface(type(IEncryptionRegistry).interfaceId)) { + revert InvalidEncryptionRegitry(address(_newSettings.encryptionRegistry)); + } + + uint16 _currentLength = uint16(addresslistLength()); + if (_newSettings.minSignerListLength > _currentLength) { + revert SignerListLengthOutOfBounds({limit: _currentLength, actual: _newSettings.minSignerListLength}); + } + + settings = _newSettings; + } +} diff --git a/src/factory/TaikoDaoFactory.sol b/src/factory/TaikoDaoFactory.sol index 00f2f06..b5733fa 100644 --- a/src/factory/TaikoDaoFactory.sol +++ b/src/factory/TaikoDaoFactory.sol @@ -5,7 +5,8 @@ import {DAO} from "@aragon/osx/core/dao/DAO.sol"; import {DAOFactory} from "@aragon/osx/framework/dao/DAOFactory.sol"; import {Multisig} from "../Multisig.sol"; import {EmergencyMultisig} from "../EmergencyMultisig.sol"; -import {PublicKeyRegistry} from "../PublicKeyRegistry.sol"; +import {SignerList, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID} from "../SignerList.sol"; +import {EncryptionRegistry} from "../EncryptionRegistry.sol"; import {OptimisticTokenVotingPlugin} from "../OptimisticTokenVotingPlugin.sol"; import {OptimisticTokenVotingPluginSetup} from "../setup/OptimisticTokenVotingPluginSetup.sol"; import {MultisigPluginSetup} from "../setup/MultisigPluginSetup.sol"; @@ -81,12 +82,13 @@ contract TaikoDaoFactory { Multisig multisigPlugin; EmergencyMultisig emergencyMultisigPlugin; OptimisticTokenVotingPlugin optimisticTokenVotingPlugin; + // Helpers + SignerList signerList; + EncryptionRegistry encryptionRegistry; // Plugin repo's PluginRepo multisigPluginRepo; PluginRepo emergencyMultisigPluginRepo; PluginRepo optimisticTokenVotingPluginRepo; - // Other - PublicKeyRegistry publicKeyRegistry; } /// @notice Thrown when attempting to call deployOnce() when the DAO is already deployed. @@ -112,11 +114,15 @@ contract TaikoDaoFactory { DAO dao = prepareDao(); deployment.dao = dao; + // DEPLOY THE SIGNER LIST AND THE ENCRYPTION REGISTRY + (deployment.signerList, deployment.encryptionRegistry) = prepareSignerListAndEncryptionRegistry(dao); + // DEPLOY THE PLUGINS - (deployment.multisigPlugin, deployment.multisigPluginRepo, preparedMultisigSetupData) = prepareMultisig(dao); + (deployment.multisigPlugin, deployment.multisigPluginRepo, preparedMultisigSetupData) = + prepareMultisig(dao, deployment.signerList); (deployment.emergencyMultisigPlugin, deployment.emergencyMultisigPluginRepo, preparedEmergencyMultisigSetupData) - = prepareEmergencyMultisig(dao, deployment.multisigPlugin); + = prepareEmergencyMultisig(dao, deployment.signerList); ( deployment.optimisticTokenVotingPlugin, @@ -149,9 +155,6 @@ contract TaikoDaoFactory { // REMOVE THIS CONTRACT AS OWNER revokeOwnerPermission(deployment.dao); - - // DEPLOY OTHER CONTRACTS - deployment.publicKeyRegistry = deployPublicKeyRegistry(); } function prepareDao() internal returns (DAO dao) { @@ -188,18 +191,40 @@ contract TaikoDaoFactory { dao.applySingleTargetPermissions(address(dao), items); } - function prepareMultisig(DAO dao) internal returns (Multisig, PluginRepo, IPluginSetup.PreparedSetupData memory) { + function prepareSignerListAndEncryptionRegistry(DAO dao) + internal + returns (SignerList signerList, EncryptionRegistry encryptionRegistry) + { + signerList = deploySignerListWithoutSettings(dao); + encryptionRegistry = new EncryptionRegistry(signerList); + + // Link them together + { + // Grant temporary permission to update the settings + dao.grant(address(signerList), address(this), UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + + signerList.updateSettings(SignerList.Settings(encryptionRegistry, uint16(settings.multisigMembers.length))); + + // Revoke the remporary permission + dao.revoke(address(signerList), address(this), UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + } + } + + function prepareMultisig(DAO dao, SignerList signerList) + internal + returns (Multisig, PluginRepo, IPluginSetup.PreparedSetupData memory) + { // Publish repo PluginRepo pluginRepo = PluginRepoFactory(settings.pluginRepoFactory).createPluginRepoWithFirstVersion( settings.stdMultisigEnsDomain, address(settings.multisigPluginSetup), address(dao), " ", " " ); bytes memory settingsData = settings.multisigPluginSetup.encodeInstallationParameters( - settings.multisigMembers, Multisig.MultisigSettings( true, // onlyListed settings.minStdApprovals, settings.minStdProposalDuration, // destination minDuration + signerList, settings.multisigExpirationPeriod ) ); @@ -216,7 +241,7 @@ contract TaikoDaoFactory { return (Multisig(plugin), pluginRepo, preparedSetupData); } - function prepareEmergencyMultisig(DAO dao, Addresslist multisigPlugin) + function prepareEmergencyMultisig(DAO dao, SignerList signerList) internal returns (EmergencyMultisig, PluginRepo, IPluginSetup.PreparedSetupData memory) { @@ -229,7 +254,7 @@ contract TaikoDaoFactory { EmergencyMultisig.MultisigSettings( true, // onlyListed settings.minEmergencyApprovals, // minAppovals - Addresslist(multisigPlugin), + signerList, settings.multisigExpirationPeriod ) ); @@ -302,8 +327,12 @@ contract TaikoDaoFactory { return (OptimisticTokenVotingPlugin(plugin), pluginRepo, preparedSetupData); } - function deployPublicKeyRegistry() internal returns (PublicKeyRegistry) { - return new PublicKeyRegistry(deployment.multisigPlugin); + function deploySignerListWithoutSettings(DAO dao) internal returns (SignerList helper) { + helper = SignerList( + createERC1967Proxy( + address(new SignerList()), abi.encodeCall(SignerList.initialize, (dao, settings.multisigMembers)) + ) + ); } function applyPluginInstallation( diff --git a/src/interfaces/IEncryptionRegistry.sol b/src/interfaces/IEncryptionRegistry.sol new file mode 100644 index 0000000..7413300 --- /dev/null +++ b/src/interfaces/IEncryptionRegistry.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.17; + +interface IEncryptionRegistry { + /// @notice Emitted when a public key is defined + event PublicKeySet(address account, bytes32 publicKey); + + /// @notice Emitted when an externally owned wallet is appointed + event WalletAppointed(address account, address appointedWallet); + + /// @notice Raised when the caller is not an addresslist member + error MustBeListed(); + + /// @notice Raised when attempting to register a contract instead of a wallet + error CannotAppointContracts(); + + /// @notice Raised when attempting to appoint an address which is already a listed signer + error AlreadyListed(); + + /// @notice Raised when attempting to appoint an already appointed address + error AlreadyAppointed(); + + /// @notice Raised when a non appointed wallet tries to define the public key + error MustBeAppointed(); + + /// @notice Raised when someone else is appointed and the account owner tries to override the public key of the appointed wallet. The appointed value should be set to address(0) or msg.sender first. + error MustResetAppointment(); + + /// @notice Raised when the caller is not an addresslist compatible contract + error InvalidAddressList(); + + /// @notice Registers the externally owned wallet's address to use for encryption. This allows smart contracts to appoint an EOA that can decrypt data. + /// @dev NOTE: calling this function will wipe any existing public key previously registered. + function appointWallet(address newWallet) external; + + /// @notice Registers the given public key as the account's target for decrypting messages. + /// @dev NOTE: Calling this function from a smart contracts will revert. + function setOwnPublicKey(bytes32 publicKey) external; + + /// @notice Registers the given public key as the member's target for decrypting messages. Only if the sender is appointed. + /// @param account The address of the account to set the public key for. The sender must be appointed or the transaction will revert. + /// @param publicKey The libsodium public key to register + function setPublicKey(address account, bytes32 publicKey) external; + + /// @notice Returns the address of the account that appointed the given wallet, if any. + /// @return appointerAddress The address of the appointer account or zero. + function appointerOf(address wallet) external returns (address appointerAddress); + + /// @notice Returns the address of the account registered at the given index + function registeredAccounts(uint256) external view returns (address); + + /// @notice Returns the list of addresses on the registry + /// @dev Use this function to get all addresses in a single call. You can still call registeredAccounts[idx] to resolve them one by one. + function getRegisteredAccounts() external view returns (address[] memory); + + /// @notice Returns the address of the wallet appointed for encryption purposes + function getAppointedWallet(address member) external view returns (address); +} diff --git a/src/interfaces/IMultisig.sol b/src/interfaces/IMultisig.sol index ba73eff..79e9e66 100644 --- a/src/interfaces/IMultisig.sol +++ b/src/interfaces/IMultisig.sol @@ -8,14 +8,6 @@ import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; /// @author Aragon Association - 2023 /// @notice An interface for an on-chain multisig governance plugin in which a proposal passes if X out of Y approvals are met. interface IMultisig { - /// @notice Adds new members to the address list. Previously, it checks if the new address list length would be greater than `type(uint16).max`, the maximal number of approvals. - /// @param _members The addresses of the members to be added. - function addAddresses(address[] calldata _members) external; - - /// @notice Removes existing members from the address list. Previously, it checks if the new address list length is at least as long as the minimum approvals parameter requires. Note that `minApprovals` is must be at least 1 so the address list cannot become empty. - /// @param _members The addresses of the members to be removed. - function removeAddresses(address[] calldata _members) external; - /// @notice Approves and, optionally, executes the proposal. /// @param _proposalId The ID of the proposal. /// @param _tryExecution If `true`, execution is tried after the approval cast. The call does not revert if execution is not possible. diff --git a/src/interfaces/ISignerList.sol b/src/interfaces/ISignerList.sol new file mode 100644 index 0000000..82846fe --- /dev/null +++ b/src/interfaces/ISignerList.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.17; + +interface ISignerList { + /// @notice Emitted when signers are added to the DAO plugin. + /// @param signers The list of new signers being added. + event SignersAdded(address[] signers); + + /// @notice Emitted when signers are removed from the DAO plugin. + /// @param signers The list of existing signers being removed. + event SignersRemoved(address[] signers); + + /// @notice Adds new signers to the address list. Previously, it checks if the new address list length would be greater than `type(uint16).max`, the maximal number of approvals. + /// @param signers The addresses of the signers to be added. + function addSigners(address[] calldata signers) external; + + /// @notice Removes existing signers from the address list. Previously, it checks if the new address list length is at least as long as the minimum approvals parameter requires. Note that `minApprovals` is must be at least 1 so the address list cannot become empty. + /// @param signers The addresses of the signers to be removed. + function removeSigners(address[] calldata signers) external; + + /// @notice Given an address, determines whether it is a listed signer or a wallet appointed by a listed owner. + /// @dev NOTE: This function will only resolve based on the current state. Do not use it as an alias of `isListedAtBock()`. + /// @return listedOrAppointedByListed If resolved, whether the given address is currently listed as a member. False otherwise. + function isListedOrAppointedByListed(address _address) external returns (bool listedOrAppointedByListed); + + /// @notice Given an address, determines the corresponding (listed) owner account and the appointed wallet, if any. + /// @param sender The address to check within the list of signers or the appointed accounts. + /// @param blockNumber The block at which the list should be checked + /// @return owner If resolved to an account, it contains the encryption owner's address. Returns address(0) otherwise. + function getListedEncryptionOwnerAtBlock(address sender, uint256 blockNumber) external returns (address owner); + + /// @notice Given an address, determines the corresponding (listed) owner account and the appointed wallet, if any. + /// @return owner If listed and resolved to an account, it contains the encryption owner's address. Returns address(0) otherwise. + /// @return voter If listed and resolved, it contains the wallet address appointed for decryption, if any. Returns address(0) otherwise. + function resolveEncryptionAccountAtBlock(address sender, uint256 _blockNumber) + external + returns (address owner, address voter); + + /// @notice Among the SignerList's members registered on the EncryptionRegistry, return the effective address they use for encryption + function getEncryptionRecipients() external view returns (address[] memory); +} diff --git a/src/setup/MultisigPluginSetup.sol b/src/setup/MultisigPluginSetup.sol index c4af62c..e19c892 100644 --- a/src/setup/MultisigPluginSetup.sol +++ b/src/setup/MultisigPluginSetup.sol @@ -26,12 +26,11 @@ contract MultisigPluginSetup is PluginSetup { returns (address plugin, PreparedSetupData memory preparedSetupData) { // Decode `_data` to extract the parameters needed for deploying and initializing `Multisig` plugin. - (address[] memory members, Multisig.MultisigSettings memory multisigSettings) = - decodeInstallationParameters(_data); + (Multisig.MultisigSettings memory multisigSettings) = decodeInstallationParameters(_data); // Prepare and Deploy the plugin proxy. plugin = createERC1967Proxy( - address(multisigBase), abi.encodeCall(Multisig.initialize, (IDAO(_dao), members, multisigSettings)) + address(multisigBase), abi.encodeCall(Multisig.initialize, (IDAO(_dao), multisigSettings)) ); // Prepare permissions @@ -99,20 +98,20 @@ contract MultisigPluginSetup is PluginSetup { } /// @notice Encodes the given installation parameters into a byte array - function encodeInstallationParameters(address[] memory _members, Multisig.MultisigSettings memory _multisigSettings) + function encodeInstallationParameters(Multisig.MultisigSettings memory _multisigSettings) external pure returns (bytes memory) { - return abi.encode(_members, _multisigSettings); + return abi.encode(_multisigSettings); } /// @notice Decodes the given byte array into the original installation parameters function decodeInstallationParameters(bytes memory _data) public pure - returns (address[] memory _members, Multisig.MultisigSettings memory _multisigSettings) + returns (Multisig.MultisigSettings memory _multisigSettings) { - (_members, _multisigSettings) = abi.decode(_data, (address[], Multisig.MultisigSettings)); + (_multisigSettings) = abi.decode(_data, (Multisig.MultisigSettings)); } } diff --git a/test/EmergencyMultisig.t.sol b/test/EmergencyMultisig.t.sol index f4f63ab..86c3e76 100644 --- a/test/EmergencyMultisig.t.sol +++ b/test/EmergencyMultisig.t.sol @@ -1,45 +1,46 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.17; +pragma solidity 0.8.17; import {AragonTest} from "./base/AragonTest.sol"; -import {DaoBuilder} from "./helpers/DaoBuilder.sol"; -import {StandardProposalCondition} from "../src/conditions/StandardProposalCondition.sol"; -import {OptimisticTokenVotingPlugin} from "../src/OptimisticTokenVotingPlugin.sol"; -import {Multisig} from "../src/Multisig.sol"; +import {Addresslist} from "@aragon/osx/plugins/utils/Addresslist.sol"; import {EmergencyMultisig} from "../src/EmergencyMultisig.sol"; -import {IEmergencyMultisig} from "../src/interfaces/IEmergencyMultisig.sol"; +import {OptimisticTokenVotingPlugin} from "../src/OptimisticTokenVotingPlugin.sol"; +import { + SignerList, + UPDATE_SIGNER_LIST_PERMISSION_ID, + UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID +} from "../src/SignerList.sol"; +import {EncryptionRegistry} from "../src/EncryptionRegistry.sol"; +import {DaoBuilder} from "./helpers/DaoBuilder.sol"; import {DAO} from "@aragon/osx/core/dao/DAO.sol"; import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; -import {PermissionManager} from "@aragon/osx/core/permission/PermissionManager.sol"; +import {createProxyAndCall} from "../src/helpers/proxy.sol"; +import {IERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol"; import {IProposal} from "@aragon/osx/core/plugin/proposal/IProposal.sol"; import {IPlugin} from "@aragon/osx/core/plugin/IPlugin.sol"; -import {IMembership} from "@aragon/osx/core/plugin/membership/IMembership.sol"; -import {Addresslist} from "@aragon/osx/plugins/utils/Addresslist.sol"; -import {DaoUnauthorized} from "@aragon/osx/core/utils/auth.sol"; -import {IERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol"; -import {createProxyAndCall} from "../src/helpers/proxy.sol"; +import {IEmergencyMultisig} from "../src/interfaces/IEmergencyMultisig.sol"; uint64 constant EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD = 10 days; contract EmergencyMultisigTest is AragonTest { DaoBuilder builder; - DAO dao; EmergencyMultisig eMultisig; - Multisig stdMultisig; OptimisticTokenVotingPlugin optimisticPlugin; + SignerList signerList; + EncryptionRegistry encryptionRegistry; - // Events/errors to be tested here (duplicate) - event MultisigSettingsUpdated( - bool onlyListed, uint16 indexed minApprovals, Addresslist addresslistSource, uint64 expiration - ); - event MembersAdded(address[] members); - event MembersRemoved(address[] members); + address immutable SIGNER_LIST_BASE = address(new SignerList()); + // Events/errors to be tested here (duplicate) + error DaoUnauthorized(address dao, address where, address who, bytes32 permissionId); error InvalidAddresslistUpdate(address member); error InvalidActions(uint256 proposalId); - // Multisig's event + event MultisigSettingsUpdated( + bool onlyListed, uint16 indexed minApprovals, SignerList signerList, uint64 proposalExpirationPeriod + ); + event EmergencyProposalCreated(uint256 indexed proposalId, address indexed creator, bytes encryptedPayloadURI); // OptimisticTokenVotingPlugin's event @@ -62,257 +63,233 @@ contract EmergencyMultisigTest is AragonTest { vm.roll(100); builder = new DaoBuilder(); - (dao, optimisticPlugin, stdMultisig, eMultisig,,) = builder.withMultisigMember(alice).withMultisigMember(bob) - .withMultisigMember(carol).withMultisigMember(david).withMinApprovals(3).withMinDuration(0).build(); + (dao, optimisticPlugin,, eMultisig,, signerList, encryptionRegistry,) = builder.withMultisigMember(alice) + .withMultisigMember(bob).withMultisigMember(carol).withMultisigMember(david).withMinApprovals(3).withMinDuration( + 0 + ).build(); + } + + modifier givenANewlyDeployedContract() { + _; + } + + modifier givenCallingInitialize() { + _; } - function test_RevertsIfTryingToReinitialize() public { - // Deploy a new stdMultisig instance + function test_GivenCallingInitialize() external givenANewlyDeployedContract givenCallingInitialize { EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ onlyListed: true, minApprovals: 3, - addresslistSource: stdMultisig, + signerList: signerList, proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); + // It should initialize the first time eMultisig = EmergencyMultisig( createProxyAndCall( address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) ) ); - // Reinitialize should fail + // It should refuse to initialize again vm.expectRevert("Initializable: contract is already initialized"); eMultisig.initialize(dao, settings); - } - function test_InitializeSetsMinApprovals() public { - // 2 - EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ - onlyListed: true, - minApprovals: 2, - addresslistSource: stdMultisig, - proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); + // It should set the DAO address + + assertEq((address(eMultisig.dao())), address(dao), "Incorrect dao"); + // It should set the minApprovals + + (, uint16 minApprovals,,) = eMultisig.multisigSettings(); + assertEq(minApprovals, uint16(3), "Incorrect minApprovals"); + settings.minApprovals = 1; eMultisig = EmergencyMultisig( createProxyAndCall( address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) ) ); + (, minApprovals,,) = eMultisig.multisigSettings(); + assertEq(minApprovals, uint16(1), "Incorrect minApprovals"); - (, uint16 minApprovals,,) = eMultisig.multisigSettings(); - assertEq(minApprovals, uint16(2), "Incorrect minApprovals"); - - // Redeploy with 1 - settings.minApprovals = 1; + // It should set onlyListed + (bool onlyListed,,,) = eMultisig.multisigSettings(); + assertEq(onlyListed, true, "Incorrect onlyListed"); + settings.onlyListed = false; eMultisig = EmergencyMultisig( createProxyAndCall( address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) ) ); + (onlyListed,,,) = eMultisig.multisigSettings(); + assertEq(onlyListed, false, "Incorrect onlyListed"); - (, minApprovals,,) = eMultisig.multisigSettings(); - assertEq(minApprovals, uint16(1), "Incorrect minApprovals"); - } + // It should set signerList - function test_InitializeSetsOnlyListed() public { - // Deploy with true - EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ - onlyListed: true, - minApprovals: 3, - addresslistSource: stdMultisig, - proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); + (,, Addresslist givenSignerList,) = eMultisig.multisigSettings(); + assertEq(address(givenSignerList), address(signerList), "Incorrect addresslistSource"); + (,,,,, signerList,,) = builder.build(); + settings.signerList = signerList; + eMultisig = EmergencyMultisig( + createProxyAndCall( + address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) + ) + ); + (,, signerList,) = eMultisig.multisigSettings(); + assertEq(address(signerList), address(settings.signerList), "Incorrect addresslistSource"); + // It should set proposalExpirationPeriod + + (,,, uint64 expirationPeriod) = eMultisig.multisigSettings(); + assertEq(expirationPeriod, EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD, "Incorrect expirationPeriod"); + settings.proposalExpirationPeriod = 3 days; eMultisig = EmergencyMultisig( createProxyAndCall( address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) ) ); + (,,, expirationPeriod) = eMultisig.multisigSettings(); + assertEq(expirationPeriod, 3 days, "Incorrect expirationPeriod"); - (bool onlyListed,,,) = eMultisig.multisigSettings(); - assertEq(onlyListed, true, "Incorrect onlyListed"); + // It should emit MultisigSettingsUpdated - // Redeploy with false - settings.onlyListed = false; + (,,,,, SignerList newSignerList,,) = builder.build(); + + settings = EmergencyMultisig.MultisigSettings({ + onlyListed: false, + minApprovals: 2, + signerList: newSignerList, + proposalExpirationPeriod: 15 days + }); + vm.expectEmit(); + emit MultisigSettingsUpdated(false, uint16(2), newSignerList, 15 days); eMultisig = EmergencyMultisig( createProxyAndCall( address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) ) ); - - (onlyListed,,,) = eMultisig.multisigSettings(); - assertEq(onlyListed, false, "Incorrect onlyListed"); } - function test_InitializeSetsAddresslistSource() public { - // Deploy the default stdMultisig as source - EmergencyMultisig.MultisigSettings memory emSettings = EmergencyMultisig.MultisigSettings({ + function test_RevertWhen_MinApprovalsIsGreaterThanSignerListLengthOnInitialize() + external + givenANewlyDeployedContract + givenCallingInitialize + { + // It should revert + EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ onlyListed: true, - minApprovals: 3, - addresslistSource: stdMultisig, + minApprovals: 5, + signerList: signerList, proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.MinApprovalsOutOfBounds.selector, 4, 5)); eMultisig = EmergencyMultisig( createProxyAndCall( - address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, emSettings)) + address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) ) ); - (,, Addresslist givenAddressListSource,) = eMultisig.multisigSettings(); - assertEq(address(givenAddressListSource), address(stdMultisig), "Incorrect addresslistSource"); - - // Redeploy with a new addresslist source - (,, Multisig newMultisig,,,) = builder.build(); - - emSettings.addresslistSource = newMultisig; + // It should revert (with onlyListed false) + settings = EmergencyMultisig.MultisigSettings({ + onlyListed: false, + minApprovals: 5, + signerList: signerList, + proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD + }); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.MinApprovalsOutOfBounds.selector, 4, 5)); eMultisig = EmergencyMultisig( createProxyAndCall( - address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, emSettings)) + address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) ) ); - (,, givenAddressListSource,) = eMultisig.multisigSettings(); - assertEq(address(givenAddressListSource), address(emSettings.addresslistSource), "Incorrect addresslistSource"); - } + // It should not revert otherwise - function test_InitializeSetsProposalExpiration() public { - // Deploy with 15 days - EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ + settings = EmergencyMultisig.MultisigSettings({ onlyListed: true, - minApprovals: 3, - addresslistSource: stdMultisig, - proposalExpirationPeriod: 15 days + minApprovals: 4, + signerList: signerList, + proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - address[] memory signers = new address[](4); - signers[0] = alice; - signers[1] = bob; - signers[2] = carol; - signers[3] = david; - eMultisig = EmergencyMultisig( createProxyAndCall( address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) ) ); - (,,, uint64 expirationPeriod) = eMultisig.multisigSettings(); - assertEq(expirationPeriod, 15 days, "Incorrect expirationPeriod"); - - // Redeploy with 3 days - settings.proposalExpirationPeriod = 3 days; - + settings = EmergencyMultisig.MultisigSettings({ + onlyListed: false, + minApprovals: 4, + signerList: signerList, + proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD + }); eMultisig = EmergencyMultisig( createProxyAndCall( address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) ) ); - - (,,, expirationPeriod) = eMultisig.multisigSettings(); - assertEq(expirationPeriod, 3 days, "Incorrect expirationPeriod"); } - function test_ShouldEmitMultisigSettingsUpdatedOnInstall() public { - // Deploy with true/3/default + function test_RevertWhen_MinApprovalsIsZeroOnInitialize() + external + givenANewlyDeployedContract + givenCallingInitialize + { + // It should revert EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ onlyListed: true, - minApprovals: 3, - addresslistSource: stdMultisig, - proposalExpirationPeriod: 5 days + minApprovals: 0, + signerList: signerList, + proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - vm.expectEmit(); - emit MultisigSettingsUpdated(true, uint16(3), stdMultisig, 5 days); - + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.MinApprovalsOutOfBounds.selector, 1, 0)); eMultisig = EmergencyMultisig( createProxyAndCall( address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) ) ); - // Deploy with false/2/new - - (,, Multisig newMultisig,,,) = builder.build(); - + // It should revert (with onlyListed false) settings = EmergencyMultisig.MultisigSettings({ onlyListed: false, - minApprovals: 2, - addresslistSource: newMultisig, - proposalExpirationPeriod: 15 days + minApprovals: 0, + signerList: signerList, + proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - vm.expectEmit(); - emit MultisigSettingsUpdated(false, uint16(2), newMultisig, 15 days); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.MinApprovalsOutOfBounds.selector, 1, 0)); eMultisig = EmergencyMultisig( createProxyAndCall( address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) ) ); - } - - // INTERFACES - - function test_DoesntSupportTheEmptyInterface() public view { - bool supported = eMultisig.supportsInterface(0); - assertEq(supported, false, "Should not support the empty interface"); - } - - function test_SupportsIERC165Upgradeable() public view { - bool supported = eMultisig.supportsInterface(type(IERC165Upgradeable).interfaceId); - assertEq(supported, true, "Should support IERC165Upgradeable"); - } - - function test_SupportsIPlugin() public view { - bool supported = eMultisig.supportsInterface(type(IPlugin).interfaceId); - assertEq(supported, true, "Should support IPlugin"); - } - - function test_SupportsIProposal() public view { - bool supported = eMultisig.supportsInterface(type(IProposal).interfaceId); - assertEq(supported, true, "Should support IProposal"); - } - - function test_SupportsIMembership() public view { - bool supported = eMultisig.supportsInterface(type(IMembership).interfaceId); - assertEq(supported, true, "Should support IMembership"); - } - - function test_SupportsIEmergencyMultisig() public view { - bool supported = eMultisig.supportsInterface(type(IEmergencyMultisig).interfaceId); - assertEq(supported, true, "Should support IEmergencyMultisig"); - } - // UPDATE MULTISIG SETTINGS + // It should not revert otherwise - function test_ShouldntAllowMinApprovalsHigherThenAddrListLength() public { - EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ + settings = EmergencyMultisig.MultisigSettings({ onlyListed: true, - minApprovals: 5, - addresslistSource: stdMultisig, // Greater than 4 members + minApprovals: 4, + signerList: signerList, proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.MinApprovalsOutOfBounds.selector, 4, 5)); - eMultisig = EmergencyMultisig( createProxyAndCall( address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) ) ); - // Retry with onlyListed false settings = EmergencyMultisig.MultisigSettings({ onlyListed: false, - minApprovals: 6, - addresslistSource: stdMultisig, // Greater than 4 members + minApprovals: 4, + signerList: signerList, proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.MinApprovalsOutOfBounds.selector, 4, 6)); eMultisig = EmergencyMultisig( createProxyAndCall( address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) @@ -320,30 +297,47 @@ contract EmergencyMultisigTest is AragonTest { ); } - function test_ShouldNotAllowMinApprovalsZero() public { + function test_RevertWhen_SignerListIsInvalidOnInitialize() + external + givenANewlyDeployedContract + givenCallingInitialize + { + // It should revert EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ - onlyListed: true, - minApprovals: 0, - addresslistSource: stdMultisig, + onlyListed: false, + minApprovals: 1, + signerList: SignerList(address(dao)), proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.InvalidSignerList.selector, address(dao))); + eMultisig = EmergencyMultisig( + createProxyAndCall( + address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) + ) + ); - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.MinApprovalsOutOfBounds.selector, 1, 0)); - + // ko 2 + settings = EmergencyMultisig.MultisigSettings({ + onlyListed: false, + minApprovals: 1, + signerList: SignerList(address(builder)), + proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD + }); + vm.expectRevert(); eMultisig = EmergencyMultisig( createProxyAndCall( address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) ) ); - // Retry with onlyListed false + // ok + (,,,,, SignerList newSignerList,,) = builder.build(); settings = EmergencyMultisig.MultisigSettings({ onlyListed: false, - minApprovals: 0, - addresslistSource: stdMultisig, + minApprovals: 1, + signerList: newSignerList, proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.MinApprovalsOutOfBounds.selector, 1, 0)); eMultisig = EmergencyMultisig( createProxyAndCall( address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) @@ -351,106 +345,198 @@ contract EmergencyMultisigTest is AragonTest { ); } - function test_ShouldEmitMultisigSettingsUpdated() public { + function test_WhenCallingUpgradeTo() external { + // It should revert when called without the permission + address initialImplementation = eMultisig.implementation(); + address _newImplementation = address(new EmergencyMultisig()); + + vm.expectRevert( + abi.encodeWithSelector( + DaoUnauthorized.selector, + address(dao), + address(eMultisig), + alice, + eMultisig.UPGRADE_PLUGIN_PERMISSION_ID() + ) + ); + eMultisig.upgradeTo(_newImplementation); + assertEq(eMultisig.implementation(), initialImplementation); + + // It should work when called with the permission + dao.grant(address(eMultisig), alice, eMultisig.UPGRADE_PLUGIN_PERMISSION_ID()); + eMultisig.upgradeTo(_newImplementation); + } + + function test_WhenCallingUpgradeToAndCall() external { + // It should revert when called without the permission + address initialImplementation = eMultisig.implementation(); + dao.grant(address(eMultisig), alice, eMultisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); + address _newImplementation = address(new EmergencyMultisig()); + + EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ + onlyListed: true, + minApprovals: 3, + signerList: signerList, + proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD + }); + vm.expectRevert( + abi.encodeWithSelector( + DaoUnauthorized.selector, + address(dao), + address(eMultisig), + alice, + eMultisig.UPGRADE_PLUGIN_PERMISSION_ID() + ) + ); + eMultisig.upgradeToAndCall( + _newImplementation, abi.encodeCall(EmergencyMultisig.updateMultisigSettings, (settings)) + ); + assertEq(eMultisig.implementation(), initialImplementation); + + // It should work when called with the permission + dao.grant(address(eMultisig), alice, eMultisig.UPGRADE_PLUGIN_PERMISSION_ID()); + eMultisig.upgradeToAndCall( + _newImplementation, abi.encodeCall(EmergencyMultisig.updateMultisigSettings, (settings)) + ); + } + + function test_WhenCallingSupportsInterface() external view { + // It does not support the empty interface + bool supported = eMultisig.supportsInterface(0); + assertEq(supported, false, "Should not support the empty interface"); + + // It supports IERC165Upgradeable + supported = eMultisig.supportsInterface(type(IERC165Upgradeable).interfaceId); + assertEq(supported, true, "Should support IERC165Upgradeable"); + + // It supports IPlugin + supported = eMultisig.supportsInterface(type(IPlugin).interfaceId); + assertEq(supported, true, "Should support IPlugin"); + + // It supports IProposal + supported = eMultisig.supportsInterface(type(IProposal).interfaceId); + assertEq(supported, true, "Should support IProposal"); + + // It supports IEmergencyMultisig + supported = eMultisig.supportsInterface(type(IEmergencyMultisig).interfaceId); + assertEq(supported, true, "Should support IEmergencyMultisig"); + } + + modifier whenCallingUpdateSettings() { + _; + } + + function test_WhenCallingUpdateSettings() external whenCallingUpdateSettings { + // It should set the minApprovals + // It should set onlyListed + // It should set signerList + // It should set proposalExpirationPeriod + // It should emit MultisigSettingsUpdated + + bool givenOnlyListed; + uint16 givenMinApprovals; + SignerList givenSignerList; + uint64 givenProposalExpirationPeriod; dao.grant(address(eMultisig), address(alice), eMultisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); // 1 EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ onlyListed: true, minApprovals: 1, - addresslistSource: stdMultisig, + signerList: signerList, proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); vm.expectEmit(); - emit MultisigSettingsUpdated(true, 1, stdMultisig, EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD); + emit MultisigSettingsUpdated(true, 1, signerList, EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD); eMultisig.updateMultisigSettings(settings); + (givenOnlyListed, givenMinApprovals, givenSignerList, givenProposalExpirationPeriod) = + eMultisig.multisigSettings(); + assertEq(givenOnlyListed, true, "onlyListed should be true"); + assertEq(givenMinApprovals, 1, "Incorrect givenMinApprovals"); + assertEq(address(givenSignerList), address(signerList), "Incorrect givenSignerList"); + assertEq( + givenProposalExpirationPeriod, + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD, + "Incorrect givenProposalExpirationPeriod" + ); + // 2 - (,, Multisig newMultisig,,,) = builder.build(); + (,,,,, SignerList newSignerList,,) = builder.build(); settings = EmergencyMultisig.MultisigSettings({ onlyListed: true, minApprovals: 2, - addresslistSource: newMultisig, + signerList: newSignerList, proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD - 1 }); vm.expectEmit(); - emit MultisigSettingsUpdated(true, 2, newMultisig, EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD - 1); + emit MultisigSettingsUpdated(true, 2, newSignerList, EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD - 1); eMultisig.updateMultisigSettings(settings); + (givenOnlyListed, givenMinApprovals, givenSignerList, givenProposalExpirationPeriod) = + eMultisig.multisigSettings(); + assertEq(givenOnlyListed, true, "onlyListed should be true"); + assertEq(givenMinApprovals, 2, "Incorrect givenMinApprovals"); + assertEq(address(givenSignerList), address(newSignerList), "Incorrect givenSignerList"); + assertEq( + givenProposalExpirationPeriod, + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD - 1, + "Incorrect givenProposalExpirationPeriod" + ); + // 3 - (,, newMultisig,,,) = builder.build(); + (,,,,, newSignerList,,) = builder.build(); settings = EmergencyMultisig.MultisigSettings({ onlyListed: false, minApprovals: 3, - addresslistSource: newMultisig, + signerList: newSignerList, proposalExpirationPeriod: 4 days }); vm.expectEmit(); - emit MultisigSettingsUpdated(false, 3, newMultisig, 4 days); + emit MultisigSettingsUpdated(false, 3, newSignerList, 4 days); eMultisig.updateMultisigSettings(settings); + (givenOnlyListed, givenMinApprovals, givenSignerList, givenProposalExpirationPeriod) = + eMultisig.multisigSettings(); + assertEq(givenOnlyListed, false, "onlyListed should be false"); + assertEq(givenMinApprovals, 3, "Incorrect givenMinApprovals"); + assertEq(address(givenSignerList), address(newSignerList), "Incorrect givenSignerList"); + assertEq(givenProposalExpirationPeriod, 4 days, "Incorrect givenProposalExpirationPeriod"); + // 4 settings = EmergencyMultisig.MultisigSettings({ onlyListed: false, minApprovals: 4, - addresslistSource: stdMultisig, + signerList: signerList, proposalExpirationPeriod: 8 days }); vm.expectEmit(); - emit MultisigSettingsUpdated(false, 4, stdMultisig, 8 days); + emit MultisigSettingsUpdated(false, 4, signerList, 8 days); eMultisig.updateMultisigSettings(settings); + + (givenOnlyListed, givenMinApprovals, givenSignerList, givenProposalExpirationPeriod) = + eMultisig.multisigSettings(); + assertEq(givenOnlyListed, false, "onlyListed should be true"); + assertEq(givenMinApprovals, 4, "Incorrect givenMinApprovals"); + assertEq(address(givenSignerList), address(signerList), "Incorrect givenSignerList"); + assertEq(givenProposalExpirationPeriod, 8 days, "Incorrect givenProposalExpirationPeriod"); } - function test_UpdateSettingsShouldRevertWithInvalidAddressSource() public { - dao.grant(address(eMultisig), alice, eMultisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); + function test_RevertGiven_CallerHasNoPermission() external whenCallingUpdateSettings { + // It should revert + (,,,,, SignerList newSignerList,,) = builder.build(); - // ko EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ onlyListed: false, - minApprovals: 1, - addresslistSource: Multisig(address(dao)), - proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.InvalidAddressListSource.selector, address(dao))); - eMultisig.updateMultisigSettings(settings); - - // ko 2 - settings = EmergencyMultisig.MultisigSettings({ - onlyListed: false, - minApprovals: 1, - addresslistSource: Multisig(address(optimisticPlugin)), - proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - vm.expectRevert( - abi.encodeWithSelector(EmergencyMultisig.InvalidAddressListSource.selector, address(optimisticPlugin)) - ); - eMultisig.updateMultisigSettings(settings); - - // ok - (,, Multisig newMultisig,,,) = builder.build(); - settings = EmergencyMultisig.MultisigSettings({ - onlyListed: false, - minApprovals: 1, - addresslistSource: newMultisig, - proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - eMultisig.updateMultisigSettings(settings); - } - - function test_onlyWalletWithPermissionsCanUpdateSettings() public { - (,, Multisig newMultisig,,,) = builder.build(); - - EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ - onlyListed: false, - minApprovals: 1, - addresslistSource: newMultisig, - proposalExpirationPeriod: 3 days + minApprovals: 2, + signerList: newSignerList, + proposalExpirationPeriod: 3 days }); vm.expectRevert( abi.encodeWithSelector( @@ -468,181 +554,160 @@ contract EmergencyMultisigTest is AragonTest { eMultisig.multisigSettings(); assertEq(onlyListed, true); assertEq(minApprovals, 3); - assertEq(address(currentSource), address(stdMultisig)); + assertEq(address(currentSource), address(signerList)); assertEq(expiration, 10 days); + // It otherwise it should just work // Retry with the permission dao.grant(address(eMultisig), alice, eMultisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); vm.expectEmit(); - emit MultisigSettingsUpdated(false, 1, newMultisig, 3 days); + emit MultisigSettingsUpdated(false, 2, newSignerList, 3 days); eMultisig.updateMultisigSettings(settings); } - function test_IsMemberShouldReturnWhenApropriate() public { - assertEq(eMultisig.isMember(alice), true, "Should be a member"); - assertEq(eMultisig.isMember(bob), true, "Should be a member"); - assertEq(eMultisig.isMember(carol), true, "Should be a member"); - assertEq(eMultisig.isMember(david), true, "Should be a member"); - - dao.grant(address(stdMultisig), alice, stdMultisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); - address[] memory signers = new address[](1); - signers[0] = bob; - stdMultisig.removeAddresses(signers); - - assertEq(eMultisig.isMember(alice), true, "Should be a member"); - assertEq(eMultisig.isMember(bob), false, "Should not be a member"); - assertEq(eMultisig.isMember(carol), true, "Should be a member"); - assertEq(eMultisig.isMember(david), true, "Should be a member"); - - // 2 - stdMultisig.addAddresses(signers); // Add Bob back - signers[0] = alice; - stdMultisig.removeAddresses(signers); - - assertEq(eMultisig.isMember(alice), false, "Should not be a member"); - assertEq(eMultisig.isMember(bob), true, "Should be a member"); - assertEq(eMultisig.isMember(carol), true, "Should be a member"); - assertEq(eMultisig.isMember(david), true, "Should be a member"); - - // 3 - stdMultisig.addAddresses(signers); // Add Alice back - signers[0] = carol; - stdMultisig.removeAddresses(signers); - - assertEq(eMultisig.isMember(alice), true, "Should be a member"); - assertEq(eMultisig.isMember(bob), true, "Should be a member"); - assertEq(eMultisig.isMember(carol), false, "Should not be a member"); - assertEq(eMultisig.isMember(david), true, "Should be a member"); + function test_RevertWhen_MinApprovalsIsGreaterThanSignerListLengthOnUpdateSettings() + external + whenCallingUpdateSettings + { + // It should revert + dao.grant(address(eMultisig), alice, eMultisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); - // 4 - stdMultisig.addAddresses(signers); // Add Carol back - signers[0] = david; - stdMultisig.removeAddresses(signers); + EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ + onlyListed: true, + minApprovals: 5, + signerList: signerList, + proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD + }); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.MinApprovalsOutOfBounds.selector, 4, 5)); + eMultisig.updateMultisigSettings(settings); - assertEq(eMultisig.isMember(alice), true, "Should be a member"); - assertEq(eMultisig.isMember(bob), true, "Should be a member"); - assertEq(eMultisig.isMember(carol), true, "Should be a member"); - assertEq(eMultisig.isMember(david), false, "Should not be a member"); - } + // It should revert (with onlyListed false) + settings = EmergencyMultisig.MultisigSettings({ + onlyListed: false, + minApprovals: 5, + signerList: signerList, + proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD + }); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.MinApprovalsOutOfBounds.selector, 4, 5)); + eMultisig.updateMultisigSettings(settings); - function test_IsMemberIsListedShouldReturnTheSameValue() public { - assertEq(stdMultisig.isListed(alice), eMultisig.isMember(alice), "isMember isListed should be equal"); - assertEq(stdMultisig.isListed(bob), eMultisig.isMember(bob), "isMember isListed should be equal"); - assertEq(stdMultisig.isListed(carol), eMultisig.isMember(carol), "isMember isListed should be equal"); - assertEq(stdMultisig.isListed(david), eMultisig.isMember(david), "isMember isListed should be equal"); + // It should not revert otherwise - dao.grant(address(stdMultisig), alice, stdMultisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); + // More signers + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); address[] memory signers = new address[](1); - signers[0] = alice; - stdMultisig.removeAddresses(signers); + signers[0] = randomWallet; + signerList.addSigners(signers); - assertEq(stdMultisig.isListed(alice), eMultisig.isMember(alice), "isMember isListed should be equal"); - assertEq(stdMultisig.isListed(bob), eMultisig.isMember(bob), "isMember isListed should be equal"); - assertEq(stdMultisig.isListed(carol), eMultisig.isMember(carol), "isMember isListed should be equal"); - assertEq(stdMultisig.isListed(david), eMultisig.isMember(david), "isMember isListed should be equal"); + eMultisig.updateMultisigSettings(settings); + } - // 2 - stdMultisig.addAddresses(signers); // Add Alice back - signers[0] = bob; - stdMultisig.removeAddresses(signers); + function test_RevertWhen_MinApprovalsIsZeroOnUpdateSettings() external whenCallingUpdateSettings { + // It should revert + dao.grant(address(eMultisig), alice, eMultisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); - assertEq(stdMultisig.isListed(alice), eMultisig.isMember(alice), "isMember isListed should be equal"); - assertEq(stdMultisig.isListed(bob), eMultisig.isMember(bob), "isMember isListed should be equal"); - assertEq(stdMultisig.isListed(carol), eMultisig.isMember(carol), "isMember isListed should be equal"); - assertEq(stdMultisig.isListed(david), eMultisig.isMember(david), "isMember isListed should be equal"); + EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ + onlyListed: true, + minApprovals: 0, + signerList: signerList, + proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD + }); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.MinApprovalsOutOfBounds.selector, 1, 0)); + eMultisig.updateMultisigSettings(settings); - // 3 - stdMultisig.addAddresses(signers); // Add Bob back - signers[0] = carol; - stdMultisig.removeAddresses(signers); + // It should revert (with onlyListed false) + settings = EmergencyMultisig.MultisigSettings({ + onlyListed: false, + minApprovals: 0, + signerList: signerList, + proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD + }); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.MinApprovalsOutOfBounds.selector, 1, 0)); + eMultisig.updateMultisigSettings(settings); - assertEq(stdMultisig.isListed(alice), eMultisig.isMember(alice), "isMember isListed should be equal"); - assertEq(stdMultisig.isListed(bob), eMultisig.isMember(bob), "isMember isListed should be equal"); - assertEq(stdMultisig.isListed(carol), eMultisig.isMember(carol), "isMember isListed should be equal"); - assertEq(stdMultisig.isListed(david), eMultisig.isMember(david), "isMember isListed should be equal"); + // It should not revert otherwise - // 4 - stdMultisig.addAddresses(signers); // Add Carol back - signers[0] = david; - stdMultisig.removeAddresses(signers); + settings.minApprovals = 1; + eMultisig.updateMultisigSettings(settings); - assertEq(stdMultisig.isListed(alice), eMultisig.isMember(alice), "isMember isListed should be equal"); - assertEq(stdMultisig.isListed(bob), eMultisig.isMember(bob), "isMember isListed should be equal"); - assertEq(stdMultisig.isListed(carol), eMultisig.isMember(carol), "isMember isListed should be equal"); - assertEq(stdMultisig.isListed(david), eMultisig.isMember(david), "isMember isListed should be equal"); + settings.onlyListed = true; + eMultisig.updateMultisigSettings(settings); } - function testFuzz_IsMemberIsFalseByDefault(uint256 _randomEntropy) public { - // Deploy a new stdMultisig instance - Multisig.MultisigSettings memory mSettings = Multisig.MultisigSettings({ - onlyListed: true, + function test_RevertWhen_SignerListIsInvalidOnUpdateSettings() external whenCallingUpdateSettings { + // It should revert + dao.grant(address(eMultisig), alice, eMultisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); + + // ko + EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ + onlyListed: false, minApprovals: 1, - destinationProposalDuration: 4 days, + signerList: SignerList(address(dao)), proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - address[] memory signers = new address[](1); - signers[0] = address(0x0); // 0x0... would be a member but the chance is negligible + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.InvalidSignerList.selector, address(dao))); + eMultisig.updateMultisigSettings(settings); - stdMultisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, mSettings))) - ); - EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ - onlyListed: true, + // ko 2 + settings = EmergencyMultisig.MultisigSettings({ + onlyListed: false, minApprovals: 1, - addresslistSource: stdMultisig, + signerList: SignerList(address(builder)), proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - eMultisig = EmergencyMultisig( - createProxyAndCall( - address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) - ) - ); + vm.expectRevert(); + eMultisig.updateMultisigSettings(settings); - assertEq( - eMultisig.isMember(vm.addr(uint256(keccak256(abi.encodePacked(_randomEntropy))))), false, "Should be false" - ); + // ok + (,,,,, SignerList newSignerList,,) = builder.build(); + settings = EmergencyMultisig.MultisigSettings({ + onlyListed: false, + minApprovals: 1, + signerList: newSignerList, + proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD + }); + eMultisig.updateMultisigSettings(settings); } function testFuzz_PermissionedUpdateSettings(address randomAccount) public { dao.grant(address(eMultisig), alice, eMultisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); - (bool onlyListed, uint16 minApprovals, Addresslist addresslistSource, uint64 expiration) = + (bool onlyListed, uint16 minApprovals, SignerList givenSignerList, uint64 expiration) = eMultisig.multisigSettings(); assertEq(minApprovals, 3, "Should be 3"); assertEq(onlyListed, true, "Should be true"); - assertEq(address(addresslistSource), address(stdMultisig), "Incorrect addresslistSource"); + assertEq(address(givenSignerList), address(signerList), "Incorrect addresslistSource"); assertEq(expiration, 10 days, "Should be 10"); // in - (,, Multisig newMultisig,,,) = builder.build(); + (,,,,, SignerList newSignerList,,) = builder.build(); EmergencyMultisig.MultisigSettings memory newSettings = EmergencyMultisig.MultisigSettings({ onlyListed: false, minApprovals: 2, - addresslistSource: newMultisig, + signerList: newSignerList, proposalExpirationPeriod: 4 days }); eMultisig.updateMultisigSettings(newSettings); - Addresslist givenAddresslistSource; - (onlyListed, minApprovals, givenAddresslistSource, expiration) = eMultisig.multisigSettings(); + (onlyListed, minApprovals, givenSignerList, expiration) = eMultisig.multisigSettings(); assertEq(minApprovals, 2, "Should be 2"); assertEq(onlyListed, false, "Should be false"); - assertEq(address(givenAddresslistSource), address(newMultisig), "Incorrect addresslistSource"); + assertEq(address(givenSignerList), address(newSignerList), "Incorrect signerList"); assertEq(expiration, 4 days, "Should be 4"); // out newSettings = EmergencyMultisig.MultisigSettings({ onlyListed: true, minApprovals: 1, - addresslistSource: stdMultisig, + signerList: signerList, proposalExpirationPeriod: 1 days }); eMultisig.updateMultisigSettings(newSettings); - (onlyListed, minApprovals, givenAddresslistSource, expiration) = eMultisig.multisigSettings(); + (onlyListed, minApprovals, givenSignerList, expiration) = eMultisig.multisigSettings(); assertEq(minApprovals, 1, "Should be 1"); assertEq(onlyListed, true, "Should be true"); - assertEq(address(givenAddresslistSource), address(stdMultisig), "Incorrect addresslistSource"); + assertEq(address(givenSignerList), address(signerList), "Incorrect signerList"); assertEq(expiration, 1 days, "Should be 1"); vm.roll(block.number + 1); @@ -651,11 +716,11 @@ contract EmergencyMultisigTest is AragonTest { if (randomAccount != alice && randomAccount != address(0)) { vm.startPrank(randomAccount); - (,, newMultisig,,,) = builder.build(); + (,,,,, newSignerList,,) = builder.build(); newSettings = EmergencyMultisig.MultisigSettings({ onlyListed: false, minApprovals: 4, - addresslistSource: newMultisig, + signerList: newSignerList, proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); @@ -670,115 +735,152 @@ contract EmergencyMultisigTest is AragonTest { ); eMultisig.updateMultisigSettings(newSettings); - (onlyListed, minApprovals, givenAddresslistSource, expiration) = eMultisig.multisigSettings(); + (onlyListed, minApprovals, givenSignerList, expiration) = eMultisig.multisigSettings(); assertEq(minApprovals, 1, "Should still be 1"); assertEq(onlyListed, true, "Should still be true"); - assertEq(address(givenAddresslistSource), address(stdMultisig), "Should still be stdMultisig"); + assertEq(address(givenSignerList), address(signerList), "Should still be signerList"); assertEq(expiration, 1 days, "Should still be 1"); } } - // PROPOSAL CREATION + modifier whenCallingCreateProposal() { + _; + } + + function test_WhenCallingCreateProposal() external whenCallingCreateProposal { + uint256 pid; + bool executed; + uint16 approvals; + EmergencyMultisig.ProposalParameters memory parameters; + bytes memory encryptedPayloadURI; + bytes32 publicMetadataUriHash; + bytes32 destinationActionsHash; + OptimisticTokenVotingPlugin destinationPlugin; + + // It increments the proposal counter + // It creates and return unique proposal IDs + // It emits the EmergencyProposalCreated event + // It creates a proposal with the given values - function test_IncrementsTheProposalCounter() public { - // increments the proposal counter assertEq(eMultisig.proposalCount(), 0, "Should have no proposals"); // 1 - eMultisig.createProposal( + vm.expectEmit(); + emit EmergencyProposalCreated({proposalId: 0, creator: alice, encryptedPayloadURI: "ipfs://"}); + pid = eMultisig.createProposal( "ipfs://", bytes32(0x1234000000000000000000000000000000000000000000000000000000000000), bytes32(0x0000123400000000000000000000000000000000000000000000000000000000), optimisticPlugin, false ); - + assertEq(pid, 0, "Should be 0"); assertEq(eMultisig.proposalCount(), 1, "Should have 1 proposal"); - // 2 - eMultisig.createProposal( - "ipfs://more", - bytes32(0x1234000000000000000000000000000000000000000000000000000000000000), - bytes32(0x0000123400000000000000000000000000000000000000000000000000000000), - optimisticPlugin, - true + ( + executed, + approvals, + parameters, + encryptedPayloadURI, + publicMetadataUriHash, + destinationActionsHash, + destinationPlugin + ) = eMultisig.getProposal(pid); + assertEq(executed, false, "Should be false"); + assertEq(approvals, 0, "Should be 0"); + assertEq(parameters.minApprovals, 3, "Incorrect minApprovals"); + assertEq(parameters.snapshotBlock, block.number - 1, "Incorrect snapshotBlock"); + assertEq( + parameters.expirationDate, + block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD, + "Incorrect expirationDate" ); - - assertEq(eMultisig.proposalCount(), 2, "Should have 2 proposals"); - } - - function test_CreatesAndReturnsUniqueProposalIds() public { - // creates unique proposal IDs for each proposal - - // 1 - uint256 pid = eMultisig.createProposal( - "", + assertEq(encryptedPayloadURI, "ipfs://", "Incorrect encryptedPayloadURI"); + assertEq( + publicMetadataUriHash, bytes32(0x1234000000000000000000000000000000000000000000000000000000000000), + "Incorrect publicMetadataUriHash" + ); + assertEq( + destinationActionsHash, bytes32(0x0000123400000000000000000000000000000000000000000000000000000000), - optimisticPlugin, - true + "Incorrect destinationActionsHash" ); - - assertEq(pid, 0, "Should be 0"); + assertEq(address(destinationPlugin), address(optimisticPlugin), "Incorrect destinationPlugin"); // 2 - pid = eMultisig.createProposal( - "ipfs://", - bytes32(0x0000567800000000000000000000000000000000000000000000000000000000), - bytes32(0x0000000056780000000000000000000000000000000000000000000000000000), - optimisticPlugin, - false - ); - - assertEq(pid, 1, "Should be 1"); + vm.startPrank(bob); - // 3 + vm.expectEmit(); + emit EmergencyProposalCreated({proposalId: 1, creator: bob, encryptedPayloadURI: "ipfs://more"}); pid = eMultisig.createProposal( "ipfs://more", - bytes32(0x1234000000000000000000000000000000000000000000000000000000000000), - bytes32(0x0000000012340000000000000000000000000000000000000000000000000000), + bytes32(0x2345000000000000000000000000000000000000000000000000000000000000), + bytes32(0x0000234500000000000000000000000000000000000000000000000000000000), optimisticPlugin, true ); - assertEq(pid, 2, "Should be 2"); - } - - function test_EmitsProposalCreated() public { - // emits the `ProposalCreated` event + assertEq(pid, 1, "Should be 1"); + assertEq(eMultisig.proposalCount(), 2, "Should have 2 proposals"); - vm.expectEmit(); - emit EmergencyProposalCreated({proposalId: 0, creator: alice, encryptedPayloadURI: ""}); - eMultisig.createProposal( - "", - bytes32(0x1234000000000000000000000000000000000000000000000000000000000000), - bytes32(0x0000123400000000000000000000000000000000000000000000000000000000), - optimisticPlugin, - true + ( + executed, + approvals, + parameters, + encryptedPayloadURI, + publicMetadataUriHash, + destinationActionsHash, + destinationPlugin + ) = eMultisig.getProposal(pid); + assertEq(executed, false, "Should be false"); + assertEq(approvals, 1, "Should be 1"); + assertEq(parameters.minApprovals, 3, "Incorrect minApprovals"); + assertEq(parameters.snapshotBlock, block.number - 1, "Incorrect snapshotBlock"); + assertEq( + parameters.expirationDate, + block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD, + "Incorrect expirationDate" + ); + assertEq(encryptedPayloadURI, "ipfs://more", "Incorrect encryptedPayloadURI"); + assertEq( + publicMetadataUriHash, + bytes32(0x2345000000000000000000000000000000000000000000000000000000000000), + "Incorrect publicMetadataUriHash" ); + assertEq( + destinationActionsHash, + bytes32(0x0000234500000000000000000000000000000000000000000000000000000000), + "Incorrect destinationActionsHash" + ); + assertEq(address(destinationPlugin), address(optimisticPlugin), "Incorrect destinationPlugin"); - // 2 - vm.startPrank(bob); + // 3 + vm.startPrank(carol); + OptimisticTokenVotingPlugin newOptimistic; + (, newOptimistic,, eMultisig,,,,) = builder.withMinApprovals(2).build(); vm.expectEmit(); - emit EmergencyProposalCreated({proposalId: 1, creator: bob, encryptedPayloadURI: "ipfs://"}); - eMultisig.createProposal( - "ipfs://", - bytes32(0x0000567800000000000000000000000000000000000000000000000000000000), - bytes32(0x0000000056780000000000000000000000000000000000000000000000000000), - optimisticPlugin, - false + emit EmergencyProposalCreated({proposalId: 0, creator: carol, encryptedPayloadURI: "ipfs://more"}); + pid = eMultisig.createProposal( + "ipfs://more", + bytes32(0x2345000000000000000000000000000000000000000000000000000000000000), + bytes32(0x0000234500000000000000000000000000000000000000000000000000000000), + newOptimistic, + true ); - } - function test_RevertsIfSettingsChangedInSameBlock() public { - // reverts if the stdMultisig settings have changed in the same block + (,, parameters,,,, destinationPlugin) = eMultisig.getProposal(pid); + assertEq(parameters.minApprovals, 2, "Incorrect minApprovals"); + assertEq(address(destinationPlugin), address(newOptimistic), "Incorrect destinationPlugin"); + } + function test_GivenSettingsChangedOnTheSameBlock() external whenCallingCreateProposal { { EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ onlyListed: true, minApprovals: 3, - addresslistSource: stdMultisig, + signerList: signerList, proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); @@ -789,389 +891,444 @@ contract EmergencyMultisigTest is AragonTest { ); } + // It reverts // Same block vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalCreationForbidden.selector, alice)); eMultisig.createProposal("", bytes32(0), bytes32(0), optimisticPlugin, false); + // It does not revert otherwise // Next block vm.roll(block.number + 1); eMultisig.createProposal("", bytes32(0), bytes32(0), optimisticPlugin, false); } - function test_CreatesWhenUnlistedAccountsAllowed() public { - // creates a proposal when unlisted accounts are allowed + function test_GivenOnlyListedIsFalse() external whenCallingCreateProposal { + // It allows anyone to create // Deploy a new instance with custom settings - (dao, optimisticPlugin, stdMultisig, eMultisig,,) = builder.withoutOnlyListed().build(); + (dao, optimisticPlugin,, eMultisig,,,,) = builder.withoutOnlyListed().build(); vm.startPrank(randomWallet); eMultisig.createProposal("", 0, 0, optimisticPlugin, false); - vm.startPrank(carol); + vm.startPrank(address(0x1234)); eMultisig.createProposal("", 0, 0, optimisticPlugin, false); - vm.startPrank(david); + vm.startPrank(address(0x22345)); eMultisig.createProposal("", 0, 0, optimisticPlugin, false); } - function test_RevertsWhenOnlyListedAndAnotherWalletCreates() public { - // reverts if the user is not on the list and only listed accounts can create proposals + modifier givenOnlyListedIsTrue() { + _; + } + + function test_GivenCreationCallerIsNotListedOrAppointed() + external + whenCallingCreateProposal + givenOnlyListedIsTrue + { + // It reverts + + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 3)); vm.startPrank(randomWallet); vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalCreationForbidden.selector, randomWallet)); eMultisig.createProposal("", 0, 0, optimisticPlugin, false); + // 2 vm.startPrank(taikoBridge); vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalCreationForbidden.selector, taikoBridge)); eMultisig.createProposal("", 0, 0, optimisticPlugin, false); - } - function test_RevertsWhenCreatorWasListedBeforeButNotNow() public { - // reverts if `msg.sender` is not listed although she was listed in the last block + // It reverts if listed before but not now - dao.grant(address(stdMultisig), alice, stdMultisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); + vm.startPrank(alice); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); - // Remove address[] memory addrs = new address[](1); addrs[0] = alice; - stdMultisig.removeAddresses(addrs); + signerList.removeSigners(addrs); vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalCreationForbidden.selector, alice)); eMultisig.createProposal("", 0, 0, optimisticPlugin, false); + } - stdMultisig.addAddresses(addrs); // Add Alice back - vm.roll(block.number + 1); - eMultisig.createProposal("", 0, 0, optimisticPlugin, false); + function test_GivenCreationCallerIsAppointedByAFormerSigner() + external + whenCallingCreateProposal + givenOnlyListedIsTrue + { + // It reverts - // Add+remove - addrs[0] = bob; - stdMultisig.removeAddresses(addrs); + encryptionRegistry.appointWallet(randomWallet); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 3)); - vm.startPrank(bob); + address[] memory addrs = new address[](1); + addrs[0] = alice; + signerList.removeSigners(addrs); - // Bob cannot create now - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalCreationForbidden.selector, bob)); + vm.startPrank(randomWallet); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalCreationForbidden.selector, randomWallet)); eMultisig.createProposal("", 0, 0, optimisticPlugin, false); + // Undo vm.startPrank(alice); + signerList.addSigners(addrs); - // Bob can create now - stdMultisig.addAddresses(addrs); // Add Bob back - - vm.startPrank(alice); - + vm.startPrank(randomWallet); eMultisig.createProposal("", 0, 0, optimisticPlugin, false); } - function test_CreatesProposalWithoutApprovingIfUnspecified() public { - // creates a proposal successfully and does not approve if not specified + function test_GivenCreationCallerIsListedAndSelfAppointed() + external + whenCallingCreateProposal + givenOnlyListedIsTrue + { + // It creates the proposal - uint256 pid = eMultisig.createProposal( - "", - 0, - 0, - optimisticPlugin, - false // approveProposal - ); + vm.startPrank(alice); + eMultisig.createProposal("a", 0, 0, optimisticPlugin, false); - assertEq(eMultisig.hasApproved(pid, alice), false, "Should not have approved"); - (, uint16 approvals,,,,,) = eMultisig.getProposal(pid); - assertEq(approvals, 0, "Should be 0"); + vm.startPrank(bob); + eMultisig.createProposal("b", 0, 0, optimisticPlugin, false); - eMultisig.approve(pid); + vm.startPrank(carol); + eMultisig.createProposal("c", 0, 0, optimisticPlugin, true); - assertEq(eMultisig.hasApproved(pid, alice), true, "Should have approved"); - (, approvals,,,,,) = eMultisig.getProposal(pid); - assertEq(approvals, 1, "Should be 1"); + vm.startPrank(david); + eMultisig.createProposal("d", 0, 0, optimisticPlugin, false); } - function test_CreatesAndApprovesWhenSpecified() public { - // creates a proposal successfully and approves if specified + function test_GivenCreationCallerIsListedAppointingSomeoneElseNow() + external + whenCallingCreateProposal + givenOnlyListedIsTrue + { + // It creates the proposal - vm.expectEmit(); - emit Approved({proposalId: 0, approver: alice}); - eMultisig.createProposal( - "ipfs://", - bytes32(0x1234000000000000000000000000000000000000000000000000000000000000), - bytes32(0x0000123400000000000000000000000000000000000000000000000000000000), - optimisticPlugin, - true - ); + vm.startPrank(alice); + encryptionRegistry.appointWallet(address(0x1234)); + eMultisig.createProposal("a", 0, 0, optimisticPlugin, false); - uint256 pid = eMultisig.createProposal( - "ipfs://", - bytes32(0x1234000000000000000000000000000000000000000000000000000000000000), - bytes32(0x0000123400000000000000000000000000000000000000000000000000000000), - optimisticPlugin, - true // approveProposal - ); - assertEq(eMultisig.hasApproved(pid, alice), true, "Should have approved"); - (, uint16 approvals,,,,,) = eMultisig.getProposal(pid); - assertEq(approvals, 1, "Should be 1"); - } + vm.startPrank(bob); + encryptionRegistry.appointWallet(address(0x2345)); + eMultisig.createProposal("b", 0, 0, optimisticPlugin, false); - function test_HashActionsReturnsProperData() public view { - IDAO.Action[] memory actions = new IDAO.Action[](1); - actions[0].to = address(dao); - actions[0].value = 1 ether; - actions[0].data = hex"00112233"; + vm.startPrank(carol); + encryptionRegistry.appointWallet(address(0x3456)); + eMultisig.createProposal("c", 0, 0, optimisticPlugin, false); - bytes32 h1 = eMultisig.hashActions(actions); + vm.startPrank(david); + encryptionRegistry.appointWallet(address(0x4567)); + eMultisig.createProposal("d", 0, 0, optimisticPlugin, false); + } - // 2 - actions[0].to = bob; - bytes32 h2 = eMultisig.hashActions(actions); - assertNotEq(h1, h2, "Hashes should differ"); + function test_GivenCreationCallerIsAppointedByACurrentSigner() + external + whenCallingCreateProposal + givenOnlyListedIsTrue + { + // It creates the proposal - // 3 - actions[0].value = 2 ether; - bytes32 h3 = eMultisig.hashActions(actions); - assertNotEq(h2, h3, "Hashes should differ"); + vm.startPrank(alice); + encryptionRegistry.appointWallet(address(0x1234)); + vm.startPrank(address(0x1234)); + eMultisig.createProposal("a", 0, 0, optimisticPlugin, false); - // 4 - actions[0].data = hex"00112235"; - bytes32 h4 = eMultisig.hashActions(actions); - assertNotEq(h3, h4, "Hashes should differ"); + vm.startPrank(bob); + encryptionRegistry.appointWallet(address(0x2345)); + vm.startPrank(address(0x2345)); + eMultisig.createProposal("b", 0, 0, optimisticPlugin, false); - // 5 - actions = new IDAO.Action[](0); - bytes32 h5 = eMultisig.hashActions(actions); - assertNotEq(h4, h5, "Hashes should differ"); + vm.startPrank(carol); + encryptionRegistry.appointWallet(address(0x3456)); + vm.startPrank(address(0x3456)); + eMultisig.createProposal("c", 0, 0, optimisticPlugin, false); - // 5' - bytes32 h5b = eMultisig.hashActions(actions); - assertEq(h5, h5b, "Hashes should match"); + vm.startPrank(david); + encryptionRegistry.appointWallet(address(0x4567)); + vm.startPrank(address(0x4567)); + eMultisig.createProposal("d", 0, 0, optimisticPlugin, false); } - // CAN APPROVE + function test_GivenApproveProposalIsTrue() external whenCallingCreateProposal { + uint256 pid; + uint256 approvals; - function testFuzz_CanApproveReturnsfFalseIfNotCreated(uint256 randomProposalId) public view { - // returns `false` if the proposal doesn't exist + // It creates and calls approval in one go - assertEq(eMultisig.canApprove(randomProposalId, alice), false, "Should be false"); - assertEq(eMultisig.canApprove(randomProposalId, bob), false, "Should be false"); - assertEq(eMultisig.canApprove(randomProposalId, carol), false, "Should be false"); - assertEq(eMultisig.canApprove(randomProposalId, david), false, "Should be false"); - } - - function testFuzz_CanApproveReturnsfFalseIfNotListed(address randomWallet) public { - // returns `false` if the approver is not listed + vm.startPrank(alice); + pid = eMultisig.createProposal("a", 0, 0, optimisticPlugin, true); + (, approvals,,,,,) = eMultisig.getProposal(pid); + assertEq(approvals, 1, "Should be 1"); - { - // Leaving the deployment for fuzz efficiency + vm.startPrank(bob); + pid = eMultisig.createProposal("b", 0, 0, optimisticPlugin, true); + (, approvals,,,,,) = eMultisig.getProposal(pid); + assertEq(approvals, 1, "Should be 1"); + } - // Deploy a new stdMultisig instance - Multisig.MultisigSettings memory mSettings = Multisig.MultisigSettings({ - onlyListed: false, - minApprovals: 1, - destinationProposalDuration: 4 days, - proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - address[] memory signers = new address[](1); - signers[0] = address(0x0); + function test_GivenApproveProposalIsFalse() external whenCallingCreateProposal { + uint256 pid; + uint256 approvals; - stdMultisig = Multisig( - createProxyAndCall( - address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, mSettings)) - ) - ); - // New emergency stdMultisig using the above - EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ - onlyListed: false, - minApprovals: 1, - addresslistSource: stdMultisig, - proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - eMultisig = EmergencyMultisig( - createProxyAndCall( - address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) - ) - ); + // It only creates the proposal - vm.roll(block.number + 1); - } + vm.startPrank(alice); + pid = eMultisig.createProposal("a", 0, 0, optimisticPlugin, true); + (, approvals,,,,,) = eMultisig.getProposal(pid); + assertEq(approvals, 1, "Should be 1"); - uint256 pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); + vm.startPrank(bob); + pid = eMultisig.createProposal("b", 0, 0, optimisticPlugin, true); + (, approvals,,,,,) = eMultisig.getProposal(pid); + assertEq(approvals, 1, "Should be 1"); - // ko - if (randomWallet != address(0x0)) { - assertEq(eMultisig.canApprove(pid, randomWallet), false, "Should be false"); - } + vm.startPrank(carol); + pid = eMultisig.createProposal("c", 0, 0, optimisticPlugin, false); + (, approvals,,,,,) = eMultisig.getProposal(pid); + assertEq(approvals, 0, "Should be 0"); - // static ok - assertEq(eMultisig.canApprove(pid, address(0)), true, "Should be true"); + vm.startPrank(david); + pid = eMultisig.createProposal("d", 0, 0, optimisticPlugin, false); + (, approvals,,,,,) = eMultisig.getProposal(pid); + assertEq(approvals, 0, "Should be 0"); } - function test_CanApproveReturnsFalseIfApproved() public { - // returns `false` if the approver has already approved - builder = new DaoBuilder(); - (dao, optimisticPlugin, stdMultisig, eMultisig,,) = builder.withMultisigMember(alice).withMultisigMember(bob) - .withMultisigMember(carol).withMultisigMember(david).withMinApprovals(4).build(); + function test_WhenCallingHashActions() external view { + bytes32 hashedActions; + IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); + // It returns the right result + // It reacts to any of the values changing + // It same input produces the same output - // Alice - assertEq(eMultisig.canApprove(pid, alice), true, "Should be true"); - eMultisig.approve(pid); - assertEq(eMultisig.canApprove(pid, alice), false, "Should be false"); + hashedActions = eMultisig.hashActions(actions); + assertEq(hashedActions, hex"569e75fc77c1a856f6daaf9e69d8a9566ca34aa47f9133711ce065a571af0cfd"); - // Bob - assertEq(eMultisig.canApprove(pid, bob), true, "Should be true"); - vm.startPrank(bob); - eMultisig.approve(pid); - assertEq(eMultisig.canApprove(pid, bob), false, "Should be false"); + actions = new IDAO.Action[](1); + actions[0] = IDAO.Action(address(0), 0, bytes(string(""))); + hashedActions = eMultisig.hashActions(actions); + assertEq(hashedActions, hex"7cde746dfbb8dfd7721b5995769f873e3ff50416302673a354990b553bb0e208"); - // Carol - assertEq(eMultisig.canApprove(pid, carol), true, "Should be true"); - vm.startPrank(carol); - eMultisig.approve(pid); - assertEq(eMultisig.canApprove(pid, carol), false, "Should be false"); + actions = new IDAO.Action[](1); + actions[0] = IDAO.Action(bob, 1 ether, bytes(string(""))); + hashedActions = eMultisig.hashActions(actions); + assertEq(hashedActions, hex"e212a57e4595f81151b46333ea31e2d5043b53bd562141e1efa1b2778cb3c208"); - // David - assertEq(eMultisig.canApprove(pid, david), true, "Should be true"); - vm.startPrank(david); - eMultisig.approve(pid); - assertEq(eMultisig.canApprove(pid, david), false, "Should be false"); + actions = new IDAO.Action[](2); + actions[0] = IDAO.Action(bob, 1 ether, bytes(string(""))); + actions[1] = IDAO.Action(carol, 2 ether, bytes(string("data"))); + hashedActions = eMultisig.hashActions(actions); + assertEq(hashedActions, hex"4be399aee320511a56f584fae21b92c78f47bff143ec3965b7d911776d39bc7d"); } - function test_CanApproveReturnsFalseIfExpired() public { - // returns `false` if the proposal has ended + modifier givenTheProposalIsNotCreated() { + // Alice: listed and self appointed - uint256 pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); - - assertEq(eMultisig.canApprove(pid, alice), true, "Should be true"); + // Bob: listed, appointing someone else now + vm.startPrank(bob); + encryptionRegistry.appointWallet(randomWallet); - vm.warp(block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD - 1); // expiration time - 1 - assertEq(eMultisig.canApprove(pid, alice), true, "Should be true"); + // Random Wallet: appointed by a listed signer - vm.warp(block.timestamp + 1); // expiration time - assertEq(eMultisig.canApprove(pid, alice), false, "Should be false"); + // 0x1234: unlisted and unappointed - // Start later - vm.warp(50 days); - pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); + _; + } - assertEq(eMultisig.canApprove(pid, alice), true, "Should be true"); + function test_WhenCallingGetProposalBeingUncreated() external givenTheProposalIsNotCreated { + // It should return empty values + bool executed; + uint16 approvals; + EmergencyMultisig.ProposalParameters memory parameters; + bytes memory encryptedPayloadURI; + bytes32 publicMetadataUriHash; + bytes32 destinationActionsHash; + OptimisticTokenVotingPlugin destinationPlugin; - vm.warp(block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD - 1); // expiration time - 1 - assertEq(eMultisig.canApprove(pid, alice), true, "Should be true"); + ( + executed, + approvals, + parameters, + encryptedPayloadURI, + publicMetadataUriHash, + destinationActionsHash, + destinationPlugin + ) = eMultisig.getProposal(1234); - vm.warp(block.timestamp + 1); // expiration time - assertEq(eMultisig.canApprove(pid, alice), false, "Should be false"); + assertEq(executed, false, "Should be false"); + assertEq(approvals, 0, "Should be 0"); + assertEq(parameters.minApprovals, 0, "Incorrect minApprovals"); + assertEq(parameters.snapshotBlock, 0, "Incorrect snapshotBlock"); + assertEq(parameters.expirationDate, 0, "Incorrect expirationDate"); + assertEq(encryptedPayloadURI, "", "Incorrect encryptedPayloadURI"); + assertEq(publicMetadataUriHash, bytes32(0), "Incorrect publicMetadataUriHash"); + assertEq(destinationActionsHash, bytes32(0), "Incorrect destinationActionsHash"); + assertEq(address(destinationPlugin), address(0), "Incorrect destinationPlugin"); } - function test_CanApproveReturnsFalseIfExecuted() public { - // returns `false` if the proposal is already executed + function testFuzz_GetProposalReturnsEmptyValuesForNonExistingOnes(uint256 randomProposalId) public view { + ( + bool executed, + uint16 approvals, + EmergencyMultisig.ProposalParameters memory parameters, + bytes memory encryptedPayloadURI, + bytes32 publicMetadataUriHash, + bytes32 destinationActionsHash, + OptimisticTokenVotingPlugin destinationPlugin + ) = eMultisig.getProposal(randomProposalId); + + assertEq(executed, false, "The proposal should not be executed"); + assertEq(approvals, 0, "The tally should be zero"); + assertEq(encryptedPayloadURI, "", "Incorrect encryptedPayloadURI"); + assertEq(parameters.expirationDate, 0, "Incorrect expirationDate"); + assertEq(parameters.snapshotBlock, 0, "Incorrect snapshotBlock"); + assertEq(parameters.minApprovals, 0, "Incorrect minApprovals"); + assertEq(publicMetadataUriHash, 0, "Metadata URI hash should have no items"); + assertEq(destinationActionsHash, 0, "Actions hash should have no items"); + assertEq(address(destinationPlugin), address(0), "Incorrect destination plugin"); + } - bool executed; - IDAO.Action[] memory actions = new IDAO.Action[](0); - bytes32 metadataUriHash = keccak256("ipfs://"); - bytes32 actionsHash = eMultisig.hashActions(actions); - uint256 pid = eMultisig.createProposal("", metadataUriHash, actionsHash, optimisticPlugin, false); + function test_WhenCallingCanApproveOrApproveBeingUncreated() external givenTheProposalIsNotCreated { + uint256 randomProposalId = 1234; + bool canApprove; - // Alice - eMultisig.approve(pid); + // It canApprove should return false (when currently listed and self appointed) + vm.startPrank(alice); + canApprove = eMultisig.canApprove(randomProposalId, alice); + assertEq(canApprove, false, "Should be false"); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + // It approve should revert (when currently listed and self appointed) + vm.expectRevert( + abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, randomProposalId, alice) + ); + eMultisig.approve(randomProposalId); - // Bob + // It canApprove should return false (when currently listed, appointing someone else now) + randomProposalId++; vm.startPrank(bob); - eMultisig.approve(pid); - - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + canApprove = eMultisig.canApprove(randomProposalId, bob); + assertEq(canApprove, false, "Should be false"); - // Carol - vm.startPrank(carol); - eMultisig.approve(pid); // passed + // It approve should revert (when currently listed, appointing someone else now) + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, randomProposalId, bob)); + eMultisig.approve(randomProposalId); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + // It canApprove should return false (when appointed by a listed signer) + randomProposalId++; + vm.startPrank(randomWallet); + canApprove = eMultisig.canApprove(randomProposalId, randomWallet); + assertEq(canApprove, false, "Should be false"); - eMultisig.execute(pid, "ipfs://", actions); + // It approve should revert (when appointed by a listed signer) + vm.expectRevert( + abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, randomProposalId, randomWallet) + ); + eMultisig.approve(randomProposalId); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, true, "Should be executed"); + // It canApprove should return false (when currently unlisted and unappointed) + randomProposalId++; + vm.startPrank(address(1234)); + canApprove = eMultisig.canApprove(randomProposalId, address(1234)); + assertEq(canApprove, false, "Should be false"); - // David cannot approve - assertEq(eMultisig.canApprove(pid, david), false, "Should be false"); + // It approve should revert (when currently unlisted and unappointed) + vm.expectRevert( + abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, randomProposalId, address(1234)) + ); + eMultisig.approve(randomProposalId); } - function test_CanApproveReturnsTrueIfListed() public { - // returns `true` if the approver is listed + function test_WhenCallingHasApprovedBeingUncreated() external givenTheProposalIsNotCreated { + bool hasApproved; + uint256 randomProposalId = 1234; + // It hasApproved should always return false - vm.warp(10); + hasApproved = eMultisig.hasApproved(randomProposalId, alice); + assertEq(hasApproved, false, "Should be false"); - uint256 pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); + randomProposalId++; + hasApproved = eMultisig.hasApproved(randomProposalId, bob); + assertEq(hasApproved, false, "Should be false"); - assertEq(eMultisig.canApprove(pid, alice), true, "Should be true"); - assertEq(eMultisig.canApprove(pid, bob), true, "Should be true"); - assertEq(eMultisig.canApprove(pid, carol), true, "Should be true"); - assertEq(eMultisig.canApprove(pid, david), true, "Should be true"); + randomProposalId++; + hasApproved = eMultisig.hasApproved(randomProposalId, randomWallet); + assertEq(hasApproved, false, "Should be false"); - // new setup - builder = new DaoBuilder(); - (dao, optimisticPlugin, stdMultisig, eMultisig,,) = - builder.withMultisigMember(randomWallet).withMinApprovals(1).withMinDuration(0).build(); + randomProposalId++; + hasApproved = eMultisig.hasApproved(randomProposalId, address(0x5555)); + assertEq(hasApproved, false, "Should be false"); + } - // now ko - vm.startPrank(randomWallet); - pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); + function test_WhenCallingCanExecuteOrExecuteBeingUncreated() external givenTheProposalIsNotCreated { + uint256 randomProposalId = 1234; + // It canExecute should always return false + + bool canExecute = eMultisig.canExecute(randomProposalId); + assertEq(canExecute, false, "Should be false"); + } - assertEq(eMultisig.canApprove(pid, alice), false, "Should be false"); - assertEq(eMultisig.canApprove(pid, bob), false, "Should be false"); - assertEq(eMultisig.canApprove(pid, carol), false, "Should be false"); - assertEq(eMultisig.canApprove(pid, david), false, "Should be false"); + function testFuzz_WhenCallingCanExecuteOrExecuteBeingUncreated(uint256 randomProposalId) + external + givenTheProposalIsNotCreated + { + // It canExecute should always return false - // ok - assertEq(eMultisig.canApprove(pid, randomWallet), true, "Should be true"); + bool canExecute = eMultisig.canExecute(randomProposalId); + assertEq(canExecute, false, "Should be false"); } - // HAS APPROVED + modifier givenTheProposalIsOpen() { + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 1)); - function test_HasApprovedReturnsFalseWhenNotApproved() public { - // returns `false` if user hasn't approved yet + // Alice: listed on creation and self appointed - uint256 pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); + // Bob: listed on creation, appointing someone else now + vm.startPrank(bob); + encryptionRegistry.appointWallet(randomWallet); - // Alice - assertEq(eMultisig.hasApproved(pid, alice), false, "Should be false"); - assertEq(eMultisig.hasApproved(pid, bob), false, "Should be false"); - assertEq(eMultisig.hasApproved(pid, carol), false, "Should be false"); - assertEq(eMultisig.hasApproved(pid, david), false, "Should be false"); - } + // Random Wallet: appointed by a listed signer on creation - function test_HasApprovedReturnsTrueWhenUserApproved() public { - // returns `true` if user has approved + // 0x1234: unlisted and unappointed on creation - uint256 pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); + vm.deal(address(dao), 1 ether); - // Alice - assertEq(eMultisig.hasApproved(pid, alice), false, "Should be false"); - eMultisig.approve(pid); - assertEq(eMultisig.hasApproved(pid, alice), true, "Should be true"); + // Create proposal + IDAO.Action[] memory actions = new IDAO.Action[](1); + actions[0].value = 1 ether; + actions[0].to = address(bob); + actions[0].data = hex""; + bytes32 metadataUriHash = keccak256("ipfs://the-metadata"); + bytes32 actionsHash = eMultisig.hashActions(actions); + eMultisig.createProposal("ipfs://encrypted", metadataUriHash, actionsHash, optimisticPlugin, false); - // Bob - vm.startPrank(bob); - assertEq(eMultisig.hasApproved(pid, bob), false, "Should be false"); - eMultisig.approve(pid); - assertEq(eMultisig.hasApproved(pid, bob), true, "Should be true"); + // Remove (later) + vm.roll(block.number + 50); + address[] memory addrs = new address[](2); + addrs[0] = alice; + addrs[1] = bob; - // Carol - vm.startPrank(carol); - assertEq(eMultisig.hasApproved(pid, carol), false, "Should be false"); - eMultisig.approve(pid); - assertEq(eMultisig.hasApproved(pid, carol), true, "Should be true"); + vm.startPrank(alice); + signerList.removeSigners(addrs); - // David - vm.startPrank(david); - assertEq(eMultisig.hasApproved(pid, david), false, "Should be false"); - eMultisig.approve(pid); - assertEq(eMultisig.hasApproved(pid, david), true, "Should be true"); + _; } - // APPROVE + function testFuzz_CanApproveReturnsfFalseIfNotCreated(uint256 randomProposalId) public view { + // returns `false` if the proposal doesn't exist + + assertEq(eMultisig.canApprove(randomProposalId, alice), false, "Should be false"); + assertEq(eMultisig.canApprove(randomProposalId, bob), false, "Should be false"); + assertEq(eMultisig.canApprove(randomProposalId, carol), false, "Should be false"); + assertEq(eMultisig.canApprove(randomProposalId, david), false, "Should be false"); + } function testFuzz_ApproveRevertsIfNotCreated(uint256 randomProposalId) public { // Reverts if the proposal doesn't exist @@ -1202,774 +1359,585 @@ contract EmergencyMultisigTest is AragonTest { eMultisig.approve(randomProposalId); } - function testFuzz_ApproveRevertsIfNotListed(address randomSigner) public { - // Reverts if the signer is not listed + function testFuzz_CanExecuteReturnsFalseIfNotCreated(uint256 randomProposalId) public view { + // returns `false` if the proposal doesn't exist - builder = new DaoBuilder(); - (,,, eMultisig,,) = builder.withMultisigMember(alice).withMinApprovals(1).build(); - uint256 pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); + assertEq(eMultisig.canExecute(randomProposalId), false, "Should be false"); + } - if (randomSigner == alice) { - return; - } + function testFuzz_ExecuteRevertsIfNotCreated(uint256 randomProposalId) public { + // reverts if the proposal doesn't exist - vm.startPrank(randomSigner); - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, pid, randomSigner)); - eMultisig.approve(pid); + IDAO.Action[] memory actions = new IDAO.Action[](0); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, randomProposalId)); + eMultisig.execute(randomProposalId, "", actions); } - function test_ApproveRevertsIfAlreadyApproved() public { - // reverts when approving multiple times + function test_WhenCallingGetProposalBeingOpen() external givenTheProposalIsOpen { + // It should return the right values - uint256 pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); - - // Alice - eMultisig.approve(pid); + // Get proposal returns the right values - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, pid, alice)); - eMultisig.approve(pid); + { + ( + bool executed, + uint16 approvals, + EmergencyMultisig.ProposalParameters memory parameters, + bytes memory encryptedPayloadURI, + bytes32 publicMetadataUriHash, + bytes32 destinationActionsHash, + OptimisticTokenVotingPlugin destinationPlugin + ) = eMultisig.getProposal(0); + + assertEq(executed, false); + assertEq(approvals, 0); + assertEq(parameters.minApprovals, 3); + assertEq(parameters.snapshotBlock, block.number - 1 - 50); // We made +50 to remove wallets + assertEq(parameters.expirationDate, block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD); + assertEq(encryptedPayloadURI, "ipfs://encrypted"); + assertEq(publicMetadataUriHash, hex"538a79dd5d5741d2d66c0b0ec46e102023a64f8e1e3caeacb6aa4b2b14662a0d"); + assertEq(destinationActionsHash, hex"e212a57e4595f81151b46333ea31e2d5043b53bd562141e1efa1b2778cb3c208"); + assertEq(address(destinationPlugin), address(optimisticPlugin)); + } + // new proposal - // Bob - vm.startPrank(bob); - eMultisig.approve(pid); + OptimisticTokenVotingPlugin newOptimisticPlugin; + (dao, newOptimisticPlugin,, eMultisig,,,,) = builder.build(); - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, pid, bob)); - eMultisig.approve(pid); + vm.deal(address(dao), 1 ether); - // Carol - vm.startPrank(carol); - eMultisig.approve(pid); + { + bytes32 metadataUriHash = keccak256("ipfs://another-public-metadata"); + + IDAO.Action[] memory actions = new IDAO.Action[](1); + actions[0].value = 1 ether; + actions[0].to = alice; + actions[0].data = hex""; + bytes32 actionsHash = eMultisig.hashActions(actions); + eMultisig.createProposal("ipfs://12340000", metadataUriHash, actionsHash, newOptimisticPlugin, true); + + ( + bool executed, + uint16 approvals, + EmergencyMultisig.ProposalParameters memory parameters, + bytes memory encryptedPayloadURI, + bytes32 publicMetadataUriHash, + bytes32 destinationActionsHash, + OptimisticTokenVotingPlugin destinationPlugin + ) = eMultisig.getProposal(0); + + assertEq(executed, false); + assertEq(approvals, 1); + assertEq(parameters.minApprovals, 3); + assertEq(parameters.snapshotBlock, block.number - 1); + assertEq(parameters.expirationDate, block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD); + assertEq(encryptedPayloadURI, "ipfs://12340000"); + assertEq(publicMetadataUriHash, metadataUriHash); + assertEq(destinationActionsHash, actionsHash); + assertEq(address(destinationPlugin), address(newOptimisticPlugin)); + } + } - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, pid, carol)); - eMultisig.approve(pid); + function test_WhenCallingCanApproveOrApproveBeingOpen() external givenTheProposalIsOpen { + // It canApprove should return true (when listed on creation, self appointed now) + bool canApprove = eMultisig.canApprove(0, alice); + assertEq(canApprove, true, "Alice should be able to approve"); + // It approve should work (when listed on creation, self appointed now) + // It approve should emit an event (when listed on creation, self appointed now) vm.startPrank(alice); - } + vm.expectEmit(); + emit Approved(0, alice); + eMultisig.approve(0); - function test_ApprovesWithTheSenderAddress() public { - // approves with the msg.sender address - // Same as test_HasApprovedReturnsTrueWhenUserApproved() + // It canApprove should return false (when listed on creation, appointing someone else now) + canApprove = eMultisig.canApprove(0, bob); + assertEq(canApprove, false, "Bob should not be able to approve directly"); - uint256 pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); + // It approve should revert (when listed on creation, appointing someone else now) + vm.startPrank(bob); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, 0, bob)); + eMultisig.approve(0); - // Alice - assertEq(eMultisig.hasApproved(pid, alice), false, "Should be false"); - eMultisig.approve(pid); - assertEq(eMultisig.hasApproved(pid, alice), true, "Should be true"); + // It canApprove should return true (when currently appointed by a signer listed on creation) + canApprove = eMultisig.canApprove(0, randomWallet); + assertEq(canApprove, true, "Random wallet should be able to approve as appointed"); - // Bob - vm.startPrank(bob); - assertEq(eMultisig.hasApproved(pid, bob), false, "Should be false"); - eMultisig.approve(pid); - assertEq(eMultisig.hasApproved(pid, bob), true, "Should be true"); + // It approve should work (when currently appointed by a signer listed on creation) + // It approve should emit an event (when currently appointed by a signer listed on creation) + vm.startPrank(randomWallet); + vm.expectEmit(); + emit Approved(0, bob); // Note: Event shows the owner, not the appointed wallet + eMultisig.approve(0); + + // Check approval count + (, uint16 approvals,,,,,) = eMultisig.getProposal(0); + assertEq(approvals, 2, "Should have 2 approvals total"); - // Carol vm.startPrank(carol); - assertEq(eMultisig.hasApproved(pid, carol), false, "Should be false"); - eMultisig.approve(pid); - assertEq(eMultisig.hasApproved(pid, carol), true, "Should be true"); + assertEq(eMultisig.canApprove(0, carol), true, "Carol should be able to approve"); + eMultisig.approve(0); + + // Should approve, pass but not execute + bool executed; + (executed, approvals,,,,,) = eMultisig.getProposal(0); + assertEq(executed, false, "Should not have executed"); + assertEq(approvals, 3, "Should have 3 approvals total"); - // David + // More approvals vm.startPrank(david); - assertEq(eMultisig.hasApproved(pid, david), false, "Should be false"); - eMultisig.approve(pid); - assertEq(eMultisig.hasApproved(pid, david), true, "Should be true"); + assertEq(eMultisig.canApprove(0, david), true, "David should be able to approve"); + eMultisig.approve(0); - vm.startPrank(alice); + (executed, approvals,,,,,) = eMultisig.getProposal(0); + assertEq(executed, false, "Should not have executed"); + assertEq(approvals, 4, "Should have 4 approvals total"); } - function test_ApproveRevertsIfExpired() public { - // reverts if the proposal has ended - - uint256 pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); + function testFuzz_CanApproveReturnsfFalseIfNotListed(address randomWallet) public { + // returns `false` if the approver is not listed - assertEq(eMultisig.canApprove(pid, alice), true, "Should be true"); + { + // Leaving the deployment for fuzz efficiency + EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ + onlyListed: false, + minApprovals: 1, + signerList: signerList, + proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD + }); + eMultisig = EmergencyMultisig( + createProxyAndCall( + address(EMERGENCY_MULTISIG_BASE), abi.encodeCall(EmergencyMultisig.initialize, (dao, settings)) + ) + ); - vm.warp(block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD); - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, pid, alice)); - eMultisig.approve(pid); + vm.roll(block.number + 1); + } - vm.warp(block.timestamp + 15 days); - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, pid, alice)); - eMultisig.approve(pid); + uint256 pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); - // 2 - vm.warp(10 days); - pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); + // ko + if (randomWallet != alice && randomWallet != bob && randomWallet != carol && randomWallet != david) { + assertEq(eMultisig.canApprove(pid, randomWallet), false, "Should be false"); + } + // static ok assertEq(eMultisig.canApprove(pid, alice), true, "Should be true"); - - vm.warp(block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD); - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, pid, alice)); - eMultisig.approve(pid); - - vm.warp(block.timestamp + 15 days); - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, pid, alice)); - eMultisig.approve(pid); - } - - function test_ApproveRevertsIfExecuted() public { - // reverts if the proposal has ended - - IDAO.Action[] memory actions = new IDAO.Action[](0); - bytes32 metadataUriHash = keccak256("ipfs://"); - bytes32 actionsHash = eMultisig.hashActions(actions); - uint256 pid = eMultisig.createProposal("", metadataUriHash, actionsHash, optimisticPlugin, false); - eMultisig.approve(pid); - vm.startPrank(bob); - eMultisig.approve(pid); - vm.startPrank(carol); - eMultisig.approve(pid); - - eMultisig.execute(pid, "ipfs://", actions); - (bool executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, true, "Should be executed"); - - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, pid, carol)); - eMultisig.approve(pid); } - function test_ApprovingProposalsEmits() public { - // Approving a proposal emits the Approved event + function testFuzz_ApproveRevertsIfNotListed(address randomSigner) public { + // Reverts if the signer is not listed + builder = new DaoBuilder(); + (,,, eMultisig,,,,) = builder.withMultisigMember(alice).withMinApprovals(1).build(); uint256 pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); - vm.expectEmit(); - emit Approved(pid, alice); - eMultisig.approve(pid); - - // Bob - vm.startPrank(bob); - vm.expectEmit(); - emit Approved(pid, bob); - eMultisig.approve(pid); + if (randomSigner == alice) { + return; + } - // Carol - vm.startPrank(carol); - vm.expectEmit(); - emit Approved(pid, carol); + vm.startPrank(randomSigner); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, pid, randomSigner)); eMultisig.approve(pid); - // David (even if it already passed) - vm.startPrank(david); - vm.expectEmit(); - emit Approved(pid, david); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, pid, randomSigner)); eMultisig.approve(pid); } - // CAN EXECUTE + function test_WhenCallingHasApprovedBeingOpen() external givenTheProposalIsOpen { + // It hasApproved should return false until approved + assertEq(eMultisig.hasApproved(0, alice), false, "Should be false before approval"); + assertEq(eMultisig.hasApproved(0, bob), false, "Should be false before approval"); + assertEq(eMultisig.hasApproved(0, randomWallet), false, "Should be false before approval"); + assertEq(eMultisig.hasApproved(0, address(0x5555)), false, "5555 should not have approved"); - function testFuzz_CanExecuteReturnsFalseIfNotCreated(uint256 randomProposalId) public view { - // returns `false` if the proposal doesn't exist - - assertEq(eMultisig.canExecute(randomProposalId), false, "Should be false"); - } - - function test_CanExecuteReturnsFalseIfBelowMinApprovals() public { - // returns `false` if the proposal has not reached the minimum approvals yet - - (dao, optimisticPlugin, stdMultisig, eMultisig,,) = builder.withMinApprovals(2).build(); - - uint256 pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); - - // Alice - eMultisig.approve(pid); - assertEq(eMultisig.canExecute(pid), false, "Should be false"); - - // Bob - vm.startPrank(bob); - eMultisig.approve(pid); - assertEq(eMultisig.canExecute(pid), true, "Should be true"); - - vm.startPrank(alice); - - // More approvals required (4) - (dao, optimisticPlugin, stdMultisig, eMultisig,,) = builder.withMinApprovals(4).build(); - - pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); - - // Alice + // After approvals vm.startPrank(alice); - eMultisig.approve(pid); - assertEq(eMultisig.canExecute(pid), false, "Should be false"); - - // Bob - vm.startPrank(bob); - eMultisig.approve(pid); - assertEq(eMultisig.canExecute(pid), false, "Should be false"); + eMultisig.approve(0); + assertEq(eMultisig.hasApproved(0, alice), true, "Should be true after approval"); - // Carol - vm.startPrank(carol); - eMultisig.approve(pid); - assertEq(eMultisig.canExecute(pid), false, "Should be false"); + vm.startPrank(randomWallet); + eMultisig.approve(0); + assertEq(eMultisig.hasApproved(0, bob), true, "Should be true after approval by appointed wallet"); + assertEq(eMultisig.hasApproved(0, randomWallet), true, "Should be true after approval"); - // David - vm.startPrank(david); - eMultisig.approve(pid); - assertEq(eMultisig.canExecute(pid), true, "Should be true"); + assertEq(eMultisig.hasApproved(0, address(0x5555)), false, "5555 should not have approved"); } - function test_CanExecuteReturnsFalseIfExpired() public { - // returns `false` if the proposal has expired + function test_WhenCallingCanExecuteOrExecuteBeingOpen() external givenTheProposalIsOpen { + // It canExecute should return false (when listed on creation, self appointed now) + assertEq(eMultisig.canExecute(0), false, "Should not be executable without approvals"); - // 1 - uint256 pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); - - eMultisig.approve(pid); - vm.startPrank(bob); - eMultisig.approve(pid); - vm.startPrank(carol); - eMultisig.approve(pid); - assertEq(eMultisig.canExecute(pid), true, "Should be true"); - - vm.warp(block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD - 1); - assertEq(eMultisig.canExecute(pid), true, "Should be true"); - - vm.warp(block.timestamp + 1); - assertEq(eMultisig.canExecute(pid), false, "Should be false"); - - // 2 - vm.warp(50 days); + vm.deal(address(dao), 1 ether); - pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); + // It execute should revert (when listed on creation, self appointed now) + IDAO.Action[] memory actions = new IDAO.Action[](1); + actions[0].value = 1 ether; + actions[0].to = address(bob); + actions[0].data = hex""; vm.startPrank(alice); - eMultisig.approve(pid); - vm.startPrank(bob); - eMultisig.approve(pid); - vm.startPrank(carol); - eMultisig.approve(pid); - assertEq(eMultisig.canExecute(pid), true, "Should be true"); - - vm.warp(block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD - 1); - assertEq(eMultisig.canExecute(pid), true, "Should be true"); - - vm.warp(block.timestamp + 1); - assertEq(eMultisig.canExecute(pid), false, "Should be false"); - } + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, 0)); + eMultisig.execute(0, "ipfs://the-metadata", actions); - function test_CanExecuteReturnsFalseIfExecuted() public { - // returns `false` if the proposal is already executed - - IDAO.Action[] memory actions = new IDAO.Action[](0); - bytes32 metadataUriHash = keccak256("ipfs://"); - bytes32 actionsHash = eMultisig.hashActions(actions); - uint256 pid = eMultisig.createProposal("", metadataUriHash, actionsHash, optimisticPlugin, false); - - // Alice - eMultisig.approve(pid); - - // Bob - vm.startPrank(bob); - eMultisig.approve(pid); - - // Carol + // Get required approvals + eMultisig.approve(0); + vm.startPrank(randomWallet); // Appointed by Bob + eMultisig.approve(0); vm.startPrank(carol); - eMultisig.approve(pid); + eMultisig.approve(0); - assertEq(eMultisig.canExecute(pid), true, "Should be true"); - eMultisig.execute(pid, "ipfs://", actions); - - assertEq(eMultisig.canExecute(pid), false, "Should be false"); + // Now it should be executable + assertEq(eMultisig.canExecute(0), true, "Should be executable after approvals"); } - function test_CanExecuteReturnsTrueWhenAllGood() public { - // returns `true` if the proposal can be executed + modifier givenTheProposalWasApprovedByTheAddress() { + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 1)); - uint256 pid = eMultisig.createProposal("", 0, 0, optimisticPlugin, false); - assertEq(eMultisig.canExecute(pid), false, "Should be false"); + // Alice: listed on creation and self appointed - // Alice - eMultisig.approve(pid); - assertEq(eMultisig.canExecute(pid), false, "Should be false"); - - // Bob + // Bob: listed on creation, appointing someone else now vm.startPrank(bob); - eMultisig.approve(pid); - assertEq(eMultisig.canExecute(pid), false, "Should be false"); - - // Carol - vm.startPrank(carol); - eMultisig.approve(pid); - - assertEq(eMultisig.canExecute(pid), true, "Should be true"); - } - - // EXECUTE + encryptionRegistry.appointWallet(randomWallet); - function testFuzz_ExecuteRevertsIfNotCreated(uint256 randomProposalId) public { - // reverts if the proposal doesn't exist - - IDAO.Action[] memory actions = new IDAO.Action[](0); - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, randomProposalId)); - eMultisig.execute(randomProposalId, "", actions); - } + // Random Wallet: appointed by a listed signer on creation - function test_ExecuteRevertsIfBelowMinApprovals() public { - // reverts if minApprovals is not met yet + // 0x1234: unlisted and unappointed on creation - (dao, optimisticPlugin, stdMultisig, eMultisig,,) = builder.withMinApprovals(2).build(); + vm.deal(address(dao), 0.5 ether); - IDAO.Action[] memory actions = new IDAO.Action[](0); - bytes32 metadataUriHash = keccak256("ipfs://"); + // Create proposal + IDAO.Action[] memory actions = new IDAO.Action[](1); + actions[0].value = 0.5 ether; + actions[0].to = address(carol); + actions[0].data = hex""; + bytes32 metadataUriHash = keccak256("ipfs://more-metadata"); bytes32 actionsHash = eMultisig.hashActions(actions); - uint256 pid = eMultisig.createProposal("", metadataUriHash, actionsHash, optimisticPlugin, false); - - // Alice - eMultisig.approve(pid); - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, pid)); - eMultisig.execute(pid, "ipfs://", actions); - - // Bob - vm.startPrank(bob); - eMultisig.approve(pid); - eMultisig.execute(pid, "ipfs://", actions); // ok - - vm.startPrank(alice); + eMultisig.createProposal("ipfs://encrypted", metadataUriHash, actionsHash, optimisticPlugin, false); - // More approvals required (4) - (dao, optimisticPlugin, stdMultisig, eMultisig,,) = builder.withMinApprovals(4).build(); - - pid = eMultisig.createProposal("", metadataUriHash, actionsHash, optimisticPlugin, false); + // Remove (later) + vm.roll(block.number + 50); + address[] memory addrs = new address[](2); + addrs[0] = alice; + addrs[1] = bob; - // Alice vm.startPrank(alice); - eMultisig.approve(pid); - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, pid)); - eMultisig.execute(pid, "ipfs://", actions); - - // Bob - vm.startPrank(bob); - eMultisig.approve(pid); - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, pid)); - eMultisig.execute(pid, "ipfs://", actions); + signerList.removeSigners(addrs); - // Carol - vm.startPrank(carol); - eMultisig.approve(pid); - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, pid)); - eMultisig.execute(pid, "ipfs://", actions); + eMultisig.approve(0); - // David - vm.startPrank(david); - eMultisig.approve(pid); - eMultisig.execute(pid, "ipfs://", actions); + _; } - function test_ExecuteRevertsIfExpired() public { - // reverts if the proposal has expired + function test_WhenCallingGetProposalBeingApproved() external givenTheProposalWasApprovedByTheAddress { + // It should return the right values + uint256 pid = 0; - // 1 - IDAO.Action[] memory actions = new IDAO.Action[](0); - bytes32 metadataUriHash = keccak256("ipfs://"); - bytes32 actionsHash = eMultisig.hashActions(actions); - uint256 pid = eMultisig.createProposal("", metadataUriHash, actionsHash, optimisticPlugin, false); - - eMultisig.approve(pid); - vm.startPrank(bob); + vm.startPrank(randomWallet); // Appointed by Bob eMultisig.approve(pid); vm.startPrank(carol); eMultisig.approve(pid); - assertEq(eMultisig.canExecute(pid), true, "Should be true"); - vm.warp(block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD); - - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, pid)); - eMultisig.execute(pid, "ipfs://", actions); + ( + bool executed, + uint16 approvals, + EmergencyMultisig.ProposalParameters memory parameters, + bytes memory encryptedPayloadURI, + bytes32 publicMetadataUriHash, + bytes32 destinationActionsHash, + OptimisticTokenVotingPlugin destinationPlugin + ) = eMultisig.getProposal(pid); + assertEq(executed, false, "Should not be executed"); + assertEq(approvals, 3, "Should be 3"); - vm.warp(100 days); + assertEq(parameters.minApprovals, 3); + assertEq(parameters.snapshotBlock, block.number - 1 - 50); // We made +50 to remove wallets + assertEq(parameters.expirationDate, block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD); + assertEq(encryptedPayloadURI, "ipfs://encrypted"); + assertEq(publicMetadataUriHash, hex"1f4c56b7231f4b1bd019565da91d099db90671db977444a5f3c231dbd6013b27"); + assertEq(destinationActionsHash, hex"ed2486fa6e91780dba02ea013f95f9e84ae8250dcf4c7b62ea5b99fbcf682ee4"); + assertEq(address(destinationPlugin), address(optimisticPlugin)); + } - // 2 - pid = eMultisig.createProposal("", metadataUriHash, actionsHash, optimisticPlugin, false); + function test_WhenCallingCanApproveOrApproveBeingApproved() external givenTheProposalWasApprovedByTheAddress { + // It canApprove should return false (when listed on creation, self appointed now) + assertEq(eMultisig.canApprove(0, alice), false, "Alice should not be able to approve again"); + // It approve should revert (when listed on creation, self appointed now) vm.startPrank(alice); - eMultisig.approve(pid); - vm.startPrank(bob); - eMultisig.approve(pid); - vm.startPrank(carol); - eMultisig.approve(pid); - assertEq(eMultisig.canExecute(pid), true, "Should be true"); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, 0, alice)); + eMultisig.approve(0); - vm.warp(block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD); + // It canApprove should return true (when currently appointed by a signer listed on creation) + assertEq(eMultisig.canApprove(0, randomWallet), true, "Random wallet should be able to approve"); - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, pid)); - eMultisig.execute(pid, "ipfs://", actions); + // It approve should work (when currently appointed by a signer listed on creation) + vm.startPrank(randomWallet); + eMultisig.approve(0); } - function test_ExecuteRevertsWhenAlreadyExecuted() public { - // executes if the minimum approval is met when stdMultisig with the `tryExecution` option + function test_WhenCallingHasApprovedBeingApproved() external givenTheProposalWasApprovedByTheAddress { + // It hasApproved should return true for approved addresses + assertEq(eMultisig.hasApproved(0, alice), true, "Should be true for alice"); + assertEq(eMultisig.hasApproved(0, bob), false, "Should be false for bob"); + assertEq(eMultisig.hasApproved(0, randomWallet), false, "Should be false for randomWallet"); + assertEq(eMultisig.hasApproved(0, address(0x5555)), false, "5555 should not have approved"); - IDAO.Action[] memory actions = new IDAO.Action[](0); - bytes32 metadataUriHash = keccak256("ipfs://"); - bytes32 actionsHash = eMultisig.hashActions(actions); - uint256 pid = eMultisig.createProposal("", metadataUriHash, actionsHash, optimisticPlugin, false); - - // Alice - eMultisig.approve(pid); - - // Bob - vm.startPrank(bob); - eMultisig.approve(pid); - - // Carol - vm.startPrank(carol); - eMultisig.approve(pid); - - assertEq(eMultisig.canExecute(pid), true, "Should be true"); - eMultisig.execute(pid, "ipfs://", actions); + // After additional approval + vm.startPrank(randomWallet); + eMultisig.approve(0); + assertEq(eMultisig.hasApproved(0, bob), true, "Should be true for bob after appointed wallet approves"); + assertEq(eMultisig.hasApproved(0, randomWallet), true, "Should be true after approval"); - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, pid)); - eMultisig.execute(pid, "ipfs://", actions); + assertEq(eMultisig.hasApproved(0, address(0x5555)), false, "5555 should not have approved"); } - function test_ExecuteEmitsEvents() public { - // emits the `ProposalExecuted` and `ProposalCreated` events - - vm.warp(5 days); - vm.deal(address(dao), 1 ether); + function test_WhenCallingCanExecuteOrExecuteBeingApproved() external givenTheProposalWasApprovedByTheAddress { + // It canExecute should return false (when listed on creation, self appointed now) + assertEq(eMultisig.canExecute(0), false, "Should not be executable with only one approval"); - IDAO.Action[] memory actions = new IDAO.Action[](0); - bytes32 metadataUriHash = keccak256("ipfs://"); - bytes32 actionsHash = eMultisig.hashActions(actions); - uint256 pid = eMultisig.createProposal("", metadataUriHash, actionsHash, optimisticPlugin, false); - - // Alice - eMultisig.approve(pid); - - // Bob - vm.startPrank(bob); - eMultisig.approve(pid); - - // Carol - vm.startPrank(carol); - eMultisig.approve(pid); - - // event - vm.expectEmit(); - emit Executed(pid); - vm.expectEmit(); - uint256 targetPid = 5 days << 128 | 5 days << 64; - emit ProposalCreated(targetPid, address(eMultisig), 5 days, 5 days, "ipfs://", actions, 0); - eMultisig.execute(pid, "ipfs://", actions); - - // 2 - vm.warp(20 days); - actions = new IDAO.Action[](1); + // It execute should revert (when listed on creation, self appointed now) + IDAO.Action[] memory actions = new IDAO.Action[](1); actions[0].value = 1 ether; actions[0].to = address(bob); - actions[0].data = hex"00112233"; - actionsHash = eMultisig.hashActions(actions); - metadataUriHash = keccak256("ipfs://more-metadata-here"); - pid = eMultisig.createProposal("ipfs://", metadataUriHash, actionsHash, optimisticPlugin, false); + actions[0].data = hex""; - // Alice vm.startPrank(alice); - eMultisig.approve(pid); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, 0)); + eMultisig.execute(0, "ipfs://the-metadata", actions); - // Bob - vm.startPrank(bob); - eMultisig.approve(pid); - - // Carol - vm.startPrank(carol); - eMultisig.approve(pid); + // It canExecute should return false (when currently appointed by a signer listed on creation) + vm.startPrank(randomWallet); + assertEq(eMultisig.canExecute(0), false, "Should not be executable with only one approval"); - // events - vm.expectEmit(); - emit Executed(pid); - vm.expectEmit(); - targetPid = (20 days << 128 | 20 days << 64) + 1; - emit ProposalCreated(targetPid, address(eMultisig), 20 days, 20 days, "ipfs://more-metadata-here", actions, 0); - eMultisig.execute(pid, "ipfs://more-metadata-here", actions); + // It execute should revert (when currently appointed by a signer listed on creation) + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, 0)); + eMultisig.execute(0, "ipfs://the-metadata", actions); } - function test_ExecutesWithEnoughApprovalsOnTime() public { - // executes if the minimum approval is met + modifier givenTheProposalPassed() { + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 1)); - vm.deal(address(dao), 1 ether); + // Alice: listed on creation and self appointed - IDAO.Action[] memory actions = new IDAO.Action[](0); - bytes32 metadataUriHash = keccak256("ipfs://"); - bytes32 actionsHash = eMultisig.hashActions(actions); - uint256 pid = eMultisig.createProposal("", metadataUriHash, actionsHash, optimisticPlugin, false); - - // Alice - eMultisig.approve(pid); - (bool executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); - - // Bob + // Bob: listed on creation, appointing someone else now vm.startPrank(bob); - eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + encryptionRegistry.appointWallet(randomWallet); - // Carol - vm.startPrank(carol); - eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + // Random Wallet: appointed by a listed signer on creation - eMultisig.execute(pid, "ipfs://", actions); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, true, "Should be executed"); + // 0x1234: unlisted and unappointed on creation - // 2 - actions = new IDAO.Action[](1); + vm.deal(address(dao), 1 ether); + + // Create proposal + IDAO.Action[] memory actions = new IDAO.Action[](1); actions[0].value = 1 ether; actions[0].to = address(bob); - actions[0].data = hex"00112233"; + actions[0].data = hex""; + bytes32 metadataUriHash = keccak256("ipfs://the-original-secret-metadata"); + bytes32 actionsHash = eMultisig.hashActions(actions); + uint256 pid = + eMultisig.createProposal("ipfs://more-encrypted", metadataUriHash, actionsHash, optimisticPlugin, false); - actionsHash = eMultisig.hashActions(actions); - pid = eMultisig.createProposal("", metadataUriHash, actionsHash, optimisticPlugin, false); + // Remove (later) + vm.roll(block.number + 50); + address[] memory addrs = new address[](2); + addrs[0] = alice; + addrs[1] = bob; - // Alice vm.startPrank(alice); + signerList.removeSigners(addrs); + eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); - // Bob - vm.startPrank(bob); + vm.startPrank(randomWallet); eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); - // Carol vm.startPrank(carol); eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); - eMultisig.execute(pid, "ipfs://", actions); + vm.startPrank(alice); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, true, "Should be executed"); + _; } - function test_ExecuteRevertsWhentheGivenMetadaUriDoesntMatchTheHash() public { - vm.deal(address(dao), 1 ether); + function test_WhenCallingGetProposalBeingPassed() external givenTheProposalPassed { + // It should return the right values - IDAO.Action[] memory actions = new IDAO.Action[](0); - bytes32 metadataUriHash = 0; // Wrong hash - bytes32 actionsHash = eMultisig.hashActions(actions); - uint256 pid = eMultisig.createProposal("", metadataUriHash, actionsHash, optimisticPlugin, false); - - // Alice - eMultisig.approve(pid); - (bool executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); - - // Bob - vm.startPrank(bob); - eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); - - // Carol - vm.startPrank(carol); - eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); - - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.InvalidMetadataUri.selector, pid)); - eMultisig.execute(pid, "ipfs://", actions); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); - - // 2 - actions = new IDAO.Action[](1); - actions[0].value = 1 ether; - actions[0].to = address(bob); - actions[0].data = hex"00112233"; + // Retrieve the proposal + ( + bool executed, + uint16 approvals, + EmergencyMultisig.ProposalParameters memory parameters, + bytes memory encryptedPayloadURI, + bytes32 publicMetadataUriHash, + bytes32 destinationActionsHash, + OptimisticTokenVotingPlugin destinationPlugin + ) = eMultisig.getProposal(0); - metadataUriHash = keccak256("ipfs://correct-metadata-uri"); - actionsHash = eMultisig.hashActions(actions); - pid = eMultisig.createProposal("", metadataUriHash, actionsHash, optimisticPlugin, false); + // Assert the proposal is not executed + assertEq(executed, false, "Proposal should not be executed"); - // Alice - vm.startPrank(alice); - eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + // Assert the number of approvals + assertEq(approvals, 3, "Approvals should be 3"); - // Bob - vm.startPrank(bob); - eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + // Assert the proposal parameters + assertEq(parameters.minApprovals, 3, "Incorrect minApprovals"); + assertEq(parameters.snapshotBlock, block.number - 1 - 50, "Incorrect snapshotBlock"); + assertEq( + parameters.expirationDate, + block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD, + "Incorrect expirationDate" + ); - // Carol - vm.startPrank(carol); - eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + // Assert the encrypted payload URI + assertEq(encryptedPayloadURI, "ipfs://more-encrypted", "Incorrect encryptedPayloadURI"); - // Fake URI - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.InvalidMetadataUri.selector, pid)); - eMultisig.execute(pid, "ipfs://wrong-metadata-uri", actions); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + // Assert the public metadata URI hash + assertEq( + publicMetadataUriHash, keccak256("ipfs://the-original-secret-metadata"), "Incorrect publicMetadataUriHash" + ); - // With ok actions - eMultisig.execute(pid, "ipfs://correct-metadata-uri", actions); + // Assert the destination actions hash + assertEq( + destinationActionsHash, + hex"e212a57e4595f81151b46333ea31e2d5043b53bd562141e1efa1b2778cb3c208", + "Incorrect destinationActionsHash" + ); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, true, "Should be executed"); + // Assert the destination plugin + assertEq(address(destinationPlugin), address(optimisticPlugin), "Incorrect destinationPlugin"); } - function test_ExecuteRevertsWhenTheGivenActionsDontMatchTheHash() public { - vm.deal(address(dao), 1 ether); + function test_WhenCallingCanApproveOrApproveBeingPassed() external givenTheProposalPassed { + // It canApprove should return false (when listed on creation, self appointed now) + // vm.startPrank(alice); + assertEq(eMultisig.canApprove(0, alice), false, "Alice should not be able to approve"); + // It approve should revert (when listed on creation, self appointed now) + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, 0, alice)); + eMultisig.approve(0); - IDAO.Action[] memory actions = new IDAO.Action[](0); - bytes32 metadataUriHash = keccak256("ipfs://"); - bytes32 actionsHash = 0; // invalid hash - uint256 pid = eMultisig.createProposal("", metadataUriHash, actionsHash, optimisticPlugin, false); + // It canApprove should return false (when listed on creation, appointing someone else now) + vm.startPrank(bob); + assertEq(eMultisig.canApprove(0, bob), false, "Bob should not be able to approve"); + // It approve should revert (when listed on creation, appointing someone else now) + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, 0, bob)); + eMultisig.approve(0); - // Alice - eMultisig.approve(pid); - (bool executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + // It canApprove should return false (when currently appointed by a signer listed on creation) + vm.startPrank(randomWallet); + assertEq(eMultisig.canApprove(0, randomWallet), false, "Random wallet should not be able to approve"); + // It approve should revert (when currently appointed by a signer listed on creation) + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, 0, randomWallet)); + eMultisig.approve(0); - // Bob - vm.startPrank(bob); - eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + // It canApprove should return false (when unlisted on creation, unappointed now) + vm.startPrank(address(0x1234)); + assertEq(eMultisig.canApprove(0, address(0x1234)), false, "Random wallet should not be able to approve"); + // It approve should revert (when unlisted on creation, unappointed now) + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, 0, address(0x1234))); + eMultisig.approve(0); + } - // Carol - vm.startPrank(carol); - eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + function test_WhenCallingHasApprovedBeingPassed() external givenTheProposalPassed { + // It hasApproved should return false until approved - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.InvalidActions.selector, pid)); - eMultisig.execute(pid, "ipfs://", actions); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + assertEq(eMultisig.hasApproved(0, alice), true, "Alice should have approved"); + assertEq(eMultisig.hasApproved(0, bob), true, "Bob should have approved"); + assertEq(eMultisig.hasApproved(0, randomWallet), true, "Should be true"); + assertEq(eMultisig.hasApproved(0, address(0x5555)), false, "5555 should not have approved"); + } - // 2 - actions = new IDAO.Action[](1); + function test_WhenCallingCanExecuteOrExecuteWithModifiedDataBeingPassed() external givenTheProposalPassed { + // It execute should revert with modified metadata + IDAO.Action[] memory actions = new IDAO.Action[](1); actions[0].value = 1 ether; actions[0].to = address(bob); - actions[0].data = hex"00112233"; - - actionsHash = eMultisig.hashActions(actions); - pid = eMultisig.createProposal("", metadataUriHash, actionsHash, optimisticPlugin, false); - - // Alice - vm.startPrank(alice); - eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); - - // Bob - vm.startPrank(bob); - eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); - - // Carol - vm.startPrank(carol); - eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + actions[0].data = hex""; - // Fake actions - IDAO.Action[] memory otherActions = new IDAO.Action[](1); - otherActions[0].value = 10000 ether; - otherActions[0].to = address(carol); - otherActions[0].data = hex"44556677"; - vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.InvalidActions.selector, pid)); - eMultisig.execute(pid, "ipfs://", otherActions); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + // vm.startPrank(alice); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.InvalidMetadataUri.selector, 0)); + eMultisig.execute(0, "ipfs://modified-metadata-1234", actions); - // With ok actions - eMultisig.execute(pid, "ipfs://", actions); + // It execute should revert with modified actions + actions[0].value = 2 ether; // Modify action + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.InvalidActions.selector, 0)); + eMultisig.execute(0, "ipfs://the-original-secret-metadata", actions); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, true, "Should be executed"); + // It execute should work with matching data + actions[0].value = 1 ether; // Reset action + eMultisig.execute(0, "ipfs://the-original-secret-metadata", actions); } - function test_ExecuteWhenPassedAndCalledByAnyoneWithTheActions() public { - // executes if the minimum approval is met and can be called by an unlisted accounts - - vm.deal(address(dao), 4 ether); + function test_WhenCallingCanExecuteOrExecuteBeingPassed() external givenTheProposalPassed { + // It canExecute should return true, always + assertEq(eMultisig.canExecute(0), true, "Proposal should be executable"); + // It execute should work, when called by anyone with the actions IDAO.Action[] memory actions = new IDAO.Action[](1); actions[0].value = 1 ether; actions[0].to = address(bob); - actions[0].data = hex"00112233"; - bytes32 metadataUriHash = keccak256("ipfs://"); - bytes32 actionsHash = eMultisig.hashActions(actions); - uint256 pid = eMultisig.createProposal("", metadataUriHash, actionsHash, optimisticPlugin, false); - - // Alice - eMultisig.approve(pid); - (bool executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + actions[0].data = hex""; - // Bob - vm.startPrank(bob); - eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); - - // Carol - vm.startPrank(carol); - eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + // It execute should emit an event, when called by anyone with the actions + vm.expectEmit(); + emit Executed(0); - vm.startPrank(randomWallet); - eMultisig.execute(pid, "ipfs://", actions); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, true, "Should be executed"); + // It A ProposalCreated event is emitted on the destination plugin + uint256 targetPid = (block.timestamp << 128) | (block.timestamp << 64); + vm.expectEmit(); + emit ProposalCreated( + targetPid, + address(eMultisig), + uint64(block.timestamp), + uint64(block.timestamp), + "ipfs://the-original-secret-metadata", + actions, + 0 + ); - assertEq(bob.balance, 1 ether, "Incorrect balance"); - assertEq(address(dao).balance, 3 ether, "Incorrect balance"); + eMultisig.execute(0, "ipfs://the-original-secret-metadata", actions); - // 2 - vm.startPrank(alice); + // It execute recreates the proposal on the destination plugin - actions = new IDAO.Action[](1); - actions[0].value = 3 ether; - actions[0].to = address(carol); - actions[0].data = hex"0011223344556677"; - actionsHash = eMultisig.hashActions(actions); - pid = eMultisig.createProposal("", metadataUriHash, actionsHash, optimisticPlugin, false); + bool open; + bool executed; + OptimisticTokenVotingPlugin.ProposalParameters memory parameters; + uint256 vetoTally; + bytes memory metadataUri; + IDAO.Action[] memory retrievedActions; + uint256 allowFailureMap; - // Alice - eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + (open, executed, parameters, vetoTally, metadataUri, retrievedActions, allowFailureMap) = + optimisticPlugin.getProposal(targetPid); - // Bob - vm.startPrank(bob); - eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + // It The parameters of the recreated proposal match the hash of the executed one + assertEq(open, false, "Should not be open"); + // It Execution is immediate on the destination plugin + assertEq(executed, true, "Should be executed"); + assertEq(vetoTally, 0, "Should be 0"); - // Carol - vm.startPrank(carol); - eMultisig.approve(pid); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + assertEq(parameters.vetoEndDate, block.timestamp, "Incorrect vetoEndDate"); + assertEq(metadataUri, "ipfs://the-original-secret-metadata", "Incorrect target metadataUri"); - vm.startPrank(randomWallet); - eMultisig.execute(pid, "ipfs://", actions); + assertEq(retrievedActions.length, 1, "Should be 3"); - (executed,,,,,,) = eMultisig.getProposal(pid); - assertEq(executed, true, "Should be executed"); + assertEq(retrievedActions[0].to, bob, "Incorrect to"); + assertEq(retrievedActions[0].value, 1 ether, "Incorrect value"); + assertEq(retrievedActions[0].data, hex"", "Incorrect data"); - assertEq(carol.balance, 3 ether, "Incorrect balance"); - assertEq(address(dao).balance, 0, "Incorrect balance"); + assertEq(allowFailureMap, 0, "Should be 0"); } - function test_ExecutesSuccessfullyDespiteIncompatibleTaikoL1() public { - // executes even if the TaikoL1 contract reverts - (dao, optimisticPlugin,, eMultisig,,) = builder.withIncompatibleTaikoL1().build(); + function test_GivenTaikoL1IsIncompatible() external givenTheProposalPassed { + // It executes successfully, regardless + + (dao, optimisticPlugin,, eMultisig,,,,) = builder.withIncompatibleTaikoL1().build(); vm.deal(address(dao), 4 ether); @@ -2006,123 +1974,59 @@ contract EmergencyMultisigTest is AragonTest { assertEq(address(dao).balance, 3 ether, "Incorrect balance"); } - function test_GetProposalReturnsTheRightValues() public { - // Get proposal returns the right values + modifier givenTheProposalIsAlreadyExecuted() { + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 1)); - vm.warp(10); - IDAO.Action[] memory actions = new IDAO.Action[](1); - actions[0].value = 1 ether; - actions[0].to = address(bob); - actions[0].data = hex"00112233"; - bytes32 metadataUriHash = keccak256("ipfs://the-metadata"); - bytes32 actionsHash = eMultisig.hashActions(actions); - uint256 pid = eMultisig.createProposal("ipfs://", metadataUriHash, actionsHash, optimisticPlugin, false); + // Alice: listed on creation and self appointed - ( - bool executed, - uint16 approvals, - EmergencyMultisig.ProposalParameters memory parameters, - bytes memory encryptedPayloadURI, - bytes32 publicMetadataUriHash, - bytes32 destinationActionsHash, - OptimisticTokenVotingPlugin destinationPlugin - ) = eMultisig.getProposal(pid); + // Bob: listed on creation, appointing someone else now + vm.startPrank(bob); + encryptionRegistry.appointWallet(randomWallet); - assertEq(executed, false); - assertEq(approvals, 0); - assertEq(parameters.minApprovals, 3); - assertEq(parameters.snapshotBlock, block.number - 1); - assertEq(parameters.expirationDate, block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD); - assertEq(encryptedPayloadURI, "ipfs://"); - assertEq(publicMetadataUriHash, metadataUriHash); - assertEq(destinationActionsHash, actionsHash); - assertEq(address(destinationPlugin), address(optimisticPlugin)); + // Random Wallet: appointed by a listed signer on creation + + // 0x1234: unlisted and unappointed on creation - // 2 new proposal - OptimisticTokenVotingPlugin newOptimisticPlugin; - (dao, newOptimisticPlugin, stdMultisig, eMultisig,,) = builder.build(); vm.deal(address(dao), 1 ether); - metadataUriHash = keccak256("ipfs://another-public-metadata"); - actions[0].to = alice; - actionsHash = eMultisig.hashActions(actions); - pid = eMultisig.createProposal("ipfs://12340000", metadataUriHash, actionsHash, newOptimisticPlugin, true); + // Create proposal + IDAO.Action[] memory actions = new IDAO.Action[](1); + actions[0].value = 0.8 ether; + actions[0].to = address(bob); + actions[0].data = hex""; + bytes32 metadataUriHash = keccak256("ipfs://the-orig-metadata"); + bytes32 actionsHash = eMultisig.hashActions(actions); + uint256 pid = + eMultisig.createProposal("ipfs://encrypted", metadataUriHash, actionsHash, optimisticPlugin, false); - ( - executed, - approvals, - parameters, - encryptedPayloadURI, - publicMetadataUriHash, - destinationActionsHash, - destinationPlugin - ) = eMultisig.getProposal(pid); + // Remove (later) + vm.roll(block.number + 50); + address[] memory addrs = new address[](2); + addrs[0] = alice; + addrs[1] = bob; - assertEq(executed, false); - assertEq(approvals, 1); - assertEq(parameters.minApprovals, 3); - assertEq(parameters.snapshotBlock, block.number - 1); - assertEq(parameters.expirationDate, block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD); - assertEq(encryptedPayloadURI, "ipfs://12340000"); - assertEq(publicMetadataUriHash, metadataUriHash); - assertEq(destinationActionsHash, actionsHash); - assertEq(address(destinationPlugin), address(newOptimisticPlugin)); + vm.startPrank(alice); + signerList.removeSigners(addrs); - // 3 approve - vm.startPrank(bob); eMultisig.approve(pid); - vm.startPrank(carol); + + vm.startPrank(randomWallet); eMultisig.approve(pid); - ( - executed, - approvals, - parameters, - encryptedPayloadURI, - publicMetadataUriHash, - destinationActionsHash, - destinationPlugin - ) = eMultisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); - assertEq(approvals, 3, "Should be 3"); + vm.startPrank(carol); + eMultisig.approve(pid); - assertEq(parameters.minApprovals, 3); - assertEq(parameters.snapshotBlock, block.number - 1); - assertEq(parameters.expirationDate, block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD); - assertEq(encryptedPayloadURI, "ipfs://12340000"); - assertEq(publicMetadataUriHash, metadataUriHash); - assertEq(destinationActionsHash, actionsHash); - assertEq(address(destinationPlugin), address(newOptimisticPlugin)); + eMultisig.execute(pid, "ipfs://the-orig-metadata", actions); - // Execute vm.startPrank(alice); - dao.grant(address(newOptimisticPlugin), address(eMultisig), newOptimisticPlugin.PROPOSER_PERMISSION_ID()); - eMultisig.execute(pid, "ipfs://another-public-metadata", actions); - - // 4 execute - ( - executed, - approvals, - parameters, - encryptedPayloadURI, - publicMetadataUriHash, - destinationActionsHash, - destinationPlugin - ) = eMultisig.getProposal(pid); - - assertEq(executed, true, "Should be executed"); - assertEq(approvals, 3, "Should be 3"); - assertEq(parameters.minApprovals, 3); - assertEq(parameters.snapshotBlock, block.number - 1); - assertEq(parameters.expirationDate, block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD); - assertEq(encryptedPayloadURI, "ipfs://12340000"); - assertEq(publicMetadataUriHash, metadataUriHash); - assertEq(destinationActionsHash, actionsHash); - assertEq(address(destinationPlugin), address(newOptimisticPlugin)); + _; } - function testFuzz_GetProposalReturnsEmptyValuesForNonExistingOnes(uint256 randomProposalId) public view { + function test_WhenCallingGetProposalBeingExecuted() external givenTheProposalIsAlreadyExecuted { + // It should return the right values ( bool executed, uint16 approvals, @@ -2131,220 +2035,275 @@ contract EmergencyMultisigTest is AragonTest { bytes32 publicMetadataUriHash, bytes32 destinationActionsHash, OptimisticTokenVotingPlugin destinationPlugin - ) = eMultisig.getProposal(randomProposalId); + ) = eMultisig.getProposal(0); - assertEq(executed, false, "The proposal should not be executed"); - assertEq(approvals, 0, "The tally should be zero"); - assertEq(encryptedPayloadURI, "", "Incorrect encryptedPayloadURI"); - assertEq(parameters.expirationDate, 0, "Incorrect expirationDate"); - assertEq(parameters.snapshotBlock, 0, "Incorrect snapshotBlock"); - assertEq(parameters.minApprovals, 0, "Incorrect minApprovals"); - assertEq(publicMetadataUriHash, 0, "Metadata URI hash should have no items"); - assertEq(destinationActionsHash, 0, "Actions hash should have no items"); - assertEq(address(destinationPlugin), address(0), "Incorrect destination plugin"); - } + assertEq(executed, true, "Proposal should be executed"); + assertEq(approvals, 3, "Approvals should be 3"); - function test_ProxiedProposalHasTheSameSettingsAsTheOriginal() public { - // Recreated proposal has the same settings and actions as registered here + assertEq(parameters.minApprovals, 3, "Incorrect minApprovals"); + assertEq(parameters.snapshotBlock, block.number - 1 - 50, "Incorrect snapshotBlock"); + assertEq( + parameters.expirationDate, + block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD, + "Incorrect expirationDate" + ); + assertEq(encryptedPayloadURI, "ipfs://encrypted"); + assertEq(publicMetadataUriHash, keccak256("ipfs://the-orig-metadata")); + assertEq(destinationActionsHash, hex"c85c954206700a1f89dfd6599c77677611fdbc8dcb7f15d44158e10b46d13391"); + assertEq(address(destinationPlugin), address(optimisticPlugin), "Incorrect destinationPlugin"); + } + + function test_WhenCallingCanApproveOrApproveBeingExecuted() external givenTheProposalIsAlreadyExecuted { + // It canApprove should return false (when listed on creation, self appointed now) + // It canApprove should return false (when listed on creation, appointing someone else now) + // It canApprove should return false (when currently appointed by a signer listed on creation) + // It canApprove should return false (when unlisted on creation, unappointed now) + assertEq(eMultisig.canApprove(0, alice), false, "Alice should not be able to approve"); + assertEq(eMultisig.canApprove(0, bob), false, "Bob should not be able to approve"); + assertEq(eMultisig.canApprove(0, randomWallet), false, "Random wallet should not be able to approve"); + assertEq(eMultisig.canApprove(0, address(0x890a)), false, "Random wallet should not be able to approve"); + + // It approve should revert (when listed on creation, self appointed now) + // It approve should revert (when listed on creation, appointing someone else now) + // It approve should revert (when currently appointed by a signer listed on creation) + // It approve should revert (when unlisted on creation, unappointed now) + vm.startPrank(alice); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, 0, alice)); + eMultisig.approve(0); - bool open; - bool executed; - OptimisticTokenVotingPlugin.ProposalParameters memory parameters; - uint256 vetoTally; - bytes memory metadataUri; - IDAO.Action[] memory retrievedActions; - uint256 allowFailureMap; + vm.startPrank(bob); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, 0, bob)); + eMultisig.approve(0); - vm.warp(10 days); - vm.deal(address(dao), 100 ether); - - IDAO.Action[] memory submittedActions = new IDAO.Action[](3); - submittedActions[0].to = alice; - submittedActions[0].value = 1 ether; - submittedActions[0].data = hex""; - submittedActions[1].to = bob; - submittedActions[1].value = 2 ether; - submittedActions[1].data = hex""; - submittedActions[2].to = carol; - submittedActions[2].value = 3 ether; - submittedActions[2].data = hex""; - uint256 pid = eMultisig.createProposal( - "ipfs://encrypted-metadata", - keccak256("ipfs://target-metadata-uri"), - eMultisig.hashActions(submittedActions), - optimisticPlugin, - false - ); + vm.startPrank(randomWallet); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, 0, randomWallet)); + eMultisig.approve(0); - // Approve - eMultisig.approve(pid); + vm.startPrank(address(0x890a)); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, 0, address(0x890a))); + eMultisig.approve(0); + } + + function test_WhenCallingHasApprovedBeingExecuted() external givenTheProposalIsAlreadyExecuted { + // It hasApproved should return false until approved + + // Assert hasApproved returns true for those who approved + assertEq(eMultisig.hasApproved(0, alice), true, "Alice should have approved"); + assertEq(eMultisig.hasApproved(0, bob), true, "Bob should have approved"); + assertEq(eMultisig.hasApproved(0, randomWallet), true, "Random wallet should have approved"); + assertEq(eMultisig.hasApproved(0, address(0x5555)), false, "5555 should not have approved"); + } + + function test_WhenCallingCanExecuteOrExecuteBeingExecuted() external givenTheProposalIsAlreadyExecuted { + // It canExecute should return false (when listed on creation, self appointed now) + // It canExecute should return false (when listed on creation, appointing someone else now) + // It canExecute should return false (when currently appointed by a signer listed on creation) + // It canExecute should return false (when unlisted on creation, unappointed now) + // vm.startPrank(alice); + assertEq(eMultisig.canExecute(0), false, "Proposal should not be executable"); vm.startPrank(bob); - eMultisig.approve(pid); - vm.startPrank(carol); - eMultisig.approve(pid); + assertEq(eMultisig.canExecute(0), false, "Proposal should not be executable"); + vm.startPrank(randomWallet); + assertEq(eMultisig.canExecute(0), false, "Proposal should not be executable"); + vm.startPrank(address(0x7890)); + assertEq(eMultisig.canExecute(0), false, "Proposal should not be executable"); + + // It execute should revert (when listed on creation, self appointed now) + // It execute should revert (when listed on creation, appointing someone else now) + // It execute should revert (when currently appointed by a signer listed on creation) + // It execute should revert (when unlisted on creation, unappointed now) + IDAO.Action[] memory actions = new IDAO.Action[](1); + actions[0].value = 0.8 ether; + actions[0].to = address(bob); + actions[0].data = hex""; vm.startPrank(alice); - eMultisig.execute(pid, "ipfs://target-metadata-uri", submittedActions); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, 0)); + eMultisig.execute(0, "ipfs://the-orig-metadata", actions); - // Check round - (open, executed, parameters, vetoTally, metadataUri, retrievedActions, allowFailureMap) = - optimisticPlugin.getProposal((uint256(block.timestamp) << 128 | uint256(block.timestamp) << 64)); + vm.startPrank(bob); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, 0)); + eMultisig.execute(0, "ipfs://the-orig-metadata", actions); - assertEq(open, false, "Should not be open"); - assertEq(executed, true, "Should be executed"); - assertEq(vetoTally, 0, "Should be 0"); + vm.startPrank(randomWallet); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, 0)); + eMultisig.execute(0, "ipfs://the-orig-metadata", actions); - assertEq(parameters.vetoEndDate, block.timestamp, "Incorrect vetoEndDate"); - assertEq(metadataUri, "ipfs://target-metadata-uri", "Incorrect target metadataUri"); + vm.startPrank(address(0x7890)); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, 0)); + eMultisig.execute(0, "ipfs://the-orig-metadata", actions); + } - assertEq(retrievedActions.length, 3, "Should be 3"); + modifier givenTheProposalExpired() { + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 1)); - assertEq(retrievedActions[0].to, alice, "Incorrect to"); - assertEq(retrievedActions[0].value, 1 ether, "Incorrect value"); - assertEq(retrievedActions[0].data, hex"", "Incorrect data"); - assertEq(retrievedActions[1].to, bob, "Incorrect to"); - assertEq(retrievedActions[1].value, 2 ether, "Incorrect value"); - assertEq(retrievedActions[1].data, hex"", "Incorrect data"); - assertEq(retrievedActions[2].to, carol, "Incorrect to"); - assertEq(retrievedActions[2].value, 3 ether, "Incorrect value"); - assertEq(retrievedActions[2].data, hex"", "Incorrect data"); + // Alice: listed on creation and self appointed - assertEq(allowFailureMap, 0, "Should be 0"); + // Bob: listed on creation, appointing someone else now + vm.startPrank(bob); + encryptionRegistry.appointWallet(randomWallet); - // New proposal - vm.warp(15 days); + // Random Wallet: appointed by a listed signer on creation - submittedActions = new IDAO.Action[](2); - submittedActions[1].to = address(dao); - submittedActions[1].value = 0; - submittedActions[1].data = abi.encodeWithSelector(DAO.daoURI.selector); - submittedActions[0].to = address(stdMultisig); - submittedActions[0].value = 0; - submittedActions[0].data = abi.encodeWithSelector(Addresslist.addresslistLength.selector); - pid = eMultisig.createProposal( - "ipfs://encrypted-metadata", - keccak256("ipfs://new-metadata-here"), - eMultisig.hashActions(submittedActions), - optimisticPlugin, - false - ); + // 0x1234: unlisted and unappointed on creation + + vm.deal(address(dao), 1 ether); + + // Create proposal + IDAO.Action[] memory actions = new IDAO.Action[](1); + actions[0].value = 1.5 ether; + actions[0].to = address(carol); + actions[0].data = hex""; + bytes32 metadataUriHash = keccak256("ipfs://the-metadata"); + bytes32 actionsHash = eMultisig.hashActions(actions); + uint256 pid = + eMultisig.createProposal("ipfs://encrypted", metadataUriHash, actionsHash, optimisticPlugin, false); + + // Remove (later) + vm.roll(block.number + 50); + address[] memory addrs = new address[](2); + addrs[0] = alice; + addrs[1] = bob; + + vm.startPrank(alice); + signerList.removeSigners(addrs); - // Approve - eMultisig.approve(pid); - vm.startPrank(bob); eMultisig.approve(pid); - vm.startPrank(carol); + + vm.startPrank(randomWallet); eMultisig.approve(pid); + vm.warp(block.timestamp + EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD); + vm.startPrank(alice); - eMultisig.execute(pid, "ipfs://new-metadata-here", submittedActions); - // Check round - (open, executed, parameters, vetoTally, metadataUri, retrievedActions, allowFailureMap) = - optimisticPlugin.getProposal((uint256(block.timestamp) << 128 | uint256(block.timestamp) << 64) + 1); + _; + } - assertEq(open, false, "Should not be open"); - assertEq(executed, true, "Should be executed"); - assertEq(vetoTally, 0, "Should be 0"); + function test_WhenCallingGetProposalBeingExpired() external givenTheProposalExpired { + // It should return the right values + ( + bool executed, + uint16 approvals, + EmergencyMultisig.ProposalParameters memory parameters, + bytes memory encryptedPayloadURI, + bytes32 publicMetadataUriHash, + bytes32 destinationActionsHash, + OptimisticTokenVotingPlugin destinationPlugin + ) = eMultisig.getProposal(0); - assertEq(parameters.vetoEndDate, 15 days, "Incorrect vetoEndDate"); - assertEq(metadataUri, "ipfs://new-metadata-here", "Incorrect target metadataUri"); + // Assert the proposal is not executed + assertEq(executed, false, "Proposal should not be executed"); - assertEq(retrievedActions.length, 2, "Should be 2"); + // Assert the number of approvals + assertEq(approvals, 2, "Approvals should be 2"); - assertEq(retrievedActions[1].to, address(dao), "Incorrect to"); - assertEq(retrievedActions[1].value, 0, "Incorrect value"); - assertEq(retrievedActions[1].data, abi.encodeWithSelector(DAO.daoURI.selector), "Incorrect data"); - assertEq(retrievedActions[0].to, address(stdMultisig), "Incorrect to"); - assertEq(retrievedActions[0].value, 0, "Incorrect value"); + // Assert the proposal parameters + assertEq(parameters.minApprovals, 3, "Incorrect minApprovals"); + assertEq(parameters.snapshotBlock, block.number - 1 - 50, "Incorrect snapshotBlock"); assertEq( - retrievedActions[0].data, abi.encodeWithSelector(Addresslist.addresslistLength.selector), "Incorrect data" + parameters.expirationDate, + block.timestamp, // we just moved to it + "Incorrect expirationDate" ); - assertEq(allowFailureMap, 0, "Should be 0"); - } - - // Upgrade eMultisig + // Assert the encrypted payload URI + assertEq(encryptedPayloadURI, "ipfs://encrypted", "Incorrect encryptedPayloadURI"); - function test_UpgradeToRevertsWhenCalledFromNonUpgrader() public { - address initialImplementation = eMultisig.implementation(); - address _newImplementation = address(new EmergencyMultisig()); + // Assert the public metadata URI hash + assertEq(publicMetadataUriHash, keccak256("ipfs://the-metadata"), "Incorrect publicMetadataUriHash"); - vm.expectRevert( - abi.encodeWithSelector( - DaoUnauthorized.selector, - address(dao), - address(eMultisig), - alice, - eMultisig.UPGRADE_PLUGIN_PERMISSION_ID() - ) + // Assert the destination actions hash + assertEq( + destinationActionsHash, + hex"3626b3f254463d63d9bd5ff77ff99d2691b20f0db6347f685befae593d8f4e6f", + "Incorrect destinationActionsHash" ); - eMultisig.upgradeTo(_newImplementation); - - assertEq(eMultisig.implementation(), initialImplementation); + // Assert the destination plugin + assertEq(address(destinationPlugin), address(optimisticPlugin), "Incorrect destinationPlugin"); } - function test_UpgradeToAndCallRevertsWhenCalledFromNonUpgrader() public { - address initialImplementation = eMultisig.implementation(); - dao.grant(address(eMultisig), alice, eMultisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); - address _newImplementation = address(new EmergencyMultisig()); - - EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ - onlyListed: true, - minApprovals: 3, - addresslistSource: stdMultisig, - proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - - vm.expectRevert( - abi.encodeWithSelector( - DaoUnauthorized.selector, - address(dao), - address(eMultisig), - alice, - eMultisig.UPGRADE_PLUGIN_PERMISSION_ID() - ) - ); - eMultisig.upgradeToAndCall( - _newImplementation, abi.encodeCall(EmergencyMultisig.updateMultisigSettings, (settings)) - ); + function test_WhenCallingCanApproveOrApproveBeingExpired() external givenTheProposalExpired { + // It canApprove should return false (when listed on creation, self appointed now) + // It canApprove should return false (when listed on creation, appointing someone else now) + // It canApprove should return false (when currently appointed by a signer listed on creation) + // It canApprove should return false (when unlisted on creation, unappointed now) + assertEq(eMultisig.canApprove(0, alice), false, "Alice should not be able to approve"); + assertEq(eMultisig.canApprove(0, bob), false, "Bob should not be able to approve"); + assertEq(eMultisig.canApprove(0, randomWallet), false, "Random wallet should not be able to approve"); + assertEq(eMultisig.canApprove(0, address(0x5555)), false, "Random wallet should not be able to approve"); - assertEq(eMultisig.implementation(), initialImplementation); - } + // It approve should revert (when listed on creation, self appointed now) + // It approve should revert (when listed on creation, appointing someone else now) + // It approve should revert (when currently appointed by a signer listed on creation) + // It approve should revert (when unlisted on creation, unappointed now) + vm.startPrank(alice); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, 0, alice)); + eMultisig.approve(0); - function test_UpgradeToSucceedsWhenCalledFromUpgrader() public { - dao.grant(address(eMultisig), alice, eMultisig.UPGRADE_PLUGIN_PERMISSION_ID()); + vm.startPrank(bob); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, 0, bob)); + eMultisig.approve(0); - address _newImplementation = address(new EmergencyMultisig()); + vm.startPrank(randomWallet); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, 0, randomWallet)); + eMultisig.approve(0); - vm.expectEmit(); - emit Upgraded(_newImplementation); + vm.startPrank(address(0x5555)); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ApprovalCastForbidden.selector, 0, address(0x5555))); + eMultisig.approve(0); + } - eMultisig.upgradeTo(_newImplementation); + function test_WhenCallingHasApprovedBeingExpired() external givenTheProposalExpired { + // It hasApproved should return false until approved - assertEq(eMultisig.implementation(), address(_newImplementation)); + assertEq(eMultisig.hasApproved(0, alice), true, "Alice should have approved"); + assertEq(eMultisig.hasApproved(0, bob), true, "Bob should have approved"); + assertEq(eMultisig.hasApproved(0, randomWallet), true, "Random wallet should have approved"); + assertEq(eMultisig.hasApproved(0, address(0x5555)), false, "5555 should not have approved"); } - function test_UpgradeToAndCallSucceedsWhenCalledFromUpgrader() public { - dao.grant(address(eMultisig), alice, eMultisig.UPGRADE_PLUGIN_PERMISSION_ID()); - dao.grant(address(eMultisig), alice, eMultisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); + function test_WhenCallingCanExecuteOrExecuteBeingExpired() external givenTheProposalExpired { + // It canExecute should return false (when listed on creation, self appointed now) + // It canExecute should return false (when listed on creation, appointing someone else now) + // It canExecute should return false (when currently appointed by a signer listed on creation) + // It canExecute should return false (when unlisted on creation, unappointed now) - address _newImplementation = address(new EmergencyMultisig()); + // vm.startPrank(alice); + assertEq(eMultisig.canExecute(0), false, "Proposal should not be executable"); + vm.startPrank(bob); + assertEq(eMultisig.canExecute(0), false, "Proposal should not be executable"); + vm.startPrank(randomWallet); + assertEq(eMultisig.canExecute(0), false, "Proposal should not be executable"); + vm.startPrank(address(0x5555)); + assertEq(eMultisig.canExecute(0), false, "Proposal should not be executable"); + + // It execute should revert (when listed on creation, self appointed now) + // It execute should revert (when listed on creation, appointing someone else now) + // It execute should revert (when currently appointed by a signer listed on creation) + // It execute should revert (when unlisted on creation, unappointed now) + IDAO.Action[] memory actions = new IDAO.Action[](1); + actions[0].value = 1.5 ether; + actions[0].to = address(carol); + actions[0].data = hex""; - vm.expectEmit(); - emit Upgraded(_newImplementation); + vm.startPrank(alice); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, 0)); + eMultisig.execute(0, "ipfs://the-metadata", actions); - EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ - onlyListed: true, - minApprovals: 3, - addresslistSource: stdMultisig, - proposalExpirationPeriod: EMERGENCY_MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - eMultisig.upgradeToAndCall( - _newImplementation, abi.encodeCall(EmergencyMultisig.updateMultisigSettings, (settings)) - ); + vm.startPrank(bob); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, 0)); + eMultisig.execute(0, "ipfs://the-metadata", actions); + + vm.startPrank(randomWallet); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, 0)); + eMultisig.execute(0, "ipfs://the-metadata", actions); - assertEq(eMultisig.implementation(), address(_newImplementation)); + vm.startPrank(address(0x5555)); + vm.expectRevert(abi.encodeWithSelector(EmergencyMultisig.ProposalExecutionForbidden.selector, 0)); + eMultisig.execute(0, "ipfs://the-metadata", actions); } } diff --git a/test/EmergencyMultisig.t.yaml b/test/EmergencyMultisig.t.yaml new file mode 100644 index 0000000..0209798 --- /dev/null +++ b/test/EmergencyMultisig.t.yaml @@ -0,0 +1,325 @@ +EmergencyMultisigTest: + # Plugin lifecycle + - given: a newly deployed contract + then: + - given: calling initialize + then: + - it: should initialize the first time + - it: should refuse to initialize again + - it: should set the DAO address + + # updateSettings below should have the same branches: + - it: should set the minApprovals + - it: should set onlyListed + - it: should set signerList + - it: should set proposalExpirationPeriod + - it: should emit MultisigSettingsUpdated + - when: minApprovals is greater than signerList length [on initialize] + then: + - it: should revert + - it: should revert (with onlyListed false) + - it: should not revert otherwise + - when: minApprovals is zero [on initialize] + then: + - it: should revert + - it: should revert (with onlyListed false) + - it: should not revert otherwise + - when: signerList is invalid [on initialize] + then: + - it: should revert + + - when: calling upgradeTo + then: + - it: should revert when called without the permission + - it: should work when called with the permission + + - when: calling upgradeToAndCall + then: + - it: should revert when called without the permission + - it: should work when called with the permission + + # General methods + + - when: calling supportsInterface + then: + - it: does not support the empty interface + - it: supports IERC165Upgradeable + - it: supports IPlugin + - it: supports IProposal + - it: supports IEmergencyMultisig + + - when: calling updateSettings + then: + - given: caller has no permission + then: + - it: should revert + - it: otherwise it should just work + + # initialize above should have the same branches: + - it: should set the minApprovals + - it: should set onlyListed + - it: should set signerList + - it: should set proposalExpirationPeriod + - it: should emit MultisigSettingsUpdated + - when: minApprovals is greater than signerList length [on updateSettings] + then: + - it: should revert + - it: should revert (with onlyListed false) + - it: should not revert otherwise + - when: minApprovals is zero [on updateSettings] + then: + - it: should revert + - it: should revert (with onlyListed false) + - it: should not revert otherwise + - when: signerList is invalid [on updateSettings] + then: + - it: should revert + + - when: calling createProposal + then: + - it: increments the proposal counter + - it: creates and return unique proposal IDs + - it: emits the ProposalCreated event + - it: creates a proposal with the given values + - given: settings changed on the same block + then: + - it: reverts + - it: does not revert otherwise + - given: onlyListed is false + then: + - it: allows anyone to create + - given: onlyListed is true + and: + - given: creation caller is not listed or appointed + then: + - it: reverts + - it: reverts if listed before but not now + - given: creation caller is appointed by a former signer + then: + - it: reverts + - given: creation caller is listed and self appointed + then: + - it: creates the proposal + - given: creation caller is listed, appointing someone else now + then: + - it: creates the proposal + - given: creation caller is appointed by a current signer + then: + - it: creates the proposal + - given: approveProposal is true + then: + - it: creates and calls approval in one go + - given: approveProposal is false + then: + - it: only creates the proposal + + - when: calling hashActions + then: + - it: returns the right result + - it: reacts to any of the values changing + - it: same input produces the same output + + # Proposal lifecycle + + - given: The proposal is not created + then: + # Get proposal + - when: calling getProposal [being uncreated] + then: + - it: should return empty values + # Approval + - when: calling canApprove or approve [being uncreated] + then: + - it: canApprove should return false (when listed and self appointed) + - it: approve should revert (when listed and self appointed) + - it: canApprove should return false (when listed, appointing someone else now) + - it: approve should revert (when listed, appointing someone else now) + - it: canApprove should return false (when appointed by a listed signer) + - it: approve should revert (when appointed by a listed signer) + - it: canApprove should return false (when unlisted and unappointed) + - it: approve should revert (when unlisted and unappointed) + # Has approved + - when: calling hasApproved [being uncreated] + then: + - it: hasApproved should always return false + # Execution + - when: calling canExecute or execute [being uncreated] + then: + - it: canExecute should always return false + + - given: The proposal is open + then: + # Get proposal + - when: calling getProposal [being open] + then: + - it: should return the right values + # Approval + - when: calling canApprove or approve [being open] + then: + - it: canApprove should return true (when listed on creation, self appointed now) + - it: approve should work (when listed on creation, self appointed now) + - it: approve should emit an event (when listed on creation, self appointed now) + - it: canApprove should return false (when listed on creation, appointing someone else now) + - it: approve should revert (when listed on creation, appointing someone else now) + - it: canApprove should return true (when currently appointed by a signer listed on creation) + - it: approve should work (when currently appointed by a signer listed on creation) + - it: approve should emit an event (when currently appointed by a signer listed on creation) + - it: canApprove should return false (when unlisted on creation, unappointed now) + - it: approve should revert (when unlisted on creation, unappointed now) + + # Has approved + - when: calling hasApproved [being open] + then: + - it: hasApproved should return false until approved + # Execution + - when: calling canExecute or execute [being open] + then: + - it: canExecute should return false (when listed on creation, self appointed now) + - it: execute should revert (when listed on creation, self appointed now) + - it: canExecute should return false (when listed on creation, appointing someone else now) + - it: execute should revert (when listed on creation, appointing someone else now) + - it: canExecute should return false (when currently appointed by a signer listed on creation) + - it: execute should revert (when currently appointed by a signer listed on creation) + - it: canExecute should return false (when unlisted on creation, unappointed now) + - it: execute should revert (when unlisted on creation, unappointed now) + + - given: The proposal was approved by the address + then: + # Get proposal + - when: calling getProposal [being approved] + then: + - it: should return the right values + # Approval + - when: calling canApprove or approve [being approved] + then: + - it: canApprove should return false (when listed on creation, self appointed now) + - it: approve should revert (when listed on creation, self appointed now) + # - it: canApprove should return false (when listed on creation, appointing someone else now) + # - it: approve should revert (when listed on creation, appointing someone else now) + - it: canApprove should return false (when currently appointed by a signer listed on creation) + - it: approve should revert (when currently appointed by a signer listed on creation) + # - it: canApprove should return false (when unlisted on creation, unappointed now) + # - it: approve should revert (when unlisted on creation, unappointed now) + # Has approved + - when: calling hasApproved [being approved] + then: + - it: hasApproved should return false until approved + # Execution + - when: calling canExecute or execute [being approved] + then: + - it: canExecute should return false (when listed on creation, self appointed now) + - it: execute should revert (when listed on creation, self appointed now) + # - it: canExecute should return false (when listed on creation, appointing someone else now) + # - it: execute should revert (when listed on creation, appointing someone else now) + - it: canExecute should return false (when currently appointed by a signer listed on creation) + - it: execute should revert (when currently appointed by a signer listed on creation) + # - it: canExecute should return false (when unlisted on creation, unappointed now) + # - it: execute should revert (when unlisted on creation, unappointed now) + + - given: The proposal passed + then: + # Get proposal + - when: calling getProposal [being passed] + then: + - it: should return the right values + # Approval + - when: calling canApprove or approve [being passed] + then: + - it: canApprove should return false (when listed on creation, self appointed now) + - it: approve should revert (when listed on creation, self appointed now) + - it: canApprove should return false (when listed on creation, appointing someone else now) + - it: approve should revert (when listed on creation, appointing someone else now) + - it: canApprove should return false (when currently appointed by a signer listed on creation) + - it: approve should revert (when currently appointed by a signer listed on creation) + - it: canApprove should return false (when unlisted on creation, unappointed now) + - it: approve should revert (when unlisted on creation, unappointed now) + # Has approved + - when: calling hasApproved [being passed] + then: + - it: hasApproved should return false until approved + # Execution integrity + - when: calling canExecute or execute with modified data [being passed] + then: + - it: execute should revert with modified metadata + - it: execute should revert with modified actions + - it: execute should work with matching data + # Execution + - when: calling canExecute or execute [being passed] + then: + - it: canExecute should return true, always + - it: execute should work, when called by anyone with the actions + - it: execute should emit an event, when called by anyone with the actions + # Proposal forwarding on execution + - it: execute recreates the proposal on the destination plugin + - it: The parameters of the recreated proposal match the hash of the executed one + - it: A ProposalCreated event is emitted on the destination plugin + - it: Execution is immediate on the destination plugin + - given: TaikoL1 is incompatible + then: + - it: executes successfully, regardless + + - given: The proposal is already executed + then: + # Get proposal + - when: calling getProposal [being executed] + then: + - it: should return the right values + # Approval + - when: calling canApprove or approve [being executed] + then: + - it: canApprove should return false (when listed on creation, self appointed now) + - it: approve should revert (when listed on creation, self appointed now) + - it: canApprove should return false (when listed on creation, appointing someone else now) + - it: approve should revert (when listed on creation, appointing someone else now) + - it: canApprove should return false (when currently appointed by a signer listed on creation) + - it: approve should revert (when currently appointed by a signer listed on creation) + - it: canApprove should return false (when unlisted on creation, unappointed now) + - it: approve should revert (when unlisted on creation, unappointed now) + # Has approved + - when: calling hasApproved [being executed] + then: + - it: hasApproved should return false until approved + # Execution + - when: calling canExecute or execute [being executed] + then: + - it: canExecute should return false (when listed on creation, self appointed now) + - it: execute should revert (when listed on creation, self appointed now) + - it: canExecute should return false (when listed on creation, appointing someone else now) + - it: execute should revert (when listed on creation, appointing someone else now) + - it: canExecute should return false (when currently appointed by a signer listed on creation) + - it: execute should revert (when currently appointed by a signer listed on creation) + - it: canExecute should return false (when unlisted on creation, unappointed now) + - it: execute should revert (when unlisted on creation, unappointed now) + + - given: The proposal expired + then: + # Get proposal + - when: calling getProposal [being expired] + then: + - it: should return the right values + # Approval + - when: calling canApprove or approve [being expired] + then: + - it: canApprove should return false (when listed on creation, self appointed now) + - it: approve should revert (when listed on creation, self appointed now) + - it: canApprove should return false (when listed on creation, appointing someone else now) + - it: approve should revert (when listed on creation, appointing someone else now) + - it: canApprove should return false (when currently appointed by a signer listed on creation) + - it: approve should revert (when currently appointed by a signer listed on creation) + - it: canApprove should return false (when unlisted on creation, unappointed now) + - it: approve should revert (when unlisted on creation, unappointed now) + # Has approved + - when: calling hasApproved [being expired] + then: + - it: hasApproved should return false until approved + # Execution + - when: calling canExecute or execute [being expired] + then: + - it: canExecute should return false (when listed on creation, self appointed now) + - it: execute should revert (when listed on creation, self appointed now) + - it: canExecute should return false (when listed on creation, appointing someone else now) + - it: execute should revert (when listed on creation, appointing someone else now) + - it: canExecute should return false (when currently appointed by a signer listed on creation) + - it: execute should revert (when currently appointed by a signer listed on creation) + - it: canExecute should return false (when unlisted on creation, unappointed now) + - it: execute should revert (when unlisted on creation, unappointed now) diff --git a/test/EmergencyMultisigPluginSetup.t.sol b/test/EmergencyMultisigPluginSetup.t.sol index ec5715a..94b8122 100644 --- a/test/EmergencyMultisigPluginSetup.t.sol +++ b/test/EmergencyMultisigPluginSetup.t.sol @@ -1,68 +1,44 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.17; -import {Test} from "forge-std/Test.sol"; +import {AragonTest} from "./base/AragonTest.sol"; +import {DaoBuilder} from "./helpers/DaoBuilder.sol"; import {EmergencyMultisig} from "../src/EmergencyMultisig.sol"; -import {Multisig} from "../src/Multisig.sol"; import {EmergencyMultisigPluginSetup} from "../src/setup/EmergencyMultisigPluginSetup.sol"; -import {GovernanceERC20} from "@aragon/osx/token/ERC20/governance/GovernanceERC20.sol"; -import {GovernanceWrappedERC20} from "@aragon/osx/token/ERC20/governance/GovernanceWrappedERC20.sol"; -import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import { + SignerList, + UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID, + UPDATE_SIGNER_LIST_PERMISSION_ID +} from "../src/SignerList.sol"; import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; -import {RATIO_BASE} from "@aragon/osx/plugins/utils/Ratio.sol"; import {DAO} from "@aragon/osx/core/dao/DAO.sol"; import {IPluginSetup} from "@aragon/osx/framework/plugin/setup/PluginSetup.sol"; import {PermissionLib} from "@aragon/osx/core/permission/PermissionLib.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {ERC20Mock} from "./mocks/ERC20Mock.sol"; -import {ITaikoL1} from "../src/adapted-dependencies/ITaikoL1.sol"; -contract EmergencyMultisigPluginSetupTest is Test { +contract EmergencyMultisigPluginSetupTest is AragonTest { EmergencyMultisigPluginSetup public pluginSetup; - GovernanceERC20 governanceERC20Base; - GovernanceWrappedERC20 governanceWrappedERC20Base; address immutable daoBase = address(new DAO()); - address immutable stdMultisigBase = address(new Multisig()); + address immutable signerListBase = address(new SignerList()); DAO dao; // Recycled installation parameters EmergencyMultisig.MultisigSettings eMultisigSettings; - address[] stdMembers; - Multisig stdMultisig; - - address alice = address(0xa11ce); - address bob = address(0xb0b); - address carol = address(0xc4601); - address dave = address(0xd473); - - error Unimplemented(); + address[] signers; + SignerList signerList; function setUp() public { - pluginSetup = new EmergencyMultisigPluginSetup(); + DaoBuilder builder = new DaoBuilder(); + (dao,,,,, signerList,,) = builder.withMultisigMember(alice).withMultisigMember(bob).withMultisigMember(carol) + .withMultisigMember(david).build(); - // Address list source (std multisig) - stdMembers = new address[](4); - stdMembers[0] = alice; - stdMembers[1] = bob; - stdMembers[2] = carol; - stdMembers[3] = dave; - Multisig.MultisigSettings memory stdSettings = Multisig.MultisigSettings({ - onlyListed: true, - minApprovals: 3, - destinationProposalDuration: 10 days, - proposalExpirationPeriod: 15 days - }); - stdMultisig = Multisig( - createProxyAndCall( - stdMultisigBase, abi.encodeCall(Multisig.initialize, (IDAO(dao), stdMembers, stdSettings)) - ) - ); + pluginSetup = new EmergencyMultisigPluginSetup(); // Default params eMultisigSettings = EmergencyMultisig.MultisigSettings({ onlyListed: true, minApprovals: 3, - addresslistSource: stdMultisig, + signerList: signerList, proposalExpirationPeriod: 15 days }); } @@ -72,37 +48,28 @@ contract EmergencyMultisigPluginSetupTest is Test { bytes memory output = pluginSetup.encodeInstallationParameters(eMultisigSettings); bytes memory expected = - hex"000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000030000000000000000000000005991a2df15a8f6a256d3ec51e99254cd3fb576a9000000000000000000000000000000000000000000000000000000000013c680"; + hex"00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003000000000000000000000000a0279152cf631d6c493901f9b576d88e2847bfa1000000000000000000000000000000000000000000000000000000000013c680"; assertEq(output, expected, "Incorrect encoded bytes"); } function test_ShouldEncodeInstallationParameters_2() public { // 2 - stdMembers = new address[](2); - stdMembers[0] = alice; - stdMembers[1] = bob; - Multisig.MultisigSettings memory stdSettings = Multisig.MultisigSettings({ - onlyListed: true, - minApprovals: 1, - destinationProposalDuration: 10 days, - proposalExpirationPeriod: 17 days - }); - stdMultisig = Multisig( - createProxyAndCall( - stdMultisigBase, abi.encodeCall(Multisig.initialize, (IDAO(dao), stdMembers, stdSettings)) - ) - ); + signers = new address[](2); + signers[0] = alice; + signers[1] = bob; + signerList = + SignerList(createProxyAndCall(signerListBase, abi.encodeCall(SignerList.initialize, (IDAO(dao), signers)))); eMultisigSettings = EmergencyMultisig.MultisigSettings({ onlyListed: true, minApprovals: 1, - addresslistSource: stdMultisig, + signerList: signerList, proposalExpirationPeriod: 17 days }); bytes memory output = pluginSetup.encodeInstallationParameters(eMultisigSettings); bytes memory expected = - hex"00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000c7183455a4c133ae270771860664b6b7ec320bb10000000000000000000000000000000000000000000000000000000000166980"; + hex"0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000003a6a84cd762d9707a21605b548aaab891562aab0000000000000000000000000000000000000000000000000000000000166980"; assertEq(output, expected, "Incorrect encoded bytes"); } @@ -116,34 +83,22 @@ contract EmergencyMultisigPluginSetupTest is Test { assertEq(outSettings.onlyListed, true, "Should be true"); assertEq(outSettings.minApprovals, 3, "Should be 3"); - assertEq( - address(outSettings.addresslistSource), - address(eMultisigSettings.addresslistSource), - "Incorrect address list source" - ); + assertEq(address(outSettings.signerList), address(signerList), "Incorrect signer list"); } function test_ShouldDecodeInstallationParameters_2() public { // 2 - stdMembers = new address[](2); - stdMembers[0] = alice; - stdMembers[1] = bob; - Multisig.MultisigSettings memory stdSettings = Multisig.MultisigSettings({ - onlyListed: true, - minApprovals: 1, - destinationProposalDuration: 10 days, - proposalExpirationPeriod: 5 days - }); - stdMultisig = Multisig( - createProxyAndCall( - stdMultisigBase, abi.encodeCall(Multisig.initialize, (IDAO(dao), stdMembers, stdSettings)) - ) - ); + signers = new address[](2); + signers[0] = alice; + signers[1] = bob; + signerList = + SignerList(createProxyAndCall(signerListBase, abi.encodeCall(SignerList.initialize, (IDAO(dao), signers)))); + eMultisigSettings = EmergencyMultisig.MultisigSettings({ onlyListed: false, minApprovals: 1, - addresslistSource: stdMultisig, + signerList: signerList, proposalExpirationPeriod: 5 days }); @@ -155,7 +110,7 @@ contract EmergencyMultisigPluginSetupTest is Test { assertEq(outSettings.onlyListed, false, "Should be false"); assertEq(outSettings.minApprovals, 1, "Should be 1"); - assertEq(address(outSettings.addresslistSource), address(stdMultisig), "Incorrect address list source"); + assertEq(address(outSettings.signerList), address(signerList), "Incorrect signer list"); } function test_PrepareInstallationReturnsTheProperPermissions() public { diff --git a/test/EncryptionRegistry.t.sol b/test/EncryptionRegistry.t.sol new file mode 100644 index 0000000..b501c27 --- /dev/null +++ b/test/EncryptionRegistry.t.sol @@ -0,0 +1,961 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {AragonTest} from "./base/AragonTest.sol"; +import {Addresslist} from "@aragon/osx/plugins/utils/Addresslist.sol"; +import {EncryptionRegistry, IEncryptionRegistry} from "../src/EncryptionRegistry.sol"; +import {SignerList} from "../src/SignerList.sol"; +import {DaoBuilder} from "./helpers/DaoBuilder.sol"; +import {DAO} from "@aragon/osx/core/dao/DAO.sol"; +import {Multisig} from "../src/Multisig.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +contract EncryptionRegistryTest is AragonTest { + EncryptionRegistry registry; + DaoBuilder builder; + DAO dao; + Multisig multisig; + + // Events/errors to be tested here (duplicate) + event PublicKeySet(address member, bytes32 publicKey); + event WalletAppointed(address member, address appointedWallet); + + function setUp() public { + builder = new DaoBuilder(); + (dao,, multisig,,,, registry,) = builder.withMultisigMember(alice).withMultisigMember(bob).withMultisigMember( + carol + ).withMultisigMember(david).build(); + } + + function test_ShouldAppointWallets() public { + address addrValue; + bytes32 bytesValue; + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + // Alice + vm.startPrank(alice); + registry.appointWallet(address(0x1234000000000000000000000000000000000000)); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(bytesValue, 0); + assertEq(addrValue, address(0x1234000000000000000000000000000000000000)); + + // Bob + vm.startPrank(bob); + registry.appointWallet(address(0x0000567800000000000000000000000000000000)); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(bytesValue, 0); + assertEq(addrValue, address(0x1234000000000000000000000000000000000000)); + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(bytesValue, 0); + assertEq(addrValue, address(0x0000567800000000000000000000000000000000)); + + // Carol + vm.startPrank(carol); + registry.appointWallet(address(0x0000000090aB0000000000000000000000000000)); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(bytesValue, 0); + assertEq(addrValue, address(0x1234000000000000000000000000000000000000)); + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(bytesValue, 0); + assertEq(addrValue, address(0x0000567800000000000000000000000000000000)); + (addrValue, bytesValue) = registry.accounts(carol); + assertEq(bytesValue, 0); + assertEq(addrValue, address(0x0000000090aB0000000000000000000000000000)); + + // David + vm.startPrank(david); + registry.appointWallet(address(0x000000000000cdEf000000000000000000000000)); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(bytesValue, 0); + assertEq(addrValue, address(0x1234000000000000000000000000000000000000)); + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(bytesValue, 0); + assertEq(addrValue, address(0x0000567800000000000000000000000000000000)); + (addrValue, bytesValue) = registry.accounts(carol); + assertEq(bytesValue, 0); + assertEq(addrValue, address(0x0000000090aB0000000000000000000000000000)); + (addrValue, bytesValue) = registry.accounts(david); + assertEq(bytesValue, 0); + assertEq(addrValue, address(0x000000000000cdEf000000000000000000000000)); + } + + function test_ShouldRegisterOwnPublicKeys() public { + address addrValue; + bytes32 bytesValue; + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + // Alice + vm.startPrank(alice); + registry.setOwnPublicKey(0x1234000000000000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x1234000000000000000000000000000000000000000000000000000000000000); + + // Bob + vm.startPrank(bob); + registry.setOwnPublicKey(0x0000567800000000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x1234000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000567800000000000000000000000000000000000000000000000000000000); + + // Carol + vm.startPrank(carol); + registry.setOwnPublicKey(0x0000000090ab0000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x1234000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000567800000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(carol); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000000090ab0000000000000000000000000000000000000000000000000000); + + // David + vm.startPrank(david); + registry.setOwnPublicKey(0x000000000000cdef000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x1234000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000567800000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(carol); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000000090ab0000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(david); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x000000000000cdef000000000000000000000000000000000000000000000000); + } + + function testFuzz_ShouldRegisterMemberPublicKeys(address appointedWallet) public { + if (skipAppointedWallet(appointedWallet)) return; + + address addrValue; + bytes32 bytesValue; + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + // Alice + vm.startPrank(alice); + registry.appointWallet(appointedWallet); + vm.startPrank(appointedWallet); + registry.setPublicKey(alice, 0x1234000000000000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, appointedWallet); + assertEq(bytesValue, 0x1234000000000000000000000000000000000000000000000000000000000000); + + // Bob + vm.startPrank(bob); + registry.appointWallet(address(uint160(appointedWallet) + 10)); + vm.startPrank(address(uint160(appointedWallet) + 10)); + registry.setPublicKey(bob, 0x0000567800000000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, appointedWallet); + assertEq(bytesValue, 0x1234000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(addrValue, address(uint160(appointedWallet) + 10)); + assertEq(bytesValue, 0x0000567800000000000000000000000000000000000000000000000000000000); + + // Carol + vm.startPrank(carol); + registry.appointWallet(address(uint160(appointedWallet) + 20)); + vm.startPrank(address(uint160(appointedWallet) + 20)); + registry.setPublicKey(carol, 0x0000000090ab0000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, appointedWallet); + assertEq(bytesValue, 0x1234000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(addrValue, address(uint160(appointedWallet) + 10)); + assertEq(bytesValue, 0x0000567800000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(carol); + assertEq(addrValue, address(uint160(appointedWallet) + 20)); + assertEq(bytesValue, 0x0000000090ab0000000000000000000000000000000000000000000000000000); + + // David + vm.startPrank(david); + registry.appointWallet(address(uint160(appointedWallet) + 30)); + vm.startPrank(address(uint160(appointedWallet) + 30)); + registry.setPublicKey(david, 0x000000000000cdef000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, appointedWallet); + assertEq(bytesValue, 0x1234000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(addrValue, address(uint160(appointedWallet) + 10)); + assertEq(bytesValue, 0x0000567800000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(carol); + assertEq(addrValue, address(uint160(appointedWallet) + 20)); + assertEq(bytesValue, 0x0000000090ab0000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(david); + assertEq(addrValue, address(uint160(appointedWallet) + 30)); + assertEq(bytesValue, 0x000000000000cdef000000000000000000000000000000000000000000000000); + } + + function testFuzz_ShouldClearPublicKeyAfterAppointing(address appointedWallet) public { + if (skipAppointedWallet(appointedWallet)) return; + + address addrValue; + bytes32 bytesValue; + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + // Alice + vm.startPrank(alice); + registry.setOwnPublicKey(0x1234000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(bytesValue, 0x1234000000000000000000000000000000000000000000000000000000000000); + + registry.appointWallet(appointedWallet); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, appointedWallet); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + // Bob + vm.startPrank(bob); + registry.setOwnPublicKey(0x0000567800000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(bytesValue, 0x0000567800000000000000000000000000000000000000000000000000000000); + + registry.appointWallet(address(uint160(appointedWallet) + 10)); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, appointedWallet); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(addrValue, address(uint160(appointedWallet) + 10)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + // Carol + vm.startPrank(carol); + registry.setOwnPublicKey(0x0000000090ab0000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(carol); + assertEq(bytesValue, 0x0000000090ab0000000000000000000000000000000000000000000000000000); + + registry.appointWallet(address(uint160(appointedWallet) + 20)); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, appointedWallet); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(addrValue, address(uint160(appointedWallet) + 10)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(carol); + assertEq(addrValue, address(uint160(appointedWallet) + 20)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + // David + vm.startPrank(david); + registry.setOwnPublicKey(0x000000000000cdef000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(david); + assertEq(bytesValue, 0x000000000000cdef000000000000000000000000000000000000000000000000); + + registry.appointWallet(address(uint160(appointedWallet) + 30)); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, appointedWallet); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(addrValue, address(uint160(appointedWallet) + 10)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(carol); + assertEq(addrValue, address(uint160(appointedWallet) + 20)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(david); + assertEq(addrValue, address(uint160(appointedWallet) + 30)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + } + + function test_ShouldRevertWhenAppointingContracts() public { + address addrValue; + bytes32 bytesValue; + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + vm.startPrank(alice); + + // OK + registry.appointWallet(address(0x1234)); + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0x1234)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + // OK + registry.appointWallet(address(0x1111)); + registry.appointWallet(address(0x2222)); + registry.appointWallet(address(0x3333)); + + // KO + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.CannotAppointContracts.selector)); + registry.appointWallet(address(dao)); + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0x3333)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + // KO + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.CannotAppointContracts.selector)); + registry.appointWallet(address(multisig)); + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0x3333)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + // KO + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.CannotAppointContracts.selector)); + registry.appointWallet(address(registry)); + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0x3333)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + } + + function test_shouldAllowToAppointBackAndForth() public { + address addrValue; + bytes32 bytesValue; + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + vm.startPrank(alice); + + // Neutral + registry.appointWallet(address(0x0)); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0x0)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + // Repeated appointments + registry.appointWallet(address(0x1234)); + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0x1234)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + registry.appointWallet(address(0x1234)); + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0x1234)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + // Bob + registry.appointWallet(address(0x1111)); + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0x1111)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + registry.appointWallet(address(0x1111)); + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0x1111)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + registry.appointWallet(address(0x1111)); + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0x1111)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + // More + registry.appointWallet(address(0x2222)); + registry.appointWallet(address(0x2222)); + registry.appointWallet(address(0x2222)); + + registry.appointWallet(address(0x3333)); + registry.appointWallet(address(0x3333)); + registry.appointWallet(address(0x3333)); + + // OK again + registry.appointWallet(address(0x1234)); + registry.appointWallet(address(0x1111)); + registry.appointWallet(address(0x2222)); + registry.appointWallet(address(0x3333)); + } + + function test_getRegisteredAccountsOnlyReturnsAddressesOnce() public { + (address ad1,) = getWallet("wallet 1"); + (address ad2,) = getWallet("wallet 2"); + (address ad3,) = getWallet("wallet 3"); + + assertEq(registry.getRegisteredAccounts().length, 0); + + vm.startPrank(alice); + + // No appointment + registry.appointWallet(address(0x0)); + assertEq(registry.getRegisteredAccounts().length, 0, "Incorrect count"); + + // Appoint + define pubKey's + registry.appointWallet(ad1); + assertEq(registry.getRegisteredAccounts().length, 1, "Incorrect count"); + + vm.startPrank(ad1); + registry.setPublicKey(alice, hex"cdeef70d62f3a538739fb51629eeca7d7cd4852b26a5b469f16af187fdbc7152"); + assertEq(registry.getRegisteredAccounts().length, 1, "Incorrect count"); + + vm.startPrank(alice); + registry.appointWallet(ad2); + assertEq(registry.getRegisteredAccounts().length, 1, "Incorrect count"); + + vm.startPrank(ad2); + registry.setPublicKey(alice, hex"00eef70d62f3a538739fb51629eeca7d7cd4852b26a5b469f16af187fdbc7152"); + assertEq(registry.getRegisteredAccounts().length, 1, "Incorrect count"); + + vm.startPrank(alice); + registry.appointWallet(ad3); + assertEq(registry.getRegisteredAccounts().length, 1, "Incorrect count"); + + vm.startPrank(ad3); + registry.setPublicKey(alice, hex"0000f70d62f3a538739fb51629eeca7d7cd4852b26a5b469f16af187fdbc7152"); + assertEq(registry.getRegisteredAccounts().length, 1, "Incorrect count"); + + // Appoint self back + vm.startPrank(alice); + registry.appointWallet(address(0)); + assertEq(registry.getRegisteredAccounts().length, 1, "Incorrect count"); + + // Set own public key + registry.setOwnPublicKey(hex"1deef70d62f3a538739fb51629eeca7d7cd4852b26a5b469f16af187fdbc7152"); + assertEq(registry.getRegisteredAccounts().length, 1, "Incorrect count"); + + // Appoint + define pubKey's (2) + registry.appointWallet(ad1); + assertEq(registry.getRegisteredAccounts().length, 1, "Incorrect count"); + + vm.startPrank(ad1); + registry.setPublicKey(alice, hex"cdeef70d62f3a538739fb51629eeca7d7cd4852b26a5b469f16af187fdbc7152"); + assertEq(registry.getRegisteredAccounts().length, 1, "Incorrect count"); + + // Appoint self back + vm.startPrank(alice); + registry.appointWallet(address(0)); + assertEq(registry.getRegisteredAccounts().length, 1, "Incorrect count"); + + // BOB + + vm.startPrank(bob); + + // No appointment + registry.appointWallet(address(0x0)); + assertEq(registry.getRegisteredAccounts().length, 1, "Incorrect count"); + + // Appoint + define pubKey's + registry.appointWallet(ad1); + assertEq(registry.getRegisteredAccounts().length, 2, "Incorrect count"); + + vm.startPrank(ad1); + registry.setPublicKey(bob, hex"cdeef70d00000038739fb51629eeca7d7cd4852b26a5b469f16af187fdbc7152"); + assertEq(registry.getRegisteredAccounts().length, 2, "Incorrect count"); + + vm.startPrank(bob); + registry.appointWallet(ad2); + assertEq(registry.getRegisteredAccounts().length, 2, "Incorrect count"); + + vm.startPrank(ad2); + registry.setPublicKey(bob, hex"00eef70d00000038739fb51629eeca7d7cd4852b26a5b469f16af187fdbc7152"); + assertEq(registry.getRegisteredAccounts().length, 2, "Incorrect count"); + + vm.startPrank(bob); + registry.appointWallet(ad3); + assertEq(registry.getRegisteredAccounts().length, 2, "Incorrect count"); + + vm.startPrank(ad3); + registry.setPublicKey(bob, hex"0000f70d00000038739fb51629eeca7d7cd4852b26a5b469f16af187fdbc7152"); + assertEq(registry.getRegisteredAccounts().length, 2, "Incorrect count"); + + // Appoint self back + vm.startPrank(bob); + registry.appointWallet(address(0)); + assertEq(registry.getRegisteredAccounts().length, 2, "Incorrect count"); + + // Set own public key + registry.setOwnPublicKey(hex"1deef70d00000038739fb51629eeca7d7cd4852b26a5b469f16af187fdbc7152"); + assertEq(registry.getRegisteredAccounts().length, 2, "Incorrect count"); + + // Appoint + define pubKey's (2) + registry.appointWallet(ad1); + assertEq(registry.getRegisteredAccounts().length, 2, "Incorrect count"); + + vm.startPrank(ad1); + registry.setPublicKey(bob, hex"cdeef70d00000038739fb51629eeca7d7cd4852b26a5b469f16af187fdbc7152"); + assertEq(registry.getRegisteredAccounts().length, 2, "Incorrect count"); + } + + function test_shouldRevertIfAppointingAnotherSigner() public { + vm.startPrank(alice); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.AlreadyListed.selector)); + registry.appointWallet(bob); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.AlreadyListed.selector)); + registry.appointWallet(carol); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.AlreadyListed.selector)); + registry.appointWallet(david); + + vm.startPrank(bob); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.AlreadyListed.selector)); + registry.appointWallet(alice); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.AlreadyListed.selector)); + registry.appointWallet(carol); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.AlreadyListed.selector)); + registry.appointWallet(david); + + // ok + registry.appointWallet(address(0x5555)); + } + + function test_shouldRevertWhenAlreadyAppointed() public { + vm.startPrank(alice); + registry.appointWallet(address(0x1234)); + + vm.startPrank(bob); + registry.appointWallet(address(0x2345)); + + // Fail + vm.startPrank(alice); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.AlreadyAppointed.selector)); + registry.appointWallet(address(0x2345)); + + vm.startPrank(bob); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.AlreadyAppointed.selector)); + registry.appointWallet(address(0x1234)); + + // ok + registry.appointWallet(address(0x5555)); + } + + function testFuzz_AppointShouldRevertIfNotListed(address appointedWallet) public { + if (Address.isContract(appointedWallet)) return; + + SignerList signerList; + address addrValue; + bytes32 bytesValue; + + // Only Alice + (,, multisig,,, signerList, registry,) = new DaoBuilder().withMultisigMember(alice).build(); + if (signerList.isListed(appointedWallet)) return; + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + // OK + + // Alice + vm.startPrank(alice); + assertEq(signerList.isListed(alice), true); + registry.setOwnPublicKey(0x5678000000000000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x5678000000000000000000000000000000000000000000000000000000000000); + + // Appoint self + registry.appointWallet(appointedWallet); + vm.startPrank(appointedWallet); + registry.setPublicKey(alice, 0x1234000000000000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, appointedWallet); + assertEq(bytesValue, 0x1234000000000000000000000000000000000000000000000000000000000000); + + // NOT OK + + // Bob + vm.startPrank(bob); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.MustBeListed.selector)); + registry.appointWallet(address(uint160(appointedWallet) + 10)); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.MustBeListed.selector)); + registry.setOwnPublicKey(0x0000567800000000000000000000000000000000000000000000000000000000); + vm.startPrank(address(uint160(appointedWallet) + 10)); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.MustBeListed.selector)); + registry.setPublicKey(bob, 0x1234000000000000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, appointedWallet); + assertEq(bytesValue, 0x1234000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + // Carol + vm.startPrank(carol); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.MustBeListed.selector)); + registry.appointWallet(address(uint160(appointedWallet) + 20)); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.MustBeListed.selector)); + registry.setOwnPublicKey(0x0000567800000000000000000000000000000000000000000000000000000000); + vm.startPrank(address(uint160(appointedWallet) + 20)); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.MustBeListed.selector)); + registry.setPublicKey(carol, 0x1234000000000000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, appointedWallet); + assertEq(bytesValue, 0x1234000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(carol); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + // David + vm.startPrank(david); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.MustBeListed.selector)); + registry.appointWallet(address(uint160(appointedWallet) + 30)); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.MustBeListed.selector)); + registry.setOwnPublicKey(0x0000567800000000000000000000000000000000000000000000000000000000); + vm.startPrank(address(uint160(appointedWallet) + 30)); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.MustBeListed.selector)); + registry.setPublicKey(david, 0x1234000000000000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, appointedWallet); + assertEq(bytesValue, 0x1234000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(carol); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + (addrValue, bytesValue) = registry.accounts(david); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + } + + function testFuzz_ShouldRevertOnSetPublicKeyIfNotAppointed(address appointedWallet) public { + if (skipAppointedWallet(appointedWallet)) return; + + address addrValue; + bytes32 bytesValue; + + // Alice + vm.startPrank(alice); + + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.MustBeAppointed.selector)); + registry.setPublicKey(alice, 0x0000567800000000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + registry.appointWallet(appointedWallet); + + // Appointed + vm.startPrank(appointedWallet); + registry.setPublicKey(alice, 0x0000567800000000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, appointedWallet); + assertEq(bytesValue, 0x0000567800000000000000000000000000000000000000000000000000000000); + + // Bob + vm.startPrank(bob); + + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.MustBeAppointed.selector)); + registry.setPublicKey(bob, 0x0000567800000000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + registry.appointWallet(address(uint160(appointedWallet) + 10)); + + // Appointed + vm.startPrank(address(uint160(appointedWallet) + 10)); + registry.setPublicKey(bob, 0x0000567800000000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(addrValue, address(uint160(appointedWallet) + 10)); + assertEq(bytesValue, 0x0000567800000000000000000000000000000000000000000000000000000000); + } + + function testFuzz_ShouldRevertOnSetOwnPublicKeyIfOwnerIsAppointing(address appointedWallet) public { + if (skipAppointedWallet(appointedWallet)) return; + + address addrValue; + bytes32 bytesValue; + + // Alice + vm.startPrank(alice); + registry.appointWallet(appointedWallet); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.MustResetAppointment.selector)); + registry.setOwnPublicKey(0x0000567800000000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, appointedWallet); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + // Appointed + registry.appointWallet(alice); + registry.setOwnPublicKey(0x0000567800000000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(alice); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x0000567800000000000000000000000000000000000000000000000000000000); + + // Bob + vm.startPrank(bob); + registry.appointWallet(appointedWallet); + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.MustResetAppointment.selector)); + registry.setOwnPublicKey(0x1234000000000000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(addrValue, appointedWallet); + assertEq(bytesValue, 0x0000000000000000000000000000000000000000000000000000000000000000); + + // Appointed + registry.appointWallet(bob); + registry.setOwnPublicKey(0x1234000000000000000000000000000000000000000000000000000000000000); + + (addrValue, bytesValue) = registry.accounts(bob); + assertEq(addrValue, address(0)); + assertEq(bytesValue, 0x1234000000000000000000000000000000000000000000000000000000000000); + } + + function test_ShouldEmitPublicKeyDefinedEvents() public { + // For itself + vm.startPrank(alice); + vm.expectEmit(); + emit PublicKeySet(alice, 0x000000000000cdef000000000000000000000000000000000000000000000000); + registry.setOwnPublicKey(0x000000000000cdef000000000000000000000000000000000000000000000000); + + vm.startPrank(bob); + vm.expectEmit(); + emit PublicKeySet(bob, 0x0000000090ab0000000000000000000000000000000000000000000000000000); + registry.setOwnPublicKey(0x0000000090ab0000000000000000000000000000000000000000000000000000); + + vm.startPrank(carol); + vm.expectEmit(); + emit PublicKeySet(carol, 0x0000567800000000000000000000000000000000000000000000000000000000); + registry.setOwnPublicKey(0x0000567800000000000000000000000000000000000000000000000000000000); + + vm.startPrank(david); + vm.expectEmit(); + emit PublicKeySet(david, 0x1234000000000000000000000000000000000000000000000000000000000000); + registry.setOwnPublicKey(0x1234000000000000000000000000000000000000000000000000000000000000); + + // As the appointee + vm.startPrank(alice); + registry.appointWallet(alice); // Self + vm.expectEmit(); + emit PublicKeySet(alice, 0x0000000000000000cdef00000000000000000000000000000000000000000000); + registry.setOwnPublicKey(0x0000000000000000cdef00000000000000000000000000000000000000000000); + + vm.startPrank(bob); + registry.appointWallet(bob); // Self + vm.expectEmit(); + emit PublicKeySet(bob, 0x00000000000090ab000000000000000000000000000000000000000000000000); + registry.setOwnPublicKey(0x00000000000090ab000000000000000000000000000000000000000000000000); + + vm.startPrank(carol); + registry.appointWallet(carol); // Self + vm.expectEmit(); + emit PublicKeySet(carol, 0x0000000056780000000000000000000000000000000000000000000000000000); + registry.setOwnPublicKey(0x0000000056780000000000000000000000000000000000000000000000000000); + + vm.startPrank(david); + registry.appointWallet(david); // Self + vm.expectEmit(); + emit PublicKeySet(david, 0x0000123400000000000000000000000000000000000000000000000000000000); + registry.setOwnPublicKey(0x0000123400000000000000000000000000000000000000000000000000000000); + } + + function test_RegisteredAddressShouldHaveTheRightLength() public { + assertEq(registry.getRegisteredAccounts().length, 0, "Incorrect length"); + + // Set public key first + + // Alice + vm.startPrank(alice); + registry.setOwnPublicKey(bytes32(uint256(1234))); + assertEq(registry.getRegisteredAccounts().length, 1, "Incorrect length"); + registry.appointWallet(address(0x1234)); + assertEq(registry.getRegisteredAccounts().length, 1, "Incorrect length"); + + // Bob + vm.startPrank(bob); + registry.setOwnPublicKey(bytes32(uint256(2345))); + assertEq(registry.getRegisteredAccounts().length, 2, "Incorrect length"); + registry.appointWallet(address(0x5678)); + assertEq(registry.getRegisteredAccounts().length, 2, "Incorrect length"); + + // Appoint first + + // Carol + vm.startPrank(carol); + registry.appointWallet(address(0x90ab)); + assertEq(registry.getRegisteredAccounts().length, 3, "Incorrect length"); + registry.appointWallet(address(0x6666)); + vm.startPrank(address(0x6666)); + registry.setPublicKey(carol, bytes32(uint256(3456))); + assertEq(registry.getRegisteredAccounts().length, 3, "Incorrect length"); + + // David + vm.startPrank(david); + registry.appointWallet(address(0xcdef)); + assertEq(registry.getRegisteredAccounts().length, 4, "Incorrect length"); + registry.appointWallet(address(0x7777)); + vm.startPrank(address(0x7777)); + registry.setPublicKey(david, bytes32(uint256(4567))); + assertEq(registry.getRegisteredAccounts().length, 4, "Incorrect length"); + } + + function test_ShouldEnumerateRegisteredAddresses() public { + // Set public key first + + // Alice + vm.startPrank(alice); + registry.setOwnPublicKey(bytes32(uint256(1234))); + assertEq(registry.registeredAccounts(0), alice); + registry.appointWallet(address(0x1234)); + assertEq(registry.registeredAccounts(0), alice); + + // Bob + vm.startPrank(bob); + registry.setOwnPublicKey(bytes32(uint256(2345))); + assertEq(registry.registeredAccounts(1), bob); + registry.appointWallet(address(0x5678)); + assertEq(registry.registeredAccounts(1), bob); + + // Appoint first + + // Carol + vm.startPrank(carol); + registry.appointWallet(address(0x90ab)); + assertEq(registry.registeredAccounts(2), carol); + registry.appointWallet(address(0x6666)); + vm.startPrank(address(0x6666)); + registry.setPublicKey(carol, bytes32(uint256(3456))); + assertEq(registry.registeredAccounts(2), carol); + + // David + vm.startPrank(david); + registry.appointWallet(address(0xcdef)); + assertEq(registry.registeredAccounts(3), david); + registry.appointWallet(address(0x7777)); + vm.startPrank(address(0x7777)); + registry.setPublicKey(david, bytes32(uint256(4567))); + assertEq(registry.registeredAccounts(3), david); + + assertEq(registry.getRegisteredAccounts().length, 4, "Incorrect length"); + + assertEq(registry.registeredAccounts(0), alice); + assertEq(registry.registeredAccounts(1), bob); + assertEq(registry.registeredAccounts(2), carol); + assertEq(registry.registeredAccounts(3), david); + } + + function test_ShouldLoadTheRegisteredAddresses() public { + // Set public key first + + // Alice + vm.startPrank(alice); + registry.setOwnPublicKey(bytes32(uint256(1234))); + assertEq(registry.registeredAccounts(0), alice); + registry.appointWallet(address(0x1234)); + assertEq(registry.registeredAccounts(0), alice); + + // Bob + vm.startPrank(bob); + registry.setOwnPublicKey(bytes32(uint256(2345))); + assertEq(registry.registeredAccounts(1), bob); + registry.appointWallet(address(0x5678)); + assertEq(registry.registeredAccounts(1), bob); + + // Appoint first + + // Carol + vm.startPrank(carol); + registry.appointWallet(address(0x90ab)); + assertEq(registry.registeredAccounts(2), carol); + registry.appointWallet(address(0x6666)); + vm.startPrank(address(0x6666)); + registry.setPublicKey(carol, bytes32(uint256(3456))); + assertEq(registry.registeredAccounts(2), carol); + + // David + vm.startPrank(david); + registry.appointWallet(address(0xcdef)); + assertEq(registry.registeredAccounts(3), david); + registry.appointWallet(address(0x7777)); + vm.startPrank(address(0x7777)); + registry.setPublicKey(david, bytes32(uint256(4567))); + assertEq(registry.registeredAccounts(3), david); + + address[] memory addresses = registry.getRegisteredAccounts(); + assertEq(addresses.length, 4); + assertEq(addresses[0], alice); + assertEq(addresses[1], bob); + assertEq(addresses[2], carol); + assertEq(addresses[3], david); + } + + function test_TheConstructorShouldRevertIfInvalidAddressList() public { + // Fail + vm.expectRevert(abi.encodeWithSelector(IEncryptionRegistry.InvalidAddressList.selector)); + new EncryptionRegistry(Addresslist(address(this))); + + // OK + (,, multisig,,,,,) = new DaoBuilder().withMultisigMember(alice).build(); + } + + /// @dev mock function for test_TheConstructorShouldRevertIfInvalidAddressList() + function supportsInterface(bytes4) public pure returns (bool) { + return false; + } + + // Internal helpers + + function skipAppointedWallet(address appointedWallet) internal view returns (bool) { + // Avoid fuzz tests overflowing + if (appointedWallet >= address(uint160(0xFFFfFFfFfFFffFFfFFFffffFfFfFffFFfFFFFF00))) return true; + + if ( + appointedWallet == address(0) || appointedWallet == alice || appointedWallet == bob + || appointedWallet == carol || appointedWallet == david || Address.isContract(appointedWallet) + ) return true; + + appointedWallet = address(uint160(appointedWallet) + 10); + + if ( + appointedWallet == address(0) || appointedWallet == alice || appointedWallet == bob + || appointedWallet == carol || appointedWallet == david || Address.isContract(appointedWallet) + ) return true; + + appointedWallet = address(uint160(appointedWallet) + 10); + + if ( + appointedWallet == address(0) || appointedWallet == alice || appointedWallet == bob + || appointedWallet == carol || appointedWallet == david || Address.isContract(appointedWallet) + ) return true; + + appointedWallet = address(uint160(appointedWallet) + 10); + + if ( + appointedWallet == address(0) || appointedWallet == alice || appointedWallet == bob + || appointedWallet == carol || appointedWallet == david || Address.isContract(appointedWallet) + ) return true; + + return false; + } +} diff --git a/test/Multisig.t.sol b/test/Multisig.t.sol index bc66372..04f2a38 100644 --- a/test/Multisig.t.sol +++ b/test/Multisig.t.sol @@ -1,44 +1,51 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.17; +pragma solidity 0.8.17; import {AragonTest} from "./base/AragonTest.sol"; -import {DaoBuilder} from "./helpers/DaoBuilder.sol"; -import {StandardProposalCondition} from "../src/conditions/StandardProposalCondition.sol"; -import {OptimisticTokenVotingPlugin} from "../src/OptimisticTokenVotingPlugin.sol"; +import {Addresslist} from "@aragon/osx/plugins/utils/Addresslist.sol"; import {Multisig} from "../src/Multisig.sol"; -import {IMultisig} from "../src/interfaces/IMultisig.sol"; +import {OptimisticTokenVotingPlugin} from "../src/OptimisticTokenVotingPlugin.sol"; +import { + SignerList, + UPDATE_SIGNER_LIST_PERMISSION_ID, + UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID +} from "../src/SignerList.sol"; +import {EncryptionRegistry} from "../src/EncryptionRegistry.sol"; +import {DaoBuilder} from "./helpers/DaoBuilder.sol"; import {DAO} from "@aragon/osx/core/dao/DAO.sol"; import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; -import {PermissionManager} from "@aragon/osx/core/permission/PermissionManager.sol"; +import {createProxyAndCall} from "../src/helpers/proxy.sol"; +import {IERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol"; import {IProposal} from "@aragon/osx/core/plugin/proposal/IProposal.sol"; import {IPlugin} from "@aragon/osx/core/plugin/IPlugin.sol"; -import {IMembership} from "@aragon/osx/core/plugin/membership/IMembership.sol"; -import {Addresslist} from "@aragon/osx/plugins/utils/Addresslist.sol"; -import {DaoUnauthorized} from "@aragon/osx/core/utils/auth.sol"; -import {IERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol"; -import {createProxyAndCall} from "../src/helpers/proxy.sol"; +import {IMultisig} from "../src/interfaces/IMultisig.sol"; uint64 constant MULTISIG_PROPOSAL_EXPIRATION_PERIOD = 10 days; +uint32 constant DESTINATION_PROPOSAL_DURATION = 9 days; contract MultisigTest is AragonTest { DaoBuilder builder; - DAO dao; Multisig multisig; OptimisticTokenVotingPlugin optimisticPlugin; + SignerList signerList; + EncryptionRegistry encryptionRegistry; + + address immutable SIGNER_LIST_BASE = address(new SignerList()); // Events/errors to be tested here (duplicate) + error DaoUnauthorized(address dao, address where, address who, bytes32 permissionId); + error InvalidAddresslistUpdate(address member); + error InvalidActions(uint256 proposalId); + event MultisigSettingsUpdated( bool onlyListed, uint16 indexed minApprovals, uint64 destinationProposalDuration, + SignerList signerList, uint64 proposalExpirationPeriod ); - event MembersAdded(address[] members); - event MembersRemoved(address[] members); - - error InvalidAddresslistUpdate(address member); - + // Multisig and OptimisticTokenVotingPlugin's event event ProposalCreated( uint256 indexed proposalId, address indexed creator, @@ -54,431 +61,478 @@ contract MultisigTest is AragonTest { function setUp() public { vm.startPrank(alice); - vm.warp(1 days); - vm.roll(100); builder = new DaoBuilder(); - (dao, optimisticPlugin, multisig,,,) = builder.withMultisigMember(alice).withMultisigMember(bob) - .withMultisigMember(carol).withMultisigMember(david).withMinApprovals(3).build(); + (dao, optimisticPlugin, multisig,,, signerList, encryptionRegistry,) = builder.withMultisigMember(alice) + .withMultisigMember(bob).withMultisigMember(carol).withMultisigMember(david).withMinApprovals(3).withDuration( + DESTINATION_PROPOSAL_DURATION + ).build(); } - function test_RevertsIfTryingToReinitialize() public { - // Deploy a new multisig instance - Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ - onlyListed: true, - minApprovals: 3, - destinationProposalDuration: 4 days, - proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - address[] memory signers = new address[](4); - signers[0] = alice; - signers[1] = bob; - signers[2] = carol; - signers[3] = david; - - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); + modifier givenANewlyDeployedContract() { + _; + } - // Reinitialize should fail - vm.expectRevert("Initializable: contract is already initialized"); - multisig.initialize(dao, signers, settings); + modifier givenCallingInitialize() { + _; } - function test_InitializeAddsInitialAddresses() public { - // Deploy with 4 signers + function test_GivenCallingInitialize() external givenANewlyDeployedContract givenCallingInitialize { Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ onlyListed: true, minApprovals: 3, destinationProposalDuration: 4 days, + signerList: signerList, proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - address[] memory signers = new address[](4); - signers[0] = alice; - signers[1] = bob; - signers[2] = carol; - signers[3] = david; - - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); - assertEq(multisig.isListed(alice), true, "Should be a member"); - assertEq(multisig.isListed(bob), true, "Should be a member"); - assertEq(multisig.isListed(carol), true, "Should be a member"); - assertEq(multisig.isListed(david), true, "Should be a member"); - assertEq(multisig.isListed(randomWallet), false, "Should not be a member"); + // It should initialize the first time + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); - // Redeploy with just 2 signers - builder = new DaoBuilder(); - (dao, optimisticPlugin, multisig,,,) = builder.withMultisigMember(alice).withMultisigMember(bob).build(); + // It should refuse to initialize again + vm.expectRevert("Initializable: contract is already initialized"); + multisig.initialize(dao, settings); - assertEq(multisig.isListed(alice), true, "Should be a member"); - assertEq(multisig.isListed(bob), true, "Should be a member"); - assertEq(multisig.isListed(carol), false, "Should not be a member"); - assertEq(multisig.isListed(david), false, "Should not be a member"); - assertEq(multisig.isListed(randomWallet), false, "Should not be a member"); + // It should set the DAO address - // Redeploy with 5 signers - builder = new DaoBuilder(); - (dao, optimisticPlugin, multisig,,,) = builder.withMultisigMember(alice).withMultisigMember(bob) - .withMultisigMember(carol).withMultisigMember(david).withMultisigMember(randomWallet).build(); + assertEq((address(multisig.dao())), address(dao), "Incorrect dao"); - assertEq(multisig.isListed(alice), true, "Should be a member"); - assertEq(multisig.isListed(bob), true, "Should be a member"); - assertEq(multisig.isListed(carol), true, "Should be a member"); - assertEq(multisig.isListed(david), true, "Should be a member"); - assertEq(multisig.isListed(randomWallet), true, "Should be a member"); - } + // It should set the minApprovals - function test_InitializeSetsMinApprovals() public { - // 2 - Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ - onlyListed: true, - minApprovals: 2, - destinationProposalDuration: 4 days, - proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - address[] memory signers = new address[](4); - signers[0] = alice; - signers[1] = bob; - signers[2] = carol; - signers[3] = david; - - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); + (, uint16 minApprovals,,,) = multisig.multisigSettings(); + assertEq(minApprovals, uint16(3), "Incorrect minApprovals"); + settings.minApprovals = 1; + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); + (, minApprovals,,,) = multisig.multisigSettings(); + assertEq(minApprovals, uint16(1), "Incorrect minApprovals"); - (, uint16 minApprovals,,) = multisig.multisigSettings(); - assertEq(minApprovals, uint16(2), "Incorrect minApprovals"); + // It should set onlyListed - // Redeploy with 1 - settings.minApprovals = 1; + (bool onlyListed,,,,) = multisig.multisigSettings(); + assertEq(onlyListed, true, "Incorrect onlyListed"); + settings.onlyListed = false; + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); + (onlyListed,,,,) = multisig.multisigSettings(); + assertEq(onlyListed, false, "Incorrect onlyListed"); - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); + // It should set destinationProposalDuration - (, minApprovals,,) = multisig.multisigSettings(); - assertEq(minApprovals, uint16(1), "Incorrect minApprovals"); + (,, uint64 destinationProposalDuration,,) = multisig.multisigSettings(); + assertEq(destinationProposalDuration, 4 days, "Incorrect destinationProposalDuration"); + settings.destinationProposalDuration = 3 days; + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); + (,, destinationProposalDuration,,) = multisig.multisigSettings(); + assertEq(destinationProposalDuration, 3 days, "Incorrect destinationProposalDuration"); + + // It should set signerList + + (,,, Addresslist givenSignerList,) = multisig.multisigSettings(); + assertEq(address(givenSignerList), address(signerList), "Incorrect addresslistSource"); + (,,,,, signerList,,) = builder.build(); + settings.signerList = signerList; + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); + (,,, signerList,) = multisig.multisigSettings(); + assertEq(address(signerList), address(settings.signerList), "Incorrect addresslistSource"); + + // It should set proposalExpirationPeriod + + (,,,, uint64 expirationPeriod) = multisig.multisigSettings(); + assertEq(expirationPeriod, MULTISIG_PROPOSAL_EXPIRATION_PERIOD, "Incorrect expirationPeriod"); + settings.proposalExpirationPeriod = 3 days; + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); + (,,,, expirationPeriod) = multisig.multisigSettings(); + assertEq(expirationPeriod, 3 days, "Incorrect expirationPeriod"); + + // It should emit MultisigSettingsUpdated + + (,,,,, SignerList newSignerList,,) = builder.build(); + + settings = Multisig.MultisigSettings({ + onlyListed: false, + minApprovals: 2, + destinationProposalDuration: 4 days, + signerList: newSignerList, + proposalExpirationPeriod: 15 days + }); + vm.expectEmit(); + emit MultisigSettingsUpdated(false, uint16(2), 4 days, newSignerList, 15 days); + + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); } - function test_InitializeSetsOnlyListed() public { - // Deploy with true + function test_RevertWhen_MinApprovalsIsGreaterThanSignerListLengthOnInitialize() + external + givenANewlyDeployedContract + givenCallingInitialize + { + // It should revert Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ onlyListed: true, - minApprovals: 3, - destinationProposalDuration: 4 days, + minApprovals: 5, + destinationProposalDuration: 10 days, + signerList: signerList, proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - address[] memory signers = new address[](4); - signers[0] = alice; - signers[1] = bob; - signers[2] = carol; - signers[3] = david; - - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); - (bool onlyListed,,,) = multisig.multisigSettings(); - assertEq(onlyListed, true, "Incorrect onlyListed"); + vm.expectRevert(abi.encodeWithSelector(Multisig.MinApprovalsOutOfBounds.selector, 4, 5)); + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); - // Redeploy with false - settings.onlyListed = false; + // It should revert (with onlyListed false) + settings = Multisig.MultisigSettings({ + onlyListed: false, + minApprovals: 5, + destinationProposalDuration: 10 days, + signerList: signerList, + proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD + }); - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); + vm.expectRevert(abi.encodeWithSelector(Multisig.MinApprovalsOutOfBounds.selector, 4, 5)); + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); - (onlyListed,,,) = multisig.multisigSettings(); - assertEq(onlyListed, false, "Incorrect onlyListed"); - } + // It should not revert otherwise - function test_InitializeSetsDestinationProposalDuration() public { - // Deploy with 5 days - Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ + settings = Multisig.MultisigSettings({ onlyListed: true, - minApprovals: 3, - destinationProposalDuration: 5 days, + minApprovals: 4, + destinationProposalDuration: 10 days, + signerList: signerList, proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - address[] memory signers = new address[](4); - signers[0] = alice; - signers[1] = bob; - signers[2] = carol; - signers[3] = david; - - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); - - (,, uint64 minDuration,) = multisig.multisigSettings(); - assertEq(minDuration, 5 days, "Incorrect minDuration"); - - // Redeploy with 3 days - settings.destinationProposalDuration = 3 days; + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); - - (,, minDuration,) = multisig.multisigSettings(); - assertEq(minDuration, 3 days, "Incorrect minDuration"); + settings = Multisig.MultisigSettings({ + onlyListed: false, + minApprovals: 4, + destinationProposalDuration: 10 days, + signerList: signerList, + proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD + }); + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); } - function test_InitializeSetsProposalExpiration() public { - // Deploy with 15 days + function test_RevertWhen_MinApprovalsIsZeroOnInitialize() + external + givenANewlyDeployedContract + givenCallingInitialize + { + // It should revert Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ onlyListed: true, - minApprovals: 3, - destinationProposalDuration: 5 days, - proposalExpirationPeriod: 15 days + minApprovals: 0, + destinationProposalDuration: 10 days, + signerList: signerList, + proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - address[] memory signers = new address[](4); - signers[0] = alice; - signers[1] = bob; - signers[2] = carol; - signers[3] = david; - - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); - (,,, uint64 expirationPeriod) = multisig.multisigSettings(); - assertEq(expirationPeriod, 15 days, "Incorrect expirationPeriod"); + vm.expectRevert(abi.encodeWithSelector(Multisig.MinApprovalsOutOfBounds.selector, 1, 0)); + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); - // Redeploy with 3 days - settings.proposalExpirationPeriod = 3 days; + // It should revert (with onlyListed false) + settings = Multisig.MultisigSettings({ + onlyListed: false, + minApprovals: 0, + destinationProposalDuration: 10 days, + signerList: signerList, + proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD + }); - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); + vm.expectRevert(abi.encodeWithSelector(Multisig.MinApprovalsOutOfBounds.selector, 1, 0)); + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); - (,,, expirationPeriod) = multisig.multisigSettings(); - assertEq(expirationPeriod, 3 days, "Incorrect expirationPeriod"); - } + // It should not revert otherwise - function test_InitializeEmitsMultisigSettingsUpdatedOnInstall1() public { - // Deploy with true/3/2 - Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ + settings = Multisig.MultisigSettings({ onlyListed: true, - minApprovals: 3, - destinationProposalDuration: 4 days, - proposalExpirationPeriod: 6 days + minApprovals: 4, + destinationProposalDuration: 10 days, + signerList: signerList, + proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - address[] memory signers = new address[](4); - signers[0] = alice; - signers[1] = bob; - signers[2] = carol; - signers[3] = david; - - vm.expectEmit(); - emit MultisigSettingsUpdated(true, uint16(3), 4 days, 6 days); + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); + settings = Multisig.MultisigSettings({ + onlyListed: false, + minApprovals: 4, + destinationProposalDuration: 10 days, + signerList: signerList, + proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD + }); + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); } - function test_InitializeEmitsMultisigSettingsUpdatedOnInstall2() public { - // Deploy with false/2/7 + function test_RevertWhen_SignerListIsInvalidOnInitialize() + external + givenANewlyDeployedContract + givenCallingInitialize + { + // It should revert Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ onlyListed: false, - minApprovals: 2, - destinationProposalDuration: 7 days, - proposalExpirationPeriod: 8 days + minApprovals: 1, + destinationProposalDuration: 10 days, + signerList: SignerList(address(dao)), + proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - address[] memory signers = new address[](4); - signers[0] = alice; - signers[1] = bob; - signers[2] = carol; - signers[3] = david; + vm.expectRevert(abi.encodeWithSelector(Multisig.InvalidSignerList.selector, address(dao))); + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); - vm.expectEmit(); - emit MultisigSettingsUpdated(false, uint16(2), 7 days, 8 days); + // ko 2 + settings = Multisig.MultisigSettings({ + onlyListed: false, + minApprovals: 1, + destinationProposalDuration: 10 days, + signerList: SignerList(address(builder)), + proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD + }); + vm.expectRevert(); + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); + + // ok + (,,,,, SignerList newSignerList,,) = builder.build(); + settings = Multisig.MultisigSettings({ + onlyListed: false, + minApprovals: 1, + destinationProposalDuration: 10 days, + signerList: newSignerList, + proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD + }); + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); + } - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) + function test_WhenCallingUpgradeTo() external { + // It should revert when called without the permission + address initialImplementation = multisig.implementation(); + address _newImplementation = address(new Multisig()); + + vm.expectRevert( + abi.encodeWithSelector( + DaoUnauthorized.selector, + address(dao), + address(multisig), + alice, + multisig.UPGRADE_PLUGIN_PERMISSION_ID() + ) ); + multisig.upgradeTo(_newImplementation); + assertEq(multisig.implementation(), initialImplementation); + + // It should work when called with the permission + dao.grant(address(multisig), alice, multisig.UPGRADE_PLUGIN_PERMISSION_ID()); + multisig.upgradeTo(_newImplementation); } - function test_InitializeRevertsIfMembersListIsTooLong() public { + function test_WhenCallingUpgradeToAndCall() external { + // It should revert when called without the permission + address initialImplementation = multisig.implementation(); + dao.grant(address(multisig), alice, multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); + address _newImplementation = address(new Multisig()); + Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ onlyListed: true, minApprovals: 3, - destinationProposalDuration: 4 days, + destinationProposalDuration: 3 days, + signerList: signerList, proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - address[] memory signers = new address[](65537); - - vm.expectRevert(abi.encodeWithSelector(Multisig.AddresslistLengthOutOfBounds.selector, 65535, 65537)); - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) + vm.expectRevert( + abi.encodeWithSelector( + DaoUnauthorized.selector, + address(dao), + address(multisig), + alice, + multisig.UPGRADE_PLUGIN_PERMISSION_ID() + ) ); - } + multisig.upgradeToAndCall(_newImplementation, abi.encodeCall(Multisig.updateMultisigSettings, (settings))); + assertEq(multisig.implementation(), initialImplementation); - // INTERFACES + // It should work when called with the permission + dao.grant(address(multisig), alice, multisig.UPGRADE_PLUGIN_PERMISSION_ID()); + multisig.upgradeToAndCall(_newImplementation, abi.encodeCall(Multisig.updateMultisigSettings, (settings))); + } - function test_DoesntSupportTheEmptyInterface() public view { + function test_WhenCallingSupportsInterface() external view { + // It does not support the empty interface bool supported = multisig.supportsInterface(0); assertEq(supported, false, "Should not support the empty interface"); - } - function test_SupportsIERC165Upgradeable() public view { - bool supported = multisig.supportsInterface(type(IERC165Upgradeable).interfaceId); + // It supports IERC165Upgradeable + supported = multisig.supportsInterface(type(IERC165Upgradeable).interfaceId); assertEq(supported, true, "Should support IERC165Upgradeable"); - } - function test_SupportsIPlugin() public view { - bool supported = multisig.supportsInterface(type(IPlugin).interfaceId); + // It supports IPlugin + supported = multisig.supportsInterface(type(IPlugin).interfaceId); assertEq(supported, true, "Should support IPlugin"); - } - function test_SupportsIProposal() public view { - bool supported = multisig.supportsInterface(type(IProposal).interfaceId); + // It supports IProposal + supported = multisig.supportsInterface(type(IProposal).interfaceId); assertEq(supported, true, "Should support IProposal"); - } - - function test_SupportsIMembership() public view { - bool supported = multisig.supportsInterface(type(IMembership).interfaceId); - assertEq(supported, true, "Should support IMembership"); - } - - function test_SupportsAddresslist() public view { - bool supported = multisig.supportsInterface(type(Addresslist).interfaceId); - assertEq(supported, true, "Should support Addresslist"); - } - function test_SupportsIMultisig() public view { - bool supported = multisig.supportsInterface(type(IMultisig).interfaceId); + // It supports IMultisig + supported = multisig.supportsInterface(type(IMultisig).interfaceId); assertEq(supported, true, "Should support IMultisig"); } - // UPDATE MULTISIG SETTINGS - - function test_ShouldntAllowMinApprovalsHigherThenAddrListLength() public { - Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ - onlyListed: true, - minApprovals: 5, - destinationProposalDuration: 4 days, // Greater than 4 members below - proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - address[] memory signers = new address[](4); - signers[0] = alice; - signers[1] = bob; - signers[2] = carol; - signers[3] = david; - - vm.expectRevert(abi.encodeWithSelector(Multisig.MinApprovalsOutOfBounds.selector, 4, 5)); - - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); - - // Retry with onlyListed false - settings = Multisig.MultisigSettings({ - onlyListed: false, - minApprovals: 6, - destinationProposalDuration: 4 days, // Greater than 4 members below - proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - vm.expectRevert(abi.encodeWithSelector(Multisig.MinApprovalsOutOfBounds.selector, 4, 6)); - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); + modifier whenCallingUpdateSettings() { + _; } - function test_ShouldNotAllowMinApprovalsZero() public { - Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ - onlyListed: true, - minApprovals: 0, - destinationProposalDuration: 4 days, - proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - address[] memory signers = new address[](4); - signers[0] = alice; - signers[1] = bob; - signers[2] = carol; - signers[3] = david; - - vm.expectRevert(abi.encodeWithSelector(Multisig.MinApprovalsOutOfBounds.selector, 1, 0)); - - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); - - // Retry with onlyListed false - settings = Multisig.MultisigSettings({ - onlyListed: false, - minApprovals: 0, - destinationProposalDuration: 4 days, - proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - vm.expectRevert(abi.encodeWithSelector(Multisig.MinApprovalsOutOfBounds.selector, 1, 0)); - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); - } + function test_WhenCallingUpdateSettings() external whenCallingUpdateSettings { + // It should set the minApprovals + // It should set onlyListed + // It should set signerList + // It should set destinationProposalDuration + // It should set proposalExpirationPeriod + // It should emit MultisigSettingsUpdated - function test_EmitsMultisigSettingsUpdated() public { + bool givenOnlyListed; + uint16 givenMinApprovals; + uint64 givenDestinationProposalDuration; + SignerList givenSignerList; + uint64 givenProposalExpirationPeriod; dao.grant(address(multisig), address(alice), multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); // 1 Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ onlyListed: true, minApprovals: 1, - destinationProposalDuration: 4 days, - proposalExpirationPeriod: 2 days + destinationProposalDuration: 1 days, + signerList: signerList, + proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); vm.expectEmit(); - emit MultisigSettingsUpdated(true, 1, 4 days, 2 days); + emit MultisigSettingsUpdated(true, 1, 1 days, signerList, MULTISIG_PROPOSAL_EXPIRATION_PERIOD); multisig.updateMultisigSettings(settings); + ( + givenOnlyListed, + givenMinApprovals, + givenDestinationProposalDuration, + givenSignerList, + givenProposalExpirationPeriod + ) = multisig.multisigSettings(); + assertEq(givenOnlyListed, true, "onlyListed should be true"); + assertEq(givenMinApprovals, 1, "Incorrect givenMinApprovals"); + assertEq(address(givenSignerList), address(signerList), "Incorrect givenSignerList"); + assertEq( + givenProposalExpirationPeriod, + MULTISIG_PROPOSAL_EXPIRATION_PERIOD, + "Incorrect givenProposalExpirationPeriod" + ); + // 2 + (,,,,, SignerList newSignerList,,) = builder.build(); + settings = Multisig.MultisigSettings({ onlyListed: true, minApprovals: 2, - destinationProposalDuration: 5 days, - proposalExpirationPeriod: 9 days + destinationProposalDuration: 2 days, + signerList: newSignerList, + proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD - 1 }); vm.expectEmit(); - emit MultisigSettingsUpdated(true, 2, 5 days, 9 days); + emit MultisigSettingsUpdated(true, 2, 2 days, newSignerList, MULTISIG_PROPOSAL_EXPIRATION_PERIOD - 1); multisig.updateMultisigSettings(settings); + ( + givenOnlyListed, + givenMinApprovals, + givenDestinationProposalDuration, + givenSignerList, + givenProposalExpirationPeriod + ) = multisig.multisigSettings(); + assertEq(givenOnlyListed, true, "onlyListed should be true"); + assertEq(givenMinApprovals, 2, "Incorrect givenMinApprovals"); + assertEq(address(givenSignerList), address(newSignerList), "Incorrect givenSignerList"); + assertEq( + givenProposalExpirationPeriod, + MULTISIG_PROPOSAL_EXPIRATION_PERIOD - 1, + "Incorrect givenProposalExpirationPeriod" + ); + // 3 + (,,,,, newSignerList,,) = builder.build(); + settings = Multisig.MultisigSettings({ onlyListed: false, minApprovals: 3, - destinationProposalDuration: 0, - proposalExpirationPeriod: 7 days + destinationProposalDuration: 3 days, + signerList: newSignerList, + proposalExpirationPeriod: 4 days }); vm.expectEmit(); - emit MultisigSettingsUpdated(false, 3, 0, 7 days); + emit MultisigSettingsUpdated(false, 3, 3 days, newSignerList, 4 days); multisig.updateMultisigSettings(settings); + ( + givenOnlyListed, + givenMinApprovals, + givenDestinationProposalDuration, + givenSignerList, + givenProposalExpirationPeriod + ) = multisig.multisigSettings(); + assertEq(givenOnlyListed, false, "onlyListed should be false"); + assertEq(givenMinApprovals, 3, "Incorrect givenMinApprovals"); + assertEq(address(givenSignerList), address(newSignerList), "Incorrect givenSignerList"); + assertEq(givenProposalExpirationPeriod, 4 days, "Incorrect givenProposalExpirationPeriod"); + // 4 settings = Multisig.MultisigSettings({ onlyListed: false, minApprovals: 4, - destinationProposalDuration: 1 days, - proposalExpirationPeriod: 0 + destinationProposalDuration: 4 days, + signerList: signerList, + proposalExpirationPeriod: 8 days }); vm.expectEmit(); - emit MultisigSettingsUpdated(false, 4, 1 days, 0); + emit MultisigSettingsUpdated(false, 4, 4 days, signerList, 8 days); multisig.updateMultisigSettings(settings); - } - function test_onlyWalletWithPermissionsCanUpdateSettings() public { + ( + givenOnlyListed, + givenMinApprovals, + givenDestinationProposalDuration, + givenSignerList, + givenProposalExpirationPeriod + ) = multisig.multisigSettings(); + assertEq(givenOnlyListed, false, "onlyListed should be true"); + assertEq(givenMinApprovals, 4, "Incorrect givenMinApprovals"); + assertEq(address(givenSignerList), address(signerList), "Incorrect givenSignerList"); + assertEq(givenProposalExpirationPeriod, 8 days, "Incorrect givenProposalExpirationPeriod"); + } + + function test_RevertGiven_CallerHasNoPermission() external whenCallingUpdateSettings { + // It should revert + (,,,,, SignerList newSignerList,,) = builder.build(); + Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ - onlyListed: true, - minApprovals: 1, - destinationProposalDuration: 3 days, - proposalExpirationPeriod: 5 days + onlyListed: false, + minApprovals: 2, + destinationProposalDuration: 17 days, + signerList: newSignerList, + proposalExpirationPeriod: 3 days }); vm.expectRevert( abi.encodeWithSelector( @@ -492,564 +546,197 @@ contract MultisigTest is AragonTest { multisig.updateMultisigSettings(settings); // Nothing changed - (bool onlyListed, uint16 minApprovals, uint64 destinationProposalDuration, uint64 expiration) = - multisig.multisigSettings(); + ( + bool onlyListed, + uint16 minApprovals, + uint64 currentDestinationProposalDuration, + Addresslist currentSource, + uint64 expiration + ) = multisig.multisigSettings(); assertEq(onlyListed, true); assertEq(minApprovals, 3); - assertEq(destinationProposalDuration, 10 days); + assertEq(currentDestinationProposalDuration, 9 days); + assertEq(address(currentSource), address(signerList)); assertEq(expiration, 10 days); + // It otherwise it should just work // Retry with the permission dao.grant(address(multisig), alice, multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); vm.expectEmit(); - emit MultisigSettingsUpdated(true, 1, 3 days, 5 days); + emit MultisigSettingsUpdated(false, 2, 17 days, newSignerList, 3 days); multisig.updateMultisigSettings(settings); } - function test_IsMemberShouldReturnWhenApropriate() public { - Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ - onlyListed: true, - minApprovals: 1, - destinationProposalDuration: 4 days, - proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - address[] memory signers = new address[](1); - signers[0] = alice; - - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); - - assertEq(multisig.isMember(alice), true, "Should be a member"); - assertEq(multisig.isMember(bob), false, "Should not be a member"); - assertEq(multisig.isMember(carol), false, "Should not be a member"); - assertEq(multisig.isMember(david), false, "Should not be a member"); - - // More members - settings = Multisig.MultisigSettings({ - onlyListed: true, - minApprovals: 1, - destinationProposalDuration: 4 days, - proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - signers = new address[](3); - signers[0] = bob; - signers[1] = carol; - signers[2] = david; - - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); - - assertEq(multisig.isMember(alice), false, "Should not be a member"); - assertEq(multisig.isMember(bob), true, "Should be a member"); - assertEq(multisig.isMember(carol), true, "Should be a member"); - assertEq(multisig.isMember(david), true, "Should be a member"); - } + function test_RevertWhen_MinApprovalsIsGreaterThanSignerListLengthOnUpdateSettings() + external + whenCallingUpdateSettings + { + // It should revert + dao.grant(address(multisig), alice, multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); - function test_IsMemberIsListedShouldReturnTheSameValue() public { Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ onlyListed: true, - minApprovals: 1, - destinationProposalDuration: 4 days, + minApprovals: 5, + destinationProposalDuration: 4 days, // More than 4 + signerList: signerList, proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - address[] memory signers = new address[](1); - signers[0] = alice; - - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); - - assertEq(multisig.isListed(alice), multisig.isMember(alice), "isMember isListed should be equal"); - assertEq(multisig.isListed(bob), multisig.isMember(bob), "isMember isListed should be equal"); - assertEq(multisig.isListed(carol), multisig.isMember(carol), "isMember isListed should be equal"); - assertEq(multisig.isListed(david), multisig.isMember(david), "isMember isListed should be equal"); + vm.expectRevert(abi.encodeWithSelector(Multisig.MinApprovalsOutOfBounds.selector, 4, 5)); + multisig.updateMultisigSettings(settings); - // More members + // It should revert (with onlyListed false) settings = Multisig.MultisigSettings({ - onlyListed: true, - minApprovals: 1, - destinationProposalDuration: 4 days, + onlyListed: false, + minApprovals: 5, + destinationProposalDuration: 4 days, // More than 4 + signerList: signerList, proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - signers = new address[](3); - signers[0] = bob; - signers[1] = carol; - signers[2] = david; - - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); - - assertEq(multisig.isListed(alice), multisig.isMember(alice), "isMember isListed should be equal"); - assertEq(multisig.isListed(bob), multisig.isMember(bob), "isMember isListed should be equal"); - assertEq(multisig.isListed(carol), multisig.isMember(carol), "isMember isListed should be equal"); - assertEq(multisig.isListed(david), multisig.isMember(david), "isMember isListed should be equal"); - } + vm.expectRevert(abi.encodeWithSelector(Multisig.MinApprovalsOutOfBounds.selector, 4, 5)); + multisig.updateMultisigSettings(settings); - function testFuzz_IsMemberIsFalseByDefault(uint256 _randomEntropy) public { - Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ - onlyListed: true, - minApprovals: 1, - destinationProposalDuration: 4 days, - proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - address[] memory signers = new address[](1); // 0x0... would be a member but the chance is negligible + // It should not revert otherwise - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); + // More signers + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + address[] memory signers = new address[](1); + signers[0] = randomWallet; + signerList.addSigners(signers); - assertEq(multisig.isListed(randomWallet), false, "Should be false"); - assertEq( - multisig.isListed(vm.addr(uint256(keccak256(abi.encodePacked(_randomEntropy))))), false, "Should be false" - ); + multisig.updateMultisigSettings(settings); } - function test_AddsNewMembersAndEmits() public { + function test_RevertWhen_MinApprovalsIsZeroOnUpdateSettings() external whenCallingUpdateSettings { + // It should revert dao.grant(address(multisig), alice, multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); - // No - assertEq(multisig.isMember(randomWallet), false, "Should not be a member"); - - address[] memory addrs = new address[](1); - addrs[0] = randomWallet; - - vm.expectEmit(); - emit MembersAdded({members: addrs}); - multisig.addAddresses(addrs); - - // Yes - assertEq(multisig.isMember(randomWallet), true, "Should be a member"); - - // Next - addrs = new address[](3); - addrs[0] = vm.addr(1234); - addrs[1] = vm.addr(2345); - addrs[2] = vm.addr(3456); - - // No - assertEq(multisig.isMember(addrs[0]), false, "Should not be a member"); - assertEq(multisig.isMember(addrs[1]), false, "Should not be a member"); - assertEq(multisig.isMember(addrs[2]), false, "Should not be a member"); - - vm.expectEmit(); - emit MembersAdded({members: addrs}); - multisig.addAddresses(addrs); - - // Yes - assertEq(multisig.isMember(addrs[0]), true, "Should be a member"); - assertEq(multisig.isMember(addrs[1]), true, "Should be a member"); - assertEq(multisig.isMember(addrs[2]), true, "Should be a member"); - } - - function test_RemovesMembersAndEmits() public { - dao.grant(address(multisig), alice, multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ onlyListed: true, - minApprovals: 1, - destinationProposalDuration: 4 days, + minApprovals: 0, + destinationProposalDuration: 4 days, // More than 4 + signerList: signerList, proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); + vm.expectRevert(abi.encodeWithSelector(Multisig.MinApprovalsOutOfBounds.selector, 1, 0)); multisig.updateMultisigSettings(settings); - // Before - assertEq(multisig.isMember(alice), true, "Should be a member"); - assertEq(multisig.isMember(bob), true, "Should be a member"); - assertEq(multisig.isMember(carol), true, "Should be a member"); - assertEq(multisig.isMember(david), true, "Should be a member"); - - address[] memory addrs = new address[](2); - addrs[0] = alice; - addrs[1] = bob; - - vm.expectEmit(); - emit MembersRemoved({members: addrs}); - multisig.removeAddresses(addrs); - - // After - assertEq(multisig.isMember(alice), false, "Should not be a member"); - assertEq(multisig.isMember(bob), false, "Should not be a member"); - assertEq(multisig.isMember(carol), true, "Should be a member"); - assertEq(multisig.isMember(david), true, "Should be a member"); - - // Next - addrs = new address[](3); - addrs[0] = vm.addr(1234); - addrs[1] = vm.addr(2345); - addrs[2] = vm.addr(3456); - multisig.addAddresses(addrs); - - // Remove - addrs = new address[](2); - addrs[0] = carol; - addrs[1] = david; - - vm.expectEmit(); - emit MembersRemoved({members: addrs}); - multisig.removeAddresses(addrs); - - // Yes - assertEq(multisig.isMember(carol), false, "Should not be a member"); - assertEq(multisig.isMember(david), false, "Should not be a member"); - } - - function test_RevertsIfAddingTooManyMembers() public { - dao.grant(address(multisig), alice, multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); - - address[] memory addrs = new address[](type(uint16).max); - addrs[0] = address(12345678); - - assertEq(multisig.isMember(addrs[0]), false, "Should not be a member"); - vm.expectRevert( - abi.encodeWithSelector( - Multisig.AddresslistLengthOutOfBounds.selector, type(uint16).max, uint256(type(uint16).max) + 4 - ) - ); - multisig.addAddresses(addrs); - - assertEq(multisig.isMember(addrs[0]), false, "Should not be a member"); - } - - function test_ShouldRevertIfEmptySignersList() public { - dao.grant(address(multisig), alice, multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); - Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ - onlyListed: true, - minApprovals: 1, - destinationProposalDuration: 4 days, + // It should revert (with onlyListed false) + settings = Multisig.MultisigSettings({ + onlyListed: false, + minApprovals: 0, + destinationProposalDuration: 4 days, // More than 4 + signerList: signerList, proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - multisig.updateMultisigSettings(settings); - - // Before - assertEq(multisig.isMember(alice), true, "Should be a member"); - assertEq(multisig.isMember(bob), true, "Should be a member"); - assertEq(multisig.isMember(carol), true, "Should be a member"); - assertEq(multisig.isMember(david), true, "Should be a member"); - - // ok - address[] memory addrs = new address[](1); - addrs[0] = alice; - multisig.removeAddresses(addrs); - - addrs[0] = bob; - multisig.removeAddresses(addrs); - - addrs[0] = carol; - multisig.removeAddresses(addrs); - - assertEq(multisig.isMember(alice), false, "Should not be a member"); - assertEq(multisig.isMember(bob), false, "Should not be a member"); - assertEq(multisig.isMember(carol), false, "Should not be a member"); - assertEq(multisig.isMember(david), true, "Should be a member"); - - // ko - addrs[0] = david; vm.expectRevert(abi.encodeWithSelector(Multisig.MinApprovalsOutOfBounds.selector, 1, 0)); - multisig.removeAddresses(addrs); - - // Next - addrs = new address[](1); - addrs[0] = vm.addr(1234); - multisig.addAddresses(addrs); + multisig.updateMultisigSettings(settings); - // Retry removing David - addrs = new address[](1); - addrs[0] = david; + // It should not revert otherwise - multisig.removeAddresses(addrs); + settings.minApprovals = 1; + multisig.updateMultisigSettings(settings); - // Yes - assertEq(multisig.isMember(david), false, "Should not be a member"); + settings.onlyListed = true; + multisig.updateMultisigSettings(settings); } - function test_ShouldRevertIfLessThanMinApproval() public { + function test_RevertWhen_SignerListIsInvalidOnUpdateSettings() external whenCallingUpdateSettings { + // It should revert dao.grant(address(multisig), alice, multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); - // Before - assertEq(multisig.isMember(alice), true, "Should be a member"); - assertEq(multisig.isMember(bob), true, "Should be a member"); - assertEq(multisig.isMember(carol), true, "Should be a member"); - assertEq(multisig.isMember(david), true, "Should be a member"); - - // ok - address[] memory addrs = new address[](1); - addrs[0] = alice; - multisig.removeAddresses(addrs); - - // ko - addrs[0] = bob; - vm.expectRevert(abi.encodeWithSelector(Multisig.MinApprovalsOutOfBounds.selector, 3, 2)); - multisig.removeAddresses(addrs); - - // ko - addrs[0] = carol; - vm.expectRevert(abi.encodeWithSelector(Multisig.MinApprovalsOutOfBounds.selector, 3, 2)); - multisig.removeAddresses(addrs); - // ko - addrs[0] = david; - vm.expectRevert(abi.encodeWithSelector(Multisig.MinApprovalsOutOfBounds.selector, 3, 2)); - multisig.removeAddresses(addrs); - - // Add and retry removing - - addrs = new address[](1); - addrs[0] = vm.addr(1234); - multisig.addAddresses(addrs); - - addrs = new address[](1); - addrs[0] = bob; - multisig.removeAddresses(addrs); - - // 2 - addrs = new address[](1); - addrs[0] = vm.addr(2345); - multisig.addAddresses(addrs); - - addrs = new address[](1); - addrs[0] = carol; - multisig.removeAddresses(addrs); - - // 3 - addrs = new address[](1); - addrs[0] = vm.addr(3456); - multisig.addAddresses(addrs); - - addrs = new address[](1); - addrs[0] = david; - multisig.removeAddresses(addrs); - } - - function test_MinApprovalsBiggerThanTheListReverts() public { - // MinApprovals should be within the boundaries of the list - dao.grant(address(multisig), alice, multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); - Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ - onlyListed: true, - minApprovals: 5, - destinationProposalDuration: 4 days, // More than 4 + onlyListed: false, + minApprovals: 1, + destinationProposalDuration: 10 days, + signerList: SignerList(address(dao)), proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - vm.expectRevert(abi.encodeWithSelector(Multisig.MinApprovalsOutOfBounds.selector, 4, 5)); - multisig.updateMultisigSettings(settings); - - // More signers - - address[] memory signers = new address[](1); - signers[0] = randomWallet; - multisig.addAddresses(signers); - - // should not fail now + vm.expectRevert(abi.encodeWithSelector(Multisig.InvalidSignerList.selector, address(dao))); multisig.updateMultisigSettings(settings); - // More than that, should fail again + // ko 2 settings = Multisig.MultisigSettings({ - onlyListed: true, - minApprovals: 6, - destinationProposalDuration: 4 days, // More than 5 + onlyListed: false, + minApprovals: 1, + destinationProposalDuration: 10 days, + signerList: SignerList(address(builder)), proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - vm.expectRevert(abi.encodeWithSelector(Multisig.MinApprovalsOutOfBounds.selector, 5, 6)); + vm.expectRevert(); multisig.updateMultisigSettings(settings); - } - - function test_ShouldRevertIfDuplicatingAddresses() public { - dao.grant(address(multisig), alice, multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); - - // ok - address[] memory addrs = new address[](1); - addrs[0] = vm.addr(1234); - multisig.addAddresses(addrs); - - // ko - vm.expectRevert(abi.encodeWithSelector(InvalidAddresslistUpdate.selector, addrs[0])); - multisig.addAddresses(addrs); - - // 1 - addrs[0] = alice; - vm.expectRevert(abi.encodeWithSelector(InvalidAddresslistUpdate.selector, addrs[0])); - multisig.addAddresses(addrs); - - // 2 - addrs[0] = bob; - vm.expectRevert(abi.encodeWithSelector(InvalidAddresslistUpdate.selector, addrs[0])); - multisig.addAddresses(addrs); - - // 3 - addrs[0] = carol; - vm.expectRevert(abi.encodeWithSelector(InvalidAddresslistUpdate.selector, addrs[0])); - multisig.addAddresses(addrs); - - // 4 - addrs[0] = david; - vm.expectRevert(abi.encodeWithSelector(InvalidAddresslistUpdate.selector, addrs[0])); - multisig.addAddresses(addrs); - - // ok - addrs[0] = vm.addr(1234); - multisig.removeAddresses(addrs); - - // ko - vm.expectRevert(abi.encodeWithSelector(InvalidAddresslistUpdate.selector, addrs[0])); - multisig.removeAddresses(addrs); - - addrs[0] = vm.addr(2345); - vm.expectRevert(abi.encodeWithSelector(InvalidAddresslistUpdate.selector, addrs[0])); - multisig.removeAddresses(addrs); - - addrs[0] = vm.addr(3456); - vm.expectRevert(abi.encodeWithSelector(InvalidAddresslistUpdate.selector, addrs[0])); - multisig.removeAddresses(addrs); - - addrs[0] = vm.addr(4567); - vm.expectRevert(abi.encodeWithSelector(InvalidAddresslistUpdate.selector, addrs[0])); - multisig.removeAddresses(addrs); - - addrs[0] = randomWallet; - vm.expectRevert(abi.encodeWithSelector(InvalidAddresslistUpdate.selector, addrs[0])); - multisig.removeAddresses(addrs); - } - - function test_onlyWalletWithPermissionsCanAddRemove() public { - // ko - address[] memory addrs = new address[](1); - addrs[0] = vm.addr(1234); - vm.expectRevert( - abi.encodeWithSelector( - DaoUnauthorized.selector, - address(dao), - address(multisig), - alice, - multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID() - ) - ); - multisig.addAddresses(addrs); - - // ko - addrs[0] = alice; - vm.expectRevert( - abi.encodeWithSelector( - DaoUnauthorized.selector, - address(dao), - address(multisig), - alice, - multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID() - ) - ); - multisig.removeAddresses(addrs); - - // Permission - dao.grant(address(multisig), alice, multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); // ok - addrs[0] = vm.addr(1234); - multisig.addAddresses(addrs); - - addrs[0] = alice; - multisig.removeAddresses(addrs); - } - - function testFuzz_PermissionedAddRemoveMembers(address randomAccount) public { - dao.grant(address(multisig), alice, multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); - - assertEq(multisig.isMember(randomWallet), false, "Should be false"); - - // in - address[] memory addrs = new address[](1); - addrs[0] = randomWallet; - multisig.addAddresses(addrs); - assertEq(multisig.isMember(randomWallet), true, "Should be true"); - - // out - multisig.removeAddresses(addrs); - assertEq(multisig.isMember(randomWallet), false, "Should be false"); - - // someone else - if (randomAccount != alice) { - vm.startPrank(randomAccount); - vm.expectRevert( - abi.encodeWithSelector( - DaoUnauthorized.selector, - address(dao), - address(multisig), - randomAccount, - multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID() - ) - ); - multisig.addAddresses(addrs); - assertEq(multisig.isMember(randomWallet), false, "Should be false"); - - addrs[0] = carol; - assertEq(multisig.isMember(carol), true, "Should be true"); - vm.expectRevert( - abi.encodeWithSelector( - DaoUnauthorized.selector, - address(dao), - address(multisig), - randomAccount, - multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID() - ) - ); - multisig.removeAddresses(addrs); - - assertEq(multisig.isMember(carol), true, "Should be true"); - } - - vm.startPrank(alice); + (,,,,, SignerList newSignerList,,) = builder.build(); + settings = Multisig.MultisigSettings({ + onlyListed: false, + minApprovals: 1, + destinationProposalDuration: 10 days, + signerList: newSignerList, + proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD + }); + multisig.updateMultisigSettings(settings); } function testFuzz_PermissionedUpdateSettings(address randomAccount) public { dao.grant(address(multisig), alice, multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); - (bool onlyListed, uint16 minApprovals, uint64 destMinDuration, uint64 expiration) = multisig.multisigSettings(); + (bool onlyListed, uint16 minApprovals, uint64 destMinDuration, SignerList givenSignerList, uint64 expiration) = + multisig.multisigSettings(); assertEq(minApprovals, 3, "Should be 3"); assertEq(onlyListed, true, "Should be true"); - assertEq(destMinDuration, 10 days, "Incorrect destMinDuration A"); - assertEq(expiration, 10 days, "Incorrect expiration A"); + assertEq(destMinDuration, 9 days, "Incorrect destMinDuration"); + assertEq(address(givenSignerList), address(signerList), "Incorrect addresslistSource"); + assertEq(expiration, 10 days, "Should be 10"); // in + (,,,,, SignerList newSignerList,,) = builder.build(); Multisig.MultisigSettings memory newSettings = Multisig.MultisigSettings({ onlyListed: false, minApprovals: 2, destinationProposalDuration: 5 days, - proposalExpirationPeriod: 6 days + signerList: newSignerList, + proposalExpirationPeriod: 4 days }); multisig.updateMultisigSettings(newSettings); - (onlyListed, minApprovals, destMinDuration, expiration) = multisig.multisigSettings(); + (onlyListed, minApprovals, destMinDuration, givenSignerList, expiration) = multisig.multisigSettings(); assertEq(minApprovals, 2, "Should be 2"); assertEq(onlyListed, false, "Should be false"); assertEq(destMinDuration, 5 days, "Incorrect destMinDuration B"); - assertEq(expiration, 6 days, "Incorrect expiration B"); + assertEq(address(givenSignerList), address(newSignerList), "Incorrect signerList"); + assertEq(expiration, 4 days, "Should be 4"); // out newSettings = Multisig.MultisigSettings({ onlyListed: true, minApprovals: 1, destinationProposalDuration: 6 days, - proposalExpirationPeriod: 9 days + signerList: signerList, + proposalExpirationPeriod: 1 days }); multisig.updateMultisigSettings(newSettings); - (onlyListed, minApprovals, destMinDuration, expiration) = multisig.multisigSettings(); + (onlyListed, minApprovals, destMinDuration, givenSignerList, expiration) = multisig.multisigSettings(); assertEq(minApprovals, 1, "Should be 1"); assertEq(onlyListed, true, "Should be true"); - assertEq(destMinDuration, 6 days, "Incorrect destMinDuration C"); - assertEq(expiration, 9 days, "Incorrect expiration C"); + assertEq(destMinDuration, 6 days, "Incorrect destMinDuration B"); + assertEq(address(givenSignerList), address(signerList), "Incorrect signerList"); + assertEq(expiration, 1 days, "Should be 1"); vm.roll(block.number + 1); // someone else - if (randomAccount != alice) { + if (randomAccount != alice && randomAccount != address(0)) { vm.startPrank(randomAccount); + (,,,,, newSignerList,,) = builder.build(); newSettings = Multisig.MultisigSettings({ onlyListed: false, minApprovals: 4, destinationProposalDuration: 4 days, - proposalExpirationPeriod: 1 days + signerList: newSignerList, + proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); vm.expectRevert( @@ -1063,128 +750,159 @@ contract MultisigTest is AragonTest { ); multisig.updateMultisigSettings(newSettings); - (onlyListed, minApprovals, destMinDuration, expiration) = multisig.multisigSettings(); + (onlyListed, minApprovals, destMinDuration, givenSignerList, expiration) = multisig.multisigSettings(); assertEq(minApprovals, 1, "Should still be 1"); assertEq(onlyListed, true, "Should still be true"); assertEq(destMinDuration, 6 days, "Should still be 6 days"); - assertEq(expiration, 9 days, "Should still be 9 days"); + assertEq(address(givenSignerList), address(signerList), "Should still be signerList"); + assertEq(expiration, 1 days, "Should still be 1"); } + } - vm.startPrank(alice); + modifier whenCallingCreateProposal() { + _; } - // PROPOSAL CREATION + function test_WhenCallingCreateProposal() external whenCallingCreateProposal { + uint256 pid; + bool executed; + uint16 approvals; + Multisig.ProposalParameters memory parameters; + bytes memory metadataURI; + OptimisticTokenVotingPlugin destinationPlugin; + IDAO.Action[] memory inputActions = new IDAO.Action[](0); + IDAO.Action[] memory outputActions = new IDAO.Action[](0); - function test_IncrementsTheProposalCounter() public { - // increments the proposal counter + // It increments the proposal counter + // It creates and return unique proposal IDs + // It emits the ProposalCreated event + // It creates a proposal with the given values assertEq(multisig.proposalCount(), 0, "Should have no proposals"); // 1 - IDAO.Action[] memory actions = new IDAO.Action[](0); - multisig.createProposal("", actions, optimisticPlugin, false); - - assertEq(multisig.proposalCount(), 1, "Should have 1 proposal"); - - // 2 - multisig.createProposal("ipfs://", actions, optimisticPlugin, true); - - assertEq(multisig.proposalCount(), 2, "Should have 2 proposals"); - } - - function test_CreatesAndReturnsUniqueProposalIds() public { - // creates unique proposal IDs for each proposal - - // 1 - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); - - assertEq(pid, 0, "Should be 0"); - - // 2 - pid = multisig.createProposal("ipfs://", actions, optimisticPlugin, true); - - assertEq(pid, 1, "Should be 1"); - - // 3 - pid = multisig.createProposal("ipfs://more", actions, optimisticPlugin, true); - - assertEq(pid, 2, "Should be 2"); - } - - function test_EmitsProposalCreated() public { - // emits the `ProposalCreated` event - - IDAO.Action[] memory actions = new IDAO.Action[](0); vm.expectEmit(); emit ProposalCreated({ proposalId: 0, creator: alice, - metadata: "", + metadata: "ipfs://", startDate: uint64(block.timestamp), - endDate: uint64(block.timestamp) + 10 days, - actions: actions, + endDate: uint64(block.timestamp) + MULTISIG_PROPOSAL_EXPIRATION_PERIOD, + actions: inputActions, allowFailureMap: 0 }); - multisig.createProposal("", actions, optimisticPlugin, true); + multisig.createProposal("ipfs://", inputActions, optimisticPlugin, false); + assertEq(pid, 0, "Should be 0"); + assertEq(multisig.proposalCount(), 1, "Should have 1 proposal"); + + (executed, approvals, parameters, metadataURI, outputActions, destinationPlugin) = multisig.getProposal(pid); + assertEq(executed, false, "Should be false"); + assertEq(approvals, 0, "Should be 0"); + assertEq(parameters.minApprovals, 3, "Incorrect minApprovals"); + assertEq(parameters.snapshotBlock, block.number - 1, "Incorrect snapshotBlock"); + assertEq( + parameters.expirationDate, block.timestamp + MULTISIG_PROPOSAL_EXPIRATION_PERIOD, "Incorrect expirationDate" + ); + assertEq(metadataURI, "ipfs://", "Incorrect metadataURI"); + assertEq(outputActions.length, 0, "Incorrect actions length"); + assertEq(address(destinationPlugin), address(optimisticPlugin), "Incorrect destinationPlugin"); // 2 vm.startPrank(bob); + vm.roll(block.number + 100); + vm.warp(block.timestamp + 100); - actions = new IDAO.Action[](1); - actions[0].to = carol; - actions[0].value = 1 ether; + inputActions = new IDAO.Action[](1); + inputActions[0].to = carol; + inputActions[0].value = 1 ether; address[] memory addrs = new address[](1); - actions[0].data = abi.encodeCall(Multisig.addAddresses, (addrs)); + inputActions[0].data = abi.encodeCall(SignerList.addSigners, (addrs)); vm.expectEmit(); emit ProposalCreated({ proposalId: 1, creator: bob, - metadata: "ipfs://", + metadata: "ipfs://more", + startDate: uint64(block.timestamp), + endDate: uint64(block.timestamp) + MULTISIG_PROPOSAL_EXPIRATION_PERIOD, + actions: inputActions, + allowFailureMap: 0 + }); + pid = multisig.createProposal("ipfs://more", inputActions, optimisticPlugin, true); + + assertEq(pid, 1, "Should be 1"); + assertEq(multisig.proposalCount(), 2, "Should have 2 proposals"); + + (executed, approvals, parameters, metadataURI, outputActions, destinationPlugin) = multisig.getProposal(pid); + assertEq(executed, false, "Should be false"); + assertEq(approvals, 1, "Should be 1"); + assertEq(parameters.minApprovals, 3, "Incorrect minApprovals"); + assertEq(parameters.snapshotBlock, block.number - 1, "Incorrect snapshotBlock"); + assertEq( + parameters.expirationDate, block.timestamp + MULTISIG_PROPOSAL_EXPIRATION_PERIOD, "Incorrect expirationDate" + ); + assertEq(metadataURI, "ipfs://more", "Incorrect metadataURI"); + assertEq(outputActions.length, 1, "Incorrect actions length"); + assertEq(outputActions[0].to, carol, "Incorrect to"); + assertEq(outputActions[0].value, 1 ether, "Incorrect value"); + assertEq(outputActions[0].data, abi.encodeCall(SignerList.addSigners, (addrs)), "Incorrect data"); + assertEq(address(destinationPlugin), address(optimisticPlugin), "Incorrect destinationPlugin"); + + // 3 + vm.startPrank(carol); + vm.roll(block.number + 100); + vm.warp(block.timestamp + 100); + + OptimisticTokenVotingPlugin newOptimistic; + (, newOptimistic, multisig,,,,,) = builder.withMinApprovals(2).build(); + + vm.expectEmit(); + emit ProposalCreated({ + proposalId: 0, + creator: carol, + metadata: "ipfs://1234", startDate: uint64(block.timestamp), - endDate: uint64(block.timestamp) + 10 days, - actions: actions, + endDate: uint64(block.timestamp) + MULTISIG_PROPOSAL_EXPIRATION_PERIOD, + actions: inputActions, allowFailureMap: 0 }); - multisig.createProposal("ipfs://", actions, optimisticPlugin, false); + pid = multisig.createProposal("ipfs://1234", inputActions, newOptimistic, true); + + (,, parameters,,, destinationPlugin) = multisig.getProposal(pid); + assertEq(parameters.minApprovals, 2, "Incorrect minApprovals"); + assertEq(address(destinationPlugin), address(newOptimistic), "Incorrect destinationPlugin"); } - function test_RevertsIfSettingsChangedInSameBlock() public { - // reverts if the multisig settings have changed in the same block + function test_GivenSettingsChangedOnTheSameBlock() external whenCallingCreateProposal { + // It reverts - // Deploy a new multisig instance Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ onlyListed: false, minApprovals: 3, destinationProposalDuration: 4 days, + signerList: signerList, proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD }); - address[] memory signers = new address[](4); - signers[0] = alice; - signers[1] = bob; - signers[2] = carol; - signers[3] = david; - - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings))) - ); + multisig = + Multisig(createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings)))); // 1 IDAO.Action[] memory actions = new IDAO.Action[](0); vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalCreationForbidden.selector, alice)); multisig.createProposal("", actions, optimisticPlugin, false); + // It does not revert otherwise + // Next block vm.roll(block.number + 1); multisig.createProposal("", actions, optimisticPlugin, false); } - function test_CreatesWhenUnlistedAccountsAllowed() public { - // creates a proposal when unlisted accounts are allowed + function test_GivenOnlyListedIsFalse() external whenCallingCreateProposal { + // It allows anyone to create builder = new DaoBuilder(); - (dao, optimisticPlugin, multisig,,,) = builder.withMultisigMember(alice).withoutOnlyListed().build(); + (, optimisticPlugin, multisig,,,,,) = builder.withMultisigMember(alice).withoutOnlyListed().build(); vm.startPrank(randomWallet); @@ -1192,642 +910,411 @@ contract MultisigTest is AragonTest { multisig.createProposal("", actions, optimisticPlugin, false); } - function test_RevertsWhenOnlyListedAndTheWalletIsNotListed() public { - // reverts if the user is not on the list and only listed accounts can create proposals - - vm.startPrank(randomWallet); + modifier givenOnlyListedIsTrue() { + _; + } + function test_GivenCreationCallerIsNotListedOrAppointed() + external + whenCallingCreateProposal + givenOnlyListedIsTrue + { IDAO.Action[] memory actions = new IDAO.Action[](0); + + // It reverts + + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 3)); + + vm.startPrank(randomWallet); vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalCreationForbidden.selector, randomWallet)); multisig.createProposal("", actions, optimisticPlugin, false); - } - function test_RevertsWhenCreatorWasListedBeforeButNotNow() public { - // reverts if `_msgSender` is not listed although she was listed in the last block - - // Deploy a new multisig instance - Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ - onlyListed: true, - minApprovals: 1, - destinationProposalDuration: 4 days, - proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - address[] memory addrs = new address[](1); - addrs[0] = alice; + // 2 + vm.startPrank(taikoBridge); + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalCreationForbidden.selector, taikoBridge)); + multisig.createProposal("", actions, optimisticPlugin, false); - multisig = Multisig( - createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, addrs, settings))) - ); - dao.grant(address(multisig), alice, multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); - vm.roll(block.number + 1); + // It reverts if listed before but not now - // Add+remove - addrs[0] = bob; - multisig.addAddresses(addrs); + vm.startPrank(alice); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + address[] memory addrs = new address[](1); addrs[0] = alice; - multisig.removeAddresses(addrs); + signerList.removeSigners(addrs); - // Alice cannot create now - IDAO.Action[] memory actions = new IDAO.Action[](0); vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalCreationForbidden.selector, alice)); multisig.createProposal("", actions, optimisticPlugin, false); - - // Bob can create now - vm.startPrank(bob); - - multisig.createProposal("", actions, optimisticPlugin, false); - - assertEq(multisig.isListed(alice), false, "Should not be listed"); - assertEq(multisig.isListed(bob), true, "Should be listed"); } - function test_CreatesProposalWithoutApprovingIfUnspecified() public { - // creates a proposal successfully and does not approve if not specified - + function test_GivenCreationCallerIsAppointedByAFormerSigner() + external + whenCallingCreateProposal + givenOnlyListedIsTrue + { IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal( - "", - actions, - optimisticPlugin, - false // approveProposal - ); - assertEq(multisig.hasApproved(pid, alice), false, "Should not have approved"); - (, uint16 approvals,,,,) = multisig.getProposal(pid); - assertEq(approvals, 0, "Should be 0"); + // It reverts - multisig.approve(pid, false); + encryptionRegistry.appointWallet(randomWallet); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 3)); - assertEq(multisig.hasApproved(pid, alice), true, "Should have approved"); - (, approvals,,,,) = multisig.getProposal(pid); - assertEq(approvals, 1, "Should be 1"); - } + address[] memory addrs = new address[](1); + addrs[0] = alice; + signerList.removeSigners(addrs); - function test_CreatesAndApprovesWhenSpecified() public { - // creates a proposal successfully and approves if specified + vm.startPrank(randomWallet); + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalCreationForbidden.selector, randomWallet)); + multisig.createProposal("", actions, optimisticPlugin, false); - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal( - "", - actions, - optimisticPlugin, - true // approveProposal - ); - assertEq(multisig.hasApproved(pid, alice), true, "Should have approved"); - (, uint16 approvals,,,,) = multisig.getProposal(pid); - assertEq(approvals, 1, "Should be 1"); + // Undo + vm.startPrank(alice); + signerList.addSigners(addrs); + + vm.startPrank(randomWallet); + multisig.createProposal("", actions, optimisticPlugin, false); } - // CAN APPROVE + function test_GivenCreationCallerIsListedAndSelfAppointed() + external + whenCallingCreateProposal + givenOnlyListedIsTrue + { + IDAO.Action[] memory actions = new IDAO.Action[](0); - function testFuzz_CanApproveReturnsfFalseIfNotCreated(uint256 randomProposalId) public view { - // returns `false` if the proposal doesn't exist + // It creates the proposal - assertEq(multisig.canApprove(randomProposalId, alice), false, "Should be false"); - assertEq(multisig.canApprove(randomProposalId, bob), false, "Should be false"); - assertEq(multisig.canApprove(randomProposalId, carol), false, "Should be false"); - assertEq(multisig.canApprove(randomProposalId, david), false, "Should be false"); - } + vm.startPrank(alice); + multisig.createProposal("a", actions, optimisticPlugin, false); - function testFuzz_CanApproveReturnsfFalseIfNotListed(address randomWallet) public { - // returns `false` if the approver is not listed + vm.startPrank(bob); + multisig.createProposal("b", actions, optimisticPlugin, false); - { - // Deploy a new multisig instance (more efficient than the builder for fuzz testing) - Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ - onlyListed: true, - minApprovals: 1, - destinationProposalDuration: 4 days, - proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - address[] memory signers = new address[](1); - signers[0] = alice; + vm.startPrank(carol); + multisig.createProposal("c", actions, optimisticPlugin, false); - multisig = Multisig( - createProxyAndCall( - address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings)) - ) - ); - vm.roll(block.number + 1); - } + vm.startPrank(david); + multisig.createProposal("d", actions, optimisticPlugin, false); + } + function test_GivenCreationCallerIsListedAppointingSomeoneElseNow() + external + whenCallingCreateProposal + givenOnlyListedIsTrue + { IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); - // ko - if (randomWallet != alice) { - assertEq(multisig.canApprove(pid, randomWallet), false, "Should be false"); - } + // It creates the proposal - // static ok - assertEq(multisig.canApprove(pid, alice), true, "Should be true"); - } + vm.startPrank(alice); + encryptionRegistry.appointWallet(address(0x1234)); + multisig.createProposal("a", actions, optimisticPlugin, false); + + vm.startPrank(bob); + encryptionRegistry.appointWallet(address(0x2345)); + multisig.createProposal("b", actions, optimisticPlugin, false); - function test_CanApproveReturnsFalseIfApproved() public { - // returns `false` if the approver has already approved + vm.startPrank(carol); + encryptionRegistry.appointWallet(address(0x3456)); + multisig.createProposal("c", actions, optimisticPlugin, false); - builder = new DaoBuilder(); - (dao, optimisticPlugin, multisig,,,) = builder.withMultisigMember(alice).withMultisigMember(bob) - .withMultisigMember(carol).withMultisigMember(david).withMinApprovals(4).build(); + vm.startPrank(david); + encryptionRegistry.appointWallet(address(0x4567)); + multisig.createProposal("d", actions, optimisticPlugin, false); + } + function test_GivenCreationCallerIsAppointedByACurrentSigner() + external + whenCallingCreateProposal + givenOnlyListedIsTrue + { IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); - // Alice - assertEq(multisig.canApprove(pid, alice), true, "Should be true"); - multisig.approve(pid, false); - assertEq(multisig.canApprove(pid, alice), false, "Should be false"); + // It creates the proposal + + vm.startPrank(alice); + encryptionRegistry.appointWallet(address(0x1234)); + vm.startPrank(address(0x1234)); + multisig.createProposal("a", actions, optimisticPlugin, false); - // Bob - assertEq(multisig.canApprove(pid, bob), true, "Should be true"); vm.startPrank(bob); - multisig.approve(pid, false); - assertEq(multisig.canApprove(pid, bob), false, "Should be false"); + encryptionRegistry.appointWallet(address(0x2345)); + vm.startPrank(address(0x2345)); + multisig.createProposal("b", actions, optimisticPlugin, false); - // Carol - assertEq(multisig.canApprove(pid, carol), true, "Should be true"); vm.startPrank(carol); - multisig.approve(pid, false); - assertEq(multisig.canApprove(pid, carol), false, "Should be false"); + encryptionRegistry.appointWallet(address(0x3456)); + vm.startPrank(address(0x3456)); + multisig.createProposal("c", actions, optimisticPlugin, false); - // David - assertEq(multisig.canApprove(pid, david), true, "Should be true"); vm.startPrank(david); - multisig.approve(pid, false); - assertEq(multisig.canApprove(pid, david), false, "Should be false"); + encryptionRegistry.appointWallet(address(0x4567)); + vm.startPrank(address(0x4567)); + multisig.createProposal("d", actions, optimisticPlugin, false); } - function test_CanApproveReturnsFalseIfExpired() public { - // returns `false` if the proposal has ended - - uint64 startDate = 10; - vm.warp(startDate); - + function test_GivenApproveProposalIsTrue() external whenCallingCreateProposal { + uint256 pid; + uint256 approvals; IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); - - (,, Multisig.ProposalParameters memory parameters,,,) = multisig.getProposal(pid); - assertEq(parameters.expirationDate, startDate + MULTISIG_PROPOSAL_EXPIRATION_PERIOD, "Incorrect expiration"); - assertEq(multisig.canApprove(pid, alice), true, "Should be true"); + // It creates and calls approval in one go - vm.warp(startDate + MULTISIG_PROPOSAL_EXPIRATION_PERIOD - 1); // multisig expiration time - 1 - assertEq(multisig.canApprove(pid, alice), true, "Should be true"); + vm.startPrank(alice); + pid = multisig.createProposal("a", actions, optimisticPlugin, true); + (, approvals,,,,) = multisig.getProposal(pid); + assertEq(approvals, 1, "Should be 1"); - vm.warp(startDate + MULTISIG_PROPOSAL_EXPIRATION_PERIOD); // multisig expiration time - assertEq(multisig.canApprove(pid, alice), false, "Should be false"); + vm.startPrank(bob); + pid = multisig.createProposal("b", actions, optimisticPlugin, true); + (, approvals,,,,) = multisig.getProposal(pid); + assertEq(approvals, 1, "Should be 1"); + } - // Start later - startDate = 5 days; - vm.warp(startDate); - pid = multisig.createProposal("", actions, optimisticPlugin, false); + function test_GivenApproveProposalIsFalse() external whenCallingCreateProposal { + uint256 pid; + uint256 approvals; + IDAO.Action[] memory actions = new IDAO.Action[](0); - assertEq(multisig.canApprove(pid, alice), true, "Should be true"); + // It only creates the proposal - vm.warp(block.timestamp + MULTISIG_PROPOSAL_EXPIRATION_PERIOD - 1); // expiration time - 1 - assertEq(multisig.canApprove(pid, alice), true, "Should be true"); + vm.startPrank(carol); + pid = multisig.createProposal("c", actions, optimisticPlugin, false); + (, approvals,,,,) = multisig.getProposal(pid); + assertEq(approvals, 0, "Should be 0"); - vm.warp(block.timestamp + 1); // expiration time - assertEq(multisig.canApprove(pid, alice), false, "Should be false"); + vm.startPrank(david); + pid = multisig.createProposal("d", actions, optimisticPlugin, false); + (, approvals,,,,) = multisig.getProposal(pid); + assertEq(approvals, 0, "Should be 0"); } - function test_CanApproveReturnsFalseIfExecuted() public { - // returns `false` if the proposal is already executed + modifier givenTheProposalIsNotCreated() { + // Alice: listed and self appointed - dao.grant(address(optimisticPlugin), address(multisig), optimisticPlugin.PROPOSER_PERMISSION_ID()); + // Bob: listed, appointing someone else now + vm.startPrank(bob); + encryptionRegistry.appointWallet(randomWallet); - bool executed; - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); + // Random Wallet: appointed by a listed signer - // Alice - multisig.approve(pid, false); + // 0x1234: unlisted and unappointed - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + _; + } - // Bob - vm.startPrank(bob); - multisig.approve(pid, false); + function test_WhenCallingGetProposalBeingUncreated() external givenTheProposalIsNotCreated { + // It should return empty values - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + bool executed; + uint16 approvals; + Multisig.ProposalParameters memory parameters; + bytes memory metadataURI; + IDAO.Action[] memory actions = new IDAO.Action[](0); + OptimisticTokenVotingPlugin destinationPlugin; - // Carol - vm.startPrank(carol); - multisig.approve(pid, true); // auto execute + (executed, approvals, parameters, metadataURI, actions, destinationPlugin) = multisig.getProposal(1234); - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, true, "Should be executed"); + assertEq(executed, false, "Should be false"); + assertEq(approvals, 0, "Should be 0"); + assertEq(parameters.minApprovals, 0, "Incorrect minApprovals"); + assertEq(parameters.snapshotBlock, 0, "Incorrect snapshotBlock"); + assertEq(parameters.expirationDate, 0, "Incorrect expirationDate"); + assertEq(metadataURI, "", "Incorrect metadataURI"); + assertEq(actions.length, 0, "Incorrect actions.length"); + assertEq(address(destinationPlugin), address(0), "Incorrect destinationPlugin"); + } - // David cannot approve - assertEq(multisig.canApprove(pid, david), false, "Should be false"); + function test_WhenCallingCanApproveOrApproveBeingUncreated() external givenTheProposalIsNotCreated { + uint256 randomProposalId = 1234; + bool canApprove; + // It canApprove should return false (when listed and self appointed) vm.startPrank(alice); - } + canApprove = multisig.canApprove(randomProposalId, alice); + assertEq(canApprove, false, "Should be false"); - function test_CanApproveReturnsTrueIfListed() public { - // returns `true` if the approver is listed + // It approve should revert (when listed and self appointed) + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, randomProposalId, alice)); + multisig.approve(randomProposalId, true); - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); + // It canApprove should return false (when listed, appointing someone else now) + randomProposalId++; + vm.startPrank(bob); + canApprove = multisig.canApprove(randomProposalId, bob); + assertEq(canApprove, false, "Should be false"); - assertEq(multisig.canApprove(pid, alice), true, "Should be true"); - assertEq(multisig.canApprove(pid, bob), true, "Should be true"); - assertEq(multisig.canApprove(pid, carol), true, "Should be true"); - assertEq(multisig.canApprove(pid, david), true, "Should be true"); + // It approve should revert (when listed, appointing someone else now) + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, randomProposalId, bob)); + multisig.approve(randomProposalId, true); - // new instance - builder = new DaoBuilder(); - (dao, optimisticPlugin, multisig,,,) = builder.withMultisigMember(randomWallet).withoutOnlyListed().build(); + // It canApprove should return false (when appointed by a listed signer) + randomProposalId++; + vm.startPrank(randomWallet); + canApprove = multisig.canApprove(randomProposalId, randomWallet); + assertEq(canApprove, false, "Should be false"); - // now ko - actions = new IDAO.Action[](0); - pid = multisig.createProposal("", actions, optimisticPlugin, false); + // It approve should revert (when appointed by a listed signer) + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, randomProposalId, randomWallet)); + multisig.approve(randomProposalId, false); - assertEq(multisig.canApprove(pid, alice), false, "Should be false"); - assertEq(multisig.canApprove(pid, bob), false, "Should be false"); - assertEq(multisig.canApprove(pid, carol), false, "Should be false"); - assertEq(multisig.canApprove(pid, david), false, "Should be false"); + // It canApprove should return false (when unlisted and unappointed) + randomProposalId++; + vm.startPrank(address(1234)); + canApprove = multisig.canApprove(randomProposalId, address(1234)); + assertEq(canApprove, false, "Should be false"); - // ok - assertEq(multisig.canApprove(pid, randomWallet), true, "Should be true"); + // It approve should revert (when unlisted and unappointed) + vm.expectRevert( + abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, randomProposalId, address(1234)) + ); + multisig.approve(randomProposalId, false); } - // HAS APPROVED + function test_WhenCallingHasApprovedBeingUncreated() external givenTheProposalIsNotCreated { + bool hasApproved; + uint256 randomProposalId = 1234; + // It hasApproved should always return false - function test_HasApprovedReturnsFalseWhenNotApproved() public { - // returns `false` if user hasn't approved yet + hasApproved = multisig.hasApproved(randomProposalId, alice); + assertEq(hasApproved, false, "Should be false"); - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); + randomProposalId++; + hasApproved = multisig.hasApproved(randomProposalId, bob); + assertEq(hasApproved, false, "Should be false"); - // Alice - assertEq(multisig.hasApproved(pid, alice), false, "Should be false"); - assertEq(multisig.hasApproved(pid, bob), false, "Should be false"); - assertEq(multisig.hasApproved(pid, carol), false, "Should be false"); - assertEq(multisig.hasApproved(pid, david), false, "Should be false"); + randomProposalId++; + hasApproved = multisig.hasApproved(randomProposalId, randomWallet); + assertEq(hasApproved, false, "Should be false"); + + randomProposalId++; + hasApproved = multisig.hasApproved(randomProposalId, address(1234)); + assertEq(hasApproved, false, "Should be false"); } - function test_HasApprovedReturnsTrueWhenUserApproved() public { - // returns `true` if user has approved + function test_WhenCallingCanExecuteOrExecuteBeingUncreated() external givenTheProposalIsNotCreated { + bool canExecute; + uint256 randomProposalId = 1234; - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); + // It canExecute should return false (when listed and self appointed) + vm.startPrank(alice); + canExecute = multisig.canExecute(randomProposalId); + assertEq(canExecute, false, "Should be false"); - // Alice - assertEq(multisig.hasApproved(pid, alice), false, "Should be false"); - multisig.approve(pid, false); - assertEq(multisig.hasApproved(pid, alice), true, "Should be true"); + // It execute should revert (when listed and self appointed) + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, randomProposalId)); + multisig.execute(randomProposalId); - // Bob + // It canExecute should return false (when listed, appointing someone else now) + randomProposalId++; vm.startPrank(bob); - assertEq(multisig.hasApproved(pid, bob), false, "Should be false"); - multisig.approve(pid, false); - assertEq(multisig.hasApproved(pid, bob), true, "Should be true"); + canExecute = multisig.canExecute(randomProposalId); + assertEq(canExecute, false, "Should be false"); - // Carol - vm.startPrank(carol); - assertEq(multisig.hasApproved(pid, carol), false, "Should be false"); - multisig.approve(pid, false); - assertEq(multisig.hasApproved(pid, carol), true, "Should be true"); + // It execute should revert (when listed, appointing someone else now) + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, randomProposalId)); + multisig.execute(randomProposalId); - // David - vm.startPrank(david); - assertEq(multisig.hasApproved(pid, david), false, "Should be false"); - multisig.approve(pid, false); - assertEq(multisig.hasApproved(pid, david), true, "Should be true"); - } + // It canExecute should return false (when appointed by a listed signer) + randomProposalId++; + vm.startPrank(randomWallet); + canExecute = multisig.canExecute(randomProposalId); + assertEq(canExecute, false, "Should be false"); - // APPROVE + // It execute should revert (when appointed by a listed signer) + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, randomProposalId)); + multisig.execute(randomProposalId); - function testFuzz_ApproveRevertsIfNotCreated(uint256 randomProposalId) public { - // Reverts if the proposal doesn't exist + // It canExecute should return false (when unlisted and unappointed) + randomProposalId++; + vm.startPrank(address(1234)); + canExecute = multisig.canExecute(randomProposalId); + assertEq(canExecute, false, "Should be false"); - vm.startPrank(alice); - vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, randomProposalId, alice)); - multisig.approve(randomProposalId, false); - - // 2 - vm.startPrank(bob); - vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, randomProposalId, bob)); - multisig.approve(randomProposalId, false); - - // 3 - vm.startPrank(carol); - vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, randomProposalId, carol)); - multisig.approve(randomProposalId, true); - - // 4 - vm.startPrank(david); - vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, randomProposalId, david)); - multisig.approve(randomProposalId, true); - } - - function testFuzz_ApproveRevertsIfNotListed(address randomSigner) public { - // Reverts if the signer is not listed - - builder = new DaoBuilder(); - (,, multisig,,,) = builder.withMultisigMember(alice).withMinApprovals(1).build(); - - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); - - if (randomSigner == alice) { - return; - } - - vm.startPrank(randomSigner); - vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, pid, randomSigner)); - multisig.approve(pid, false); - - vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, pid, randomSigner)); - multisig.approve(pid, true); + // It execute should revert (when unlisted and unappointed) + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, randomProposalId)); + multisig.execute(randomProposalId); } - function test_ApproveRevertsIfAlreadyApproved() public { - // reverts when approving multiple times - - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); + modifier givenTheProposalIsOpen() { + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 1)); - // Alice - multisig.approve(pid, true); - - vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, pid, alice)); - multisig.approve(pid, true); + // Alice: listed on creation and self appointed - // Bob + // Bob: listed on creation, appointing someone else now vm.startPrank(bob); - multisig.approve(pid, true); - - vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, pid, bob)); - multisig.approve(pid, false); + encryptionRegistry.appointWallet(randomWallet); - // Carol - vm.startPrank(carol); - multisig.approve(pid, false); + // Random Wallet: appointed by a listed signer on creation + // 0x1234: unlisted and unappointed on creation - vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, pid, carol)); - multisig.approve(pid, true); - } + vm.deal(address(dao), 1 ether); - function test_ApprovesWithTheSenderAddress() public { - // approves with the msg.sender address - // Same as test_HasApprovedReturnsTrueWhenUserApproved() + // Create proposal 0 + IDAO.Action[] memory actions = new IDAO.Action[](2); + actions[0].value = 0.25 ether; + actions[0].to = address(alice); + actions[0].data = hex""; + actions[1].value = 0.75 ether; + actions[1].to = address(dao); + actions[1].data = abi.encodeCall(DAO.setMetadata, "ipfs://new-metadata"); + multisig.createProposal("ipfs://pub-metadata", actions, optimisticPlugin, false); - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); - - // Alice - assertEq(multisig.hasApproved(pid, alice), false, "Should be false"); - multisig.approve(pid, false); - assertEq(multisig.hasApproved(pid, alice), true, "Should be true"); - - // Bob - vm.startPrank(bob); - assertEq(multisig.hasApproved(pid, bob), false, "Should be false"); - multisig.approve(pid, false); - assertEq(multisig.hasApproved(pid, bob), true, "Should be true"); - - // Carol - vm.startPrank(carol); - assertEq(multisig.hasApproved(pid, carol), false, "Should be false"); - multisig.approve(pid, false); - assertEq(multisig.hasApproved(pid, carol), true, "Should be true"); - - // David - vm.startPrank(david); - assertEq(multisig.hasApproved(pid, david), false, "Should be false"); - multisig.approve(pid, false); - assertEq(multisig.hasApproved(pid, david), true, "Should be true"); - } - - function test_ApproveRevertsIfExpired() public { - // reverts if the proposal has ended - - uint64 expirationTime = uint64(block.timestamp) + 10 days; - - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); - - assertEq(multisig.canApprove(pid, alice), true, "Should be true"); - - vm.warp(expirationTime); - vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, pid, alice)); - multisig.approve(pid, false); - - vm.warp(expirationTime + 15 days); - vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, pid, alice)); - multisig.approve(pid, false); - - // 2 - vm.warp(1000); - expirationTime = uint64(block.timestamp) + 10 days; - pid = multisig.createProposal("", actions, optimisticPlugin, false); - - assertEq(multisig.canApprove(pid, alice), true, "Should be true"); - - vm.warp(expirationTime); - vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, pid, alice)); - multisig.approve(pid, true); + // Remove (later) + vm.roll(block.number + 50); + address[] memory addrs = new address[](2); + addrs[0] = alice; + addrs[1] = bob; + vm.startPrank(alice); + signerList.removeSigners(addrs); - vm.warp(expirationTime + 500); - vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, pid, alice)); - multisig.approve(pid, true); + _; } - function test_ApproveRevertsIfExecuted() public { - // reverts if the proposal has ended - - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); - multisig.approve(pid, false); - vm.startPrank(bob); - multisig.approve(pid, false); - vm.startPrank(carol); - multisig.approve(pid, false); - - multisig.execute(pid); - (bool executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, true, "Should be executed"); + function testFuzz_CanApproveReturnsfFalseIfNotCreated(uint256 randomProposalId) public view { + // returns `false` if the proposal doesn't exist - vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, pid, carol)); - multisig.approve(pid, false); - vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, pid, carol)); - multisig.approve(pid, true); + assertEq(multisig.canApprove(randomProposalId, alice), false, "Should be false"); + assertEq(multisig.canApprove(randomProposalId, bob), false, "Should be false"); + assertEq(multisig.canApprove(randomProposalId, carol), false, "Should be false"); + assertEq(multisig.canApprove(randomProposalId, david), false, "Should be false"); } - function test_ApprovingProposalsEmits() public { - // Approving a proposal emits the Approved event - - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); + function testFuzz_ApproveRevertsIfNotCreated(uint256 randomProposalId) public { + // Reverts if the proposal doesn't exist - vm.expectEmit(); - emit Approved(pid, alice); - multisig.approve(pid, false); + vm.startPrank(alice); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, randomProposalId, alice)); + multisig.approve(randomProposalId, false); - // Bob + // 2 vm.startPrank(bob); - vm.expectEmit(); - emit Approved(pid, bob); - multisig.approve(pid, false); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, randomProposalId, bob)); + multisig.approve(randomProposalId, false); - // Carol + // 3 vm.startPrank(carol); - vm.expectEmit(); - emit Approved(pid, carol); - multisig.approve(pid, false); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, randomProposalId, carol)); + multisig.approve(randomProposalId, true); - // David (even if it already passed) + // 4 vm.startPrank(david); - vm.expectEmit(); - emit Approved(pid, david); - multisig.approve(pid, false); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, randomProposalId, david)); + multisig.approve(randomProposalId, true); } - // CAN EXECUTE - function testFuzz_CanExecuteReturnsFalseIfNotCreated(uint256 randomProposalId) public view { // returns `false` if the proposal doesn't exist assertEq(multisig.canExecute(randomProposalId), false, "Should be false"); } - function test_CanExecuteReturnsFalseIfBelowMinApprovals() public { - // returns `false` if the proposal has not reached the minimum approvals yet - (dao, optimisticPlugin, multisig,,,) = builder.withMinApprovals(2).build(); - - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); - - // Alice - multisig.approve(pid, false); - assertEq(multisig.canExecute(pid), false, "Should be false"); - - // Bob - vm.startPrank(bob); - multisig.approve(pid, false); - assertEq(multisig.canExecute(pid), true, "Should be true"); - - vm.startPrank(alice); - - // More approvals required (4) - (dao, optimisticPlugin, multisig,,,) = builder.withMinApprovals(4).build(); - - pid = multisig.createProposal("", actions, optimisticPlugin, false); - - // Alice - vm.startPrank(alice); - multisig.approve(pid, false); - assertEq(multisig.canExecute(pid), false, "Should be false"); - - // Bob - vm.startPrank(bob); - multisig.approve(pid, false); - assertEq(multisig.canExecute(pid), false, "Should be false"); - - // Carol - vm.startPrank(carol); - multisig.approve(pid, false); - assertEq(multisig.canExecute(pid), false, "Should be false"); - - // David - vm.startPrank(david); - multisig.approve(pid, false); - assertEq(multisig.canExecute(pid), true, "Should be true"); - } - - function test_CanExecuteReturnsFalseIfExpired() public { - // returns `false` if the proposal has ended - - // 1 - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); - - multisig.approve(pid, false); - vm.startPrank(bob); - multisig.approve(pid, false); - vm.startPrank(carol); - multisig.approve(pid, false); - assertEq(multisig.canExecute(pid), true, "Should be true"); - - vm.warp(block.timestamp + 10 days - 1); - assertEq(multisig.canExecute(pid), true, "Should be true"); - - vm.warp(block.timestamp + 1); - assertEq(multisig.canExecute(pid), false, "Should be false"); - - // 2 - vm.warp(50 days); - actions = new IDAO.Action[](0); - pid = multisig.createProposal("", actions, optimisticPlugin, false); - - vm.startPrank(alice); - multisig.approve(pid, false); - vm.startPrank(bob); - multisig.approve(pid, false); - vm.startPrank(carol); - multisig.approve(pid, false); - assertEq(multisig.canExecute(pid), true, "Should be true"); - - vm.warp(block.timestamp + 10 days - 1); - assertEq(multisig.canExecute(pid), true, "Should be true"); - - vm.warp(block.timestamp + 1); - assertEq(multisig.canExecute(pid), false, "Should be false"); - } - - function test_CanExecuteReturnsFalseIfExecuted() public { - // returns `false` if the proposal is already executed - - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); - - // Alice - multisig.approve(pid, false); - - // Bob - vm.startPrank(bob); - multisig.approve(pid, false); - - // Carol - vm.startPrank(carol); - multisig.approve(pid, false); - - assertEq(multisig.canExecute(pid), true, "Should be true"); - multisig.execute(pid); - - assertEq(multisig.canExecute(pid), false, "Should be false"); - } - - function test_CanExecuteReturnsTrueWhenAllGood() public { - // returns `true` if the proposal can be executed - - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); - assertEq(multisig.canExecute(pid), false, "Should be false"); - - // Alice - multisig.approve(pid, false); - assertEq(multisig.canExecute(pid), false, "Should be false"); - - // Bob - vm.startPrank(bob); - multisig.approve(pid, false); - assertEq(multisig.canExecute(pid), false, "Should be false"); - - // Carol - vm.startPrank(carol); - multisig.approve(pid, false); - - assertEq(multisig.canExecute(pid), true, "Should be true"); - } - - // EXECUTE - function testFuzz_ExecuteRevertsIfNotCreated(uint256 randomProposalId) public { // reverts if the proposal doesn't exist @@ -1835,883 +1322,1052 @@ contract MultisigTest is AragonTest { multisig.execute(randomProposalId); } - function test_ExecuteRevertsIfBelowMinApprovals() public { - // reverts if minApprovals is not met yet + function test_WhenCallingGetProposalBeingOpen() external givenTheProposalIsOpen { + // It should return the right values - (dao, optimisticPlugin, multisig,,,) = builder.withMinApprovals(2).build(); - - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); - - // Alice - multisig.approve(pid, false); - vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, pid)); - multisig.execute(pid); - - // Bob - vm.startPrank(bob); - multisig.approve(pid, false); - multisig.execute(pid); // ok + ( + bool executed, + uint16 approvals, + Multisig.ProposalParameters memory parameters, + bytes memory metadataURI, + IDAO.Action[] memory proposalActions, + OptimisticTokenVotingPlugin destinationPlugin + ) = multisig.getProposal(0); - vm.startPrank(alice); + // Check basic proposal state + assertEq(executed, false, "Should not be executed"); + assertEq(approvals, 0, "Should have no approvals"); - // More approvals required (4) - (dao, optimisticPlugin, multisig,,,) = builder.withMinApprovals(4).build(); + // Check parameters + assertEq(parameters.minApprovals, 3, "Should require 3 approvals"); + assertEq(parameters.snapshotBlock, block.number - 1 - 50, "Incorrect snapshot block"); + assertEq( + parameters.expirationDate, + block.timestamp + MULTISIG_PROPOSAL_EXPIRATION_PERIOD, + "Incorrect expiration date" + ); - pid = multisig.createProposal("", actions, optimisticPlugin, false); + // Check metadata and plugin + assertEq(metadataURI, "ipfs://pub-metadata", "Incorrect metadata URI"); + assertEq(address(destinationPlugin), address(optimisticPlugin), "Incorrect destination plugin"); + + // Verify actions + IDAO.Action[] memory actions = new IDAO.Action[](2); + actions[0].value = 0.25 ether; + actions[0].to = address(alice); + actions[0].data = hex""; + actions[1].value = 0.75 ether; + actions[1].to = address(dao); + actions[1].data = abi.encodeCall(DAO.setMetadata, "ipfs://new-metadata"); + + assertEq(proposalActions.length, actions.length, "Actions length should match"); + for (uint256 i = 0; i < actions.length; i++) { + assertEq(proposalActions[i].to, actions[i].to, "Action to should match"); + assertEq(proposalActions[i].value, actions[i].value, "Action value should match"); + assertEq(proposalActions[i].data, actions[i].data, "Action data should match"); + } + } - // Alice - vm.startPrank(alice); - multisig.approve(pid, false); - vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, pid)); - multisig.execute(pid); + function test_WhenCallingCanApproveOrApproveBeingOpen() external givenTheProposalIsOpen { + // It canApprove should return true (when listed on creation, self appointed now) + // It approve should work (when listed on creation, self appointed now) + // It approve should emit an event (when listed on creation, self appointed now) + assertEq(multisig.canApprove(0, alice), true, "Alice should be able to approve"); + vm.expectEmit(); + emit Approved(0, alice); + multisig.approve(0, false); + assertEq(multisig.hasApproved(0, alice), true, "Alice's approval should be recorded"); - // Bob + // It canApprove should return false (when listed on creation, appointing someone else now) + // It approve should revert (when listed on creation, appointing someone else now) + assertEq(multisig.canApprove(0, bob), false, "Bob should be able to approve"); vm.startPrank(bob); - multisig.approve(pid, false); - vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, pid)); - multisig.execute(pid); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, bob)); + multisig.approve(0, false); + assertEq(multisig.hasApproved(0, bob), false, "Bob's approval should not be recorded"); + + // It canApprove should return true (when currently appointed by a signer listed on creation) + // It approve should work (when currently appointed by a signer listed on creation) + // It approve should emit an event (when currently appointed by a signer listed on creation) + assertEq(multisig.canApprove(0, randomWallet), true, "RandomWallet should be able to approve"); + vm.startPrank(randomWallet); + vm.expectEmit(); + emit Approved(0, bob); + multisig.approve(0, false); + assertEq(multisig.hasApproved(0, bob), true, "RandomWallet's approval should be recorded"); + assertEq(multisig.hasApproved(0, randomWallet), true, "RandomWallet's approval should be recorded"); + + // It canApprove should return false (when unlisted on creation, unappointed now) + // It approve should revert (when unlisted on creation, unappointed now) + assertEq(multisig.canApprove(0, address(0x5555)), false, "Random wallet should not be able to approve"); + vm.startPrank(address(0x5555)); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, address(0x5555))); + multisig.approve(0, false); + + // Check approval count + (, uint16 approvals,,,,) = multisig.getProposal(0); + assertEq(approvals, 2, "Should have 2 approvals total"); + + // Try to approve again + vm.startPrank(randomWallet); + // Should not be able to approve again + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, randomWallet)); + multisig.approve(0, false); // Carol vm.startPrank(carol); - multisig.approve(pid, false); - vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, pid)); - multisig.execute(pid); - - // David - vm.startPrank(david); - multisig.approve(pid, false); - multisig.execute(pid); - } - - function test_ExecuteRevertsIfExpired() public { - // reverts if the proposal has expired - - // 1 - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); - - multisig.approve(pid, false); - vm.startPrank(bob); - multisig.approve(pid, false); - vm.startPrank(carol); - multisig.approve(pid, false); - assertEq(multisig.canExecute(pid), true, "Should be true"); + assertEq(multisig.canApprove(0, carol), true, "Carol should be able to approve"); + multisig.approve(0, false); - vm.warp(block.timestamp + 10 days); + // Should approve, pass but not execute + bool executed; + (executed, approvals,,,,) = multisig.getProposal(0); + assertEq(executed, false, "Should not have executed"); + assertEq(approvals, 3, "Should have 3 approvals total"); - vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, pid)); - multisig.execute(pid); + // David should approve + vm.startPrank(david); + assertEq(multisig.canApprove(0, david), true, "David should be able to approve"); + multisig.approve(0, false); - vm.warp(100 days); + // Should approve, pass but not execute + (executed, approvals,,,,) = multisig.getProposal(0); + assertEq(executed, false, "Should not have executed"); + assertEq(approvals, 4, "Should have 4 approvals total"); + } - // 2 - pid = multisig.createProposal("", actions, optimisticPlugin, false); + function test_WhenCallingApproveWithTryExecutionAndAlmostPassedBeingOpen() external givenTheProposalIsOpen { + // It approve should also execute the proposal + // It approve should emit an Executed event + // It approve recreates the proposal on the destination plugin + // It The parameters of the recreated proposal match those of the approved one + // It A ProposalCreated event is emitted on the destination plugin - vm.startPrank(alice); - multisig.approve(pid, false); - vm.startPrank(bob); - multisig.approve(pid, false); - vm.startPrank(carol); - multisig.approve(pid, false); - assertEq(multisig.canExecute(pid), true, "Should be true"); - - vm.warp(block.timestamp + 10 days); + bool executed; + uint16 approvals; - vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, pid)); - multisig.execute(pid); + (executed, approvals,,,,) = multisig.getProposal(0); + assertEq(executed, false, "Should not have executed"); + assertEq(approvals, 0, "Should have 0 approvals total"); - vm.startPrank(alice); - } + // Approve with tryExecute on + multisig.approve(0, true); + // Should not be able to approve again even with tryExecution + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, alice)); + multisig.approve(0, true); - function test_ExecuteRevertsWhenAlreadyExecuted() public { - // executes if the minimum approval is met when multisig with the `tryExecution` option + (executed, approvals,,,,) = multisig.getProposal(0); + assertEq(executed, false, "Should not have executed"); + assertEq(approvals, 1, "Should have 1 approvals total"); - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); + vm.startPrank(randomWallet); + multisig.approve(0, true); + assertEq(multisig.hasApproved(0, bob), true, "RandomWallet's approval should be recorded for Bob"); + assertEq(multisig.hasApproved(0, randomWallet), true, "RandomWallet's approval should be recorded"); - // Alice - multisig.approve(pid, false); + (executed, approvals,,,,) = multisig.getProposal(0); + assertEq(executed, false, "Should not have executed"); + assertEq(approvals, 2, "Should have 2 approvals total"); - // Bob - vm.startPrank(bob); - multisig.approve(pid, false); + vm.startPrank(randomWallet); + // Should not be able to approve again even with tryExecution + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, randomWallet)); + multisig.approve(0, true); // Carol vm.startPrank(carol); - multisig.approve(pid, false); + assertEq(multisig.canApprove(0, carol), true, "Carol should be able to approve"); + multisig.approve(0, false); - assertEq(multisig.canExecute(pid), true, "Should be true"); - multisig.execute(pid); - - vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, pid)); - multisig.execute(pid); - } + // Should approve, pass but not execute (yet) + (executed, approvals,,,,) = multisig.getProposal(0); + assertEq(executed, false, "Should not have executed"); + assertEq(approvals, 3, "Should have 3 approvals total"); - function test_ExecuteEmitsEvents() public { - // emits the `ProposalExecuted` and `ProposalCreated` events + assertEq(multisig.canExecute(0), true, "Should be already executable"); - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); - - // Alice - multisig.approve(pid, false); - - // Bob - vm.startPrank(bob); - multisig.approve(pid, false); - - // Carol - vm.startPrank(carol); - multisig.approve(pid, false); - - // event - vm.expectEmit(); - emit Executed(pid); - uint256 targetPid = uint256(block.timestamp) << 128 | uint256(block.timestamp + 10 days) << 64; + // David should approve and trigger auto execution + vm.startPrank(david); + assertEq(multisig.canApprove(0, david), true, "David should be able to approve"); + + // It execute recreates the proposal on the destination plugin + uint256 targetPid = (block.timestamp << 128) | ((block.timestamp + DESTINATION_PROPOSAL_DURATION) << 64); + IDAO.Action[] memory actions = new IDAO.Action[](2); + actions[0].value = 0.25 ether; + actions[0].to = address(alice); + actions[0].data = hex""; + actions[1].value = 0.75 ether; + actions[1].to = address(dao); + actions[1].data = abi.encodeCall(DAO.setMetadata, "ipfs://new-metadata"); vm.expectEmit(); emit ProposalCreated( - targetPid, address(multisig), uint64(block.timestamp), uint64(block.timestamp) + 10 days, "", actions, 0 + targetPid, + address(multisig), + uint64(block.timestamp), + uint64(block.timestamp + DESTINATION_PROPOSAL_DURATION), + "ipfs://pub-metadata", + actions, + 0 ); - multisig.execute(pid); - - // 2 - (dao, optimisticPlugin, multisig,,,) = builder.withDuration(50 days).build(); - - vm.warp(20 days); - actions = new IDAO.Action[](1); - actions[0].value = 1 ether; - actions[0].to = address(bob); - actions[0].data = hex"00112233"; - pid = multisig.createProposal("ipfs://", actions, optimisticPlugin, false); + multisig.approve(0, true); - // Alice - vm.startPrank(alice); - multisig.approve(pid, false); - - // Bob - vm.startPrank(bob); - multisig.approve(pid, false); + (executed, approvals,,,,) = multisig.getProposal(0); + assertEq(executed, true, "Should have executed"); + assertEq(approvals, 4, "Should have 4 approvals total"); + } - // Carol - vm.startPrank(carol); - multisig.approve(pid, false); + function testFuzz_CanApproveReturnsfFalseIfNotListed(address randomWallet) public { + // returns `false` if the approver is not listed - // events - vm.expectEmit(); - emit Executed(pid); - targetPid = (uint256(block.timestamp) << 128 | uint256(block.timestamp + 50 days) << 64); - vm.expectEmit(); - emit ProposalCreated( - targetPid, address(multisig), uint64(block.timestamp), 20 days + 50 days, "ipfs://", actions, 0 - ); - multisig.execute(pid); - } + { + // Deploy a new multisig instance (more efficient than the builder for fuzz testing) + Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ + onlyListed: true, + minApprovals: 1, + destinationProposalDuration: 4 days, + signerList: signerList, + proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD + }); + address[] memory signers = new address[](1); + signers[0] = alice; - function test_ExecutesWhenApprovingWithTryExecutionAndEnoughApprovals() public { - // executes if the minimum approval is met when multisig with the `tryExecution` option + multisig = Multisig( + createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings))) + ); + vm.roll(block.number + 1); + } IDAO.Action[] memory actions = new IDAO.Action[](0); uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); - (bool executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); - // Alice - multisig.approve(pid, true); - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); - - // Bob - vm.startPrank(bob); - multisig.approve(pid, true); - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); - - // Carol - vm.startPrank(carol); - multisig.approve(pid, true); + // ko + if (randomWallet != alice && randomWallet != bob && randomWallet != carol && randomWallet != david) { + assertEq(multisig.canApprove(pid, randomWallet), false, "Should be false"); + } - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, true, "Should be executed"); + // static ok + assertEq(multisig.canApprove(pid, alice), true, "Should be true"); } - function test_ExecuteEmitsWhenAutoExecutedFromApprove() public { - // emits the `Approved`, `ProposalExecuted`, and `ProposalCreated` events if execute is called inside the `approve` method + function testFuzz_ApproveRevertsIfNotListed(address randomSigner) public { + // Reverts if the signer is not listed + + builder = new DaoBuilder(); + (,, multisig,,,,,) = builder.withMultisigMember(alice).withMinApprovals(1).build(); IDAO.Action[] memory actions = new IDAO.Action[](0); uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); - // Alice - multisig.approve(pid, true); + if (randomSigner == alice) { + return; + } - // Bob - vm.startPrank(bob); + vm.startPrank(randomSigner); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, pid, randomSigner)); + multisig.approve(pid, false); + + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, pid, randomSigner)); multisig.approve(pid, true); + } - // Carol - vm.startPrank(carol); - vm.expectEmit(); - emit Approved(pid, carol); - vm.expectEmit(); - emit Executed(pid); + function test_WhenCallingHasApprovedBeingOpen() external givenTheProposalIsOpen { + // It hasApproved should return false until approved - uint256 targetPid = (uint256(block.timestamp) << 128 | uint256(block.timestamp + 10 days) << 64); - vm.expectEmit(); - emit ProposalCreated( - targetPid, address(multisig), uint64(block.timestamp), uint64(block.timestamp) + 10 days, "", actions, 0 - ); - multisig.approve(pid, true); + assertEq(multisig.hasApproved(0, alice), false, "Alice should not have approved"); + assertEq(multisig.hasApproved(0, bob), false, "Bob should not have approved"); + assertEq(multisig.hasApproved(0, carol), false, "Carol should not have approved"); + } - // 2 - vm.warp(5 days); - actions = new IDAO.Action[](1); - actions[0].value = 1 ether; - actions[0].to = address(bob); - actions[0].data = hex"00112233"; - pid = multisig.createProposal("ipfs://", actions, optimisticPlugin, false); + function test_WhenCallingCanExecuteOrExecuteBeingOpen() external givenTheProposalIsOpen { + // It canExecute should return false (when listed on creation, self appointed now) + // It canExecute should return false (when listed on creation, appointing someone else now) + // It canExecute should return false (when currently appointed by a signer listed on creation) + // It canExecute should return false (when unlisted on creation, unappointed now) - // Alice + // vm.startPrank(alice); + assertEq(multisig.canExecute(0), false, "Should not be executable with only 1 approval"); + vm.startPrank(bob); + assertEq(multisig.canExecute(0), false, "Should not be executable with only 1 approval"); + vm.startPrank(randomWallet); + assertEq(multisig.canExecute(0), false, "Should not be executable with only 1 approval"); + vm.startPrank(address(0x5555)); + assertEq(multisig.canExecute(0), false, "Should not be executable with only 1 approval"); + + // It execute should revert (when listed on creation, self appointed now) + // It execute should revert (when listed on creation, appointing someone else now) + // It execute should revert (when currently appointed by a signer listed on creation) + // It execute should revert (when unlisted on creation, unappointed now) vm.startPrank(alice); - multisig.approve(pid, true); - - // Bob + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, 0)); + multisig.execute(0); vm.startPrank(bob); - multisig.approve(pid, true); - - // Carol - vm.startPrank(carol); - vm.expectEmit(); - emit Approved(pid, carol); - vm.expectEmit(); - emit Executed(pid); - - targetPid = (uint256(5 days) << 128 | uint256(5 days + 10 days) << 64) + 1; - vm.expectEmit(); - emit ProposalCreated( - targetPid, // foreign pid - address(multisig), - uint64(block.timestamp), - uint64(block.timestamp) + 10 days, - "ipfs://", - actions, - 0 - ); - multisig.approve(pid, true); - - // 3 - (dao, optimisticPlugin, multisig,,,) = builder.withDuration(50 days).build(); - - vm.warp(7 days); - actions = new IDAO.Action[](1); - actions[0].value = 5 ether; - actions[0].to = address(carol); - actions[0].data = hex"44556677"; - pid = multisig.createProposal("ipfs://...", actions, optimisticPlugin, false); + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, 0)); + multisig.execute(0); + vm.startPrank(randomWallet); + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, 0)); + multisig.execute(0); + vm.startPrank(address(0x5555)); + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, 0)); + multisig.execute(0); - // Alice + // Approvals vm.startPrank(alice); - multisig.approve(pid, true); + multisig.approve(0, false); + vm.startPrank(randomWallet); + multisig.approve(0, false); - // Bob - vm.startPrank(bob); - multisig.approve(pid, true); + assertEq(multisig.canExecute(0), false, "Should not be executable with only 2 approvals"); + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, 0)); + multisig.execute(0); - // Carol + // Add final approval vm.startPrank(carol); - vm.expectEmit(); - emit Approved(pid, carol); - vm.expectEmit(); - emit Executed(pid); + multisig.approve(0, false); - targetPid = (uint256(7 days) << 128 | uint256(7 days + 50 days) << 64); - vm.expectEmit(); - emit ProposalCreated(targetPid, address(multisig), 7 days, 7 days + 50 days, "ipfs://...", actions, 0); - multisig.approve(pid, true); - } + assertEq(multisig.canExecute(0), true, "Should be executable with 3 approvals"); + multisig.execute(0); - function test_ExecutesWithEnoughApprovalsOnTime() public { - // executes if the minimum approval is met + // Verify execution + (bool executed,,,,,) = multisig.getProposal(0); + assertEq(executed, true, "Should now be executed"); + } - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); + modifier givenTheProposalWasApprovedByTheAddress() { + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 1)); - // Alice - multisig.approve(pid, false); - (bool executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + // Alice: listed on creation and self appointed - // Bob + // Bob: listed on creation, appointing someone else now vm.startPrank(bob); - multisig.approve(pid, false); - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + encryptionRegistry.appointWallet(randomWallet); - // Carol - vm.startPrank(carol); - multisig.approve(pid, false); - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + // Random Wallet: appointed by a listed signer on creation + // 0x1234: unlisted and unappointed on creation - multisig.execute(pid); - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, true, "Should be executed"); + vm.deal(address(dao), 1 ether); - // 2 - actions = new IDAO.Action[](1); - actions[0].value = 1 ether; - actions[0].to = address(bob); - actions[0].data = hex"00112233"; - pid = multisig.createProposal("ipfs://", actions, optimisticPlugin, false); + // Create proposal 0 + IDAO.Action[] memory actions = new IDAO.Action[](1); + actions[0].value = 0.3 ether; + actions[0].to = address(carol); + actions[0].data = hex""; + uint256 pid = multisig.createProposal("ipfs://more-metadata", actions, optimisticPlugin, false); - // Alice + // Remove (later) + vm.roll(block.number + 50); + address[] memory addrs = new address[](2); + addrs[0] = alice; + addrs[1] = bob; vm.startPrank(alice); - multisig.approve(pid, false); - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + signerList.removeSigners(addrs); - // Bob - vm.startPrank(bob); + // Alice approves multisig.approve(pid, false); - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); - // Carol - vm.startPrank(carol); - multisig.approve(pid, false); - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); - - multisig.execute(pid); - - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, true, "Should be executed"); + _; } - function test_ExecuteWhenPassedAndCalledByAnyone() public { - // executes if the minimum approval is met and can be called by an unlisted accounts + function test_WhenCallingGetProposalBeingApproved() external givenTheProposalWasApprovedByTheAddress { + // It should return the right values - IDAO.Action[] memory actions = new IDAO.Action[](0); - uint256 pid = multisig.createProposal("", actions, optimisticPlugin, false); + ( + bool executed, + uint16 approvals, + Multisig.ProposalParameters memory parameters, + bytes memory metadataURI, + IDAO.Action[] memory proposalActions, + OptimisticTokenVotingPlugin destinationPlugin + ) = multisig.getProposal(0); - // Alice - multisig.approve(pid, false); - (bool executed,,,,,) = multisig.getProposal(pid); assertEq(executed, false, "Should not be executed"); + assertEq(approvals, 1, "Should have 1 approval"); + assertEq(parameters.minApprovals, 3, "Should require 3 approvals"); + assertEq(parameters.snapshotBlock, block.number - 1 - 50, "Incorrect snapshot block"); // -51 due to vm.roll(block.number + 50) + assertEq( + parameters.expirationDate, block.timestamp + MULTISIG_PROPOSAL_EXPIRATION_PERIOD, "Incorrect expiration" + ); + assertEq(metadataURI, "ipfs://more-metadata", "Incorrect metadata URI"); + assertEq(address(destinationPlugin), address(optimisticPlugin), "Incorrect destination plugin"); - // Bob - vm.startPrank(bob); - multisig.approve(pid, false); - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + // Verify actions + IDAO.Action[] memory actions = new IDAO.Action[](1); + actions[0].value = 0.3 ether; + actions[0].to = address(carol); + actions[0].data = hex""; - // Carol - vm.startPrank(carol); - multisig.approve(pid, false); - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + assertEq(proposalActions.length, actions.length, "Actions length should match"); + for (uint256 i = 0; i < actions.length; i++) { + assertEq(proposalActions[i].to, actions[i].to, "Action to should match"); + assertEq(proposalActions[i].value, actions[i].value, "Action value should match"); + assertEq(proposalActions[i].data, actions[i].data, "Action data should match"); + } + } + function test_WhenCallingCanApproveOrApproveBeingApproved() external givenTheProposalWasApprovedByTheAddress { + // Approve without executing vm.startPrank(randomWallet); - multisig.execute(pid); - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, true, "Should be executed"); + multisig.approve(0, false); - // 2 - vm.startPrank(alice); + // It canApprove should return false (when listed on creation, self appointed now) + // It canApprove should return false (when listed on creation, appointing someone else now) + // It canApprove should return false (when currently appointed by a signer listed on creation) + // It canApprove should return false (when unlisted on creation, unappointed now) - actions = new IDAO.Action[](1); - actions[0].value = 1 ether; - actions[0].to = address(bob); - actions[0].data = hex"00112233"; - pid = multisig.createProposal("ipfs://", actions, optimisticPlugin, false); + // It approve should revert (when listed on creation, self appointed now) + // It approve should revert (when listed on creation, appointing someone else now) + // It approve should revert (when currently appointed by a signer listed on creation) + // It approve should revert (when unlisted on creation, unappointed now) - // Alice - multisig.approve(pid, false); - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + // When listed on creation, self appointed now + vm.startPrank(alice); + assertEq(multisig.canApprove(0, alice), false, "Alice should not be able to approve"); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, alice)); + multisig.approve(0, false); - // Bob + // When listed on creation, appointing someone else now + assertEq(multisig.canApprove(0, bob), false, "Bob should not be able to approve"); vm.startPrank(bob); - multisig.approve(pid, false); - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); - - // Carol - vm.startPrank(carol); - multisig.approve(pid, false); - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, false, "Should not be executed"); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, bob)); + multisig.approve(0, false); + // When currently appointed by a signer listed on creation + // RandomWallet should not be able to approve again + assertEq(multisig.canApprove(0, randomWallet), false, "Random wallet should not be able to approve again"); vm.startPrank(randomWallet); - multisig.execute(pid); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, randomWallet)); + multisig.approve(0, false); - (executed,,,,,) = multisig.getProposal(pid); - assertEq(executed, true, "Should be executed"); + // When unlisted on creation, unappointed now + assertEq(multisig.canApprove(0, address(0x1234)), false, "Unlisted address should not be able to approve"); + vm.startPrank(address(0x1234)); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, address(0x1234))); + multisig.approve(0, false); } - function test_GetProposalReturnsTheRightValues() public { - // Get proposal returns the right values - - bool executed; - uint16 approvals; - Multisig.ProposalParameters memory parameters; - bytes memory metadataURI; - IDAO.Action[] memory actions; - OptimisticTokenVotingPlugin destPlugin; + function test_WhenCallingHasApprovedBeingApproved() external givenTheProposalWasApprovedByTheAddress { + // It hasApproved should return false until approved - vm.warp(5 days); + assertEq(multisig.hasApproved(0, alice), true, "Alice should have approved"); + assertEq(multisig.hasApproved(0, bob), false, "Bob should not have approved"); + assertEq(multisig.hasApproved(0, carol), false, "Carol should not have approved"); + assertEq(multisig.hasApproved(0, david), false, "David should not have approved"); + assertEq(multisig.hasApproved(0, randomWallet), false, "Random wallet should not have approved"); - IDAO.Action[] memory createActions = new IDAO.Action[](3); - createActions[0].to = alice; - createActions[0].value = 1 ether; - createActions[0].data = hex"001122334455"; - createActions[1].to = bob; - createActions[1].value = 2 ether; - createActions[1].data = hex"112233445566"; - createActions[2].to = carol; - createActions[2].value = 3 ether; - createActions[2].data = hex"223344556677"; + vm.startPrank(randomWallet); // Appointed + multisig.approve(0, false); + assertEq(multisig.hasApproved(0, bob), true, "Bob should have approved"); - uint256 pid = multisig.createProposal("ipfs://metadata", createActions, optimisticPlugin, false); - assertEq(pid, 0, "PID should be 0"); + vm.startPrank(carol); + multisig.approve(0, false); + assertEq(multisig.hasApproved(0, carol), true, "Carol should have approved"); - // Check round 1 - (executed, approvals, parameters, metadataURI, actions, destPlugin) = multisig.getProposal(pid); + vm.startPrank(david); + multisig.approve(0, false); + assertEq(multisig.hasApproved(0, david), true, "Bob should have approved"); + } - assertEq(executed, false, "Should not be executed"); - assertEq(approvals, 0, "Should be 0"); + function test_WhenCallingCanExecuteOrExecuteBeingApproved() external givenTheProposalWasApprovedByTheAddress { + // It execute should revert (when listed on creation, self appointed now) + // It execute should revert (when currently appointed by a signer listed on creation) - assertEq(parameters.minApprovals, 3, "Incorrect minApprovals"); - assertEq(parameters.snapshotBlock, block.number - 1, "Incorrect snapshotBlock"); - assertEq(parameters.expirationDate, block.timestamp + 10 days, "Incorrect expirationDate"); + // It canExecute should return false (when listed on creation, self appointed now) + // It canExecute should return false (when currently appointed by a signer listed on creation) - assertEq(actions.length, 3, "Should be 3"); + // It canExecute should return false with insufficient approvals + // vm.startPrank(alice); + assertEq(multisig.canExecute(0), false, "Should not be executable with only 1 approval"); + vm.startPrank(randomWallet); + assertEq(multisig.canExecute(0), false, "Should not be executable with only 1 approval"); - assertEq(actions[0].to, alice, "Incorrect to"); - assertEq(actions[0].value, 1 ether, "Incorrect value"); - assertEq(actions[0].data, hex"001122334455", "Incorrect data"); - assertEq(actions[1].to, bob, "Incorrect to"); - assertEq(actions[1].value, 2 ether, "Incorrect value"); - assertEq(actions[1].data, hex"112233445566", "Incorrect data"); - assertEq(actions[2].to, carol, "Incorrect to"); - assertEq(actions[2].value, 3 ether, "Incorrect value"); - assertEq(actions[2].data, hex"223344556677", "Incorrect data"); + // It execute should revert with insufficient approvals + vm.startPrank(alice); + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, 0)); + multisig.execute(0); + vm.startPrank(randomWallet); + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, 0)); + multisig.execute(0); - assertEq(metadataURI, "ipfs://metadata", "Incorrect metadata URI"); - assertEq(address(destPlugin), address(optimisticPlugin), "Incorrect destPlugin"); + // Add remaining approvals + vm.startPrank(randomWallet); + multisig.approve(0, false); - // Approve - multisig.approve(pid, false); + // It canExecute should return false with insufficient approvals + assertEq(multisig.canExecute(0), false, "Should not be executable with 2 approvals"); - // Check round 2 - (executed, approvals, parameters, metadataURI, actions, destPlugin) = multisig.getProposal(pid); + vm.startPrank(carol); + multisig.approve(0, false); - assertEq(executed, false, "Should not be executed"); - assertEq(approvals, 1, "Should be 1"); + // It canExecute should return true with sufficient approvals + assertEq(multisig.canExecute(0), true, "Should be executable with 3 approvals"); - assertEq(parameters.minApprovals, 3, "Incorrect minApprovals"); - assertEq(parameters.snapshotBlock, block.number - 1, "Incorrect snapshotBlock"); - assertEq(parameters.expirationDate, block.timestamp + 10 days, "Incorrect expirationDate"); + // It execute should work with sufficient approvals + vm.expectEmit(); + emit Executed(0); + multisig.execute(0); - assertEq(actions.length, 3, "Should be 3"); + // Verify execution + (bool executed,,,,,) = multisig.getProposal(0); + assertEq(executed, true, "Should be executed"); + } - assertEq(actions[0].to, alice, "Incorrect to"); - assertEq(actions[0].value, 1 ether, "Incorrect value"); - assertEq(actions[0].data, hex"001122334455", "Incorrect data"); - assertEq(actions[1].to, bob, "Incorrect to"); - assertEq(actions[1].value, 2 ether, "Incorrect value"); - assertEq(actions[1].data, hex"112233445566", "Incorrect data"); - assertEq(actions[2].to, carol, "Incorrect to"); - assertEq(actions[2].value, 3 ether, "Incorrect value"); - assertEq(actions[2].data, hex"223344556677", "Incorrect data"); + modifier givenTheProposalPassed() { + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 1)); - assertEq(metadataURI, "ipfs://metadata", "Incorrect metadata URI"); - assertEq(address(destPlugin), address(optimisticPlugin), "Incorrect destPlugin"); + // Alice: listed on creation and self appointed - // Approve + // Bob: listed on creation, appointing someone else now vm.startPrank(bob); - multisig.approve(pid, false); - vm.startPrank(carol); - multisig.approve(pid, false); + encryptionRegistry.appointWallet(randomWallet); - // Check round 3 - (executed, approvals, parameters, metadataURI, actions, destPlugin) = multisig.getProposal(pid); + // Random Wallet: appointed by a listed signer on creation + // 0x1234: unlisted and unappointed on creation - assertEq(executed, false, "Should not be executed"); - assertEq(approvals, 3, "Should be 3"); + vm.deal(address(dao), 1 ether); - assertEq(parameters.minApprovals, 3, "Incorrect minApprovals"); - assertEq(parameters.snapshotBlock, block.number - 1, "Incorrect snapshotBlock"); - assertEq(parameters.expirationDate, block.timestamp + 10 days, "Incorrect expirationDate"); + // Create proposal 0 + IDAO.Action[] memory actions = new IDAO.Action[](1); + actions[0].value = 0.65 ether; + actions[0].to = address(david); + actions[0].data = hex""; + uint256 pid = multisig.createProposal("ipfs://proposal-metadata-here", actions, optimisticPlugin, false); + + // Remove (later) + vm.roll(block.number + 50); + address[] memory addrs = new address[](2); + addrs[0] = alice; + addrs[1] = bob; + vm.startPrank(alice); + signerList.removeSigners(addrs); - assertEq(actions.length, 3, "Should be 3"); + multisig.approve(pid, false); - assertEq(actions[0].to, alice, "Incorrect to"); - assertEq(actions[0].value, 1 ether, "Incorrect value"); - assertEq(actions[0].data, hex"001122334455", "Incorrect data"); - assertEq(actions[1].to, bob, "Incorrect to"); - assertEq(actions[1].value, 2 ether, "Incorrect value"); - assertEq(actions[1].data, hex"112233445566", "Incorrect data"); - assertEq(actions[2].to, carol, "Incorrect to"); - assertEq(actions[2].value, 3 ether, "Incorrect value"); - assertEq(actions[2].data, hex"223344556677", "Incorrect data"); + vm.startPrank(randomWallet); + multisig.approve(pid, false); - assertEq(metadataURI, "ipfs://metadata", "Incorrect metadata URI"); - assertEq(address(destPlugin), address(optimisticPlugin), "Incorrect destPlugin"); + vm.startPrank(carol); + multisig.approve(pid, false); - // Execute vm.startPrank(alice); - multisig.execute(pid); - // Check round 4 - (executed, approvals, parameters, metadataURI, actions, destPlugin) = multisig.getProposal(pid); + _; + } - assertEq(executed, true, "Should be executed"); - assertEq(approvals, 3, "Should be 3"); + function test_WhenCallingGetProposalBeingPassed() external givenTheProposalPassed { + // It should return the right values + ( + bool executed, + uint16 approvals, + Multisig.ProposalParameters memory parameters, + bytes memory metadataURI, + IDAO.Action[] memory proposalActions, + OptimisticTokenVotingPlugin destinationPlugin + ) = multisig.getProposal(0); - assertEq(parameters.minApprovals, 3, "Incorrect minApprovals"); - assertEq(parameters.snapshotBlock, block.number - 1, "Incorrect snapshotBlock"); - assertEq(parameters.expirationDate, block.timestamp + 10 days, "Incorrect expirationDate"); + assertEq(executed, false, "Should not be executed yet"); + assertEq(approvals, 3, "Should have 3 approvals"); + assertEq(parameters.minApprovals, 3, "Should require 3 approvals"); + assertEq(parameters.snapshotBlock, block.number - 1 - 50, "Incorrect snapshot block"); // -51 due to vm.roll(block.number + 50) + assertEq( + parameters.expirationDate, block.timestamp + MULTISIG_PROPOSAL_EXPIRATION_PERIOD, "Incorrect expiration" + ); + assertEq(metadataURI, "ipfs://proposal-metadata-here", "Incorrect metadata URI"); + assertEq(address(destinationPlugin), address(optimisticPlugin), "Incorrect destination plugin"); + + // Verify actions + IDAO.Action[] memory actions = new IDAO.Action[](1); + actions[0].value = 0.65 ether; + actions[0].to = address(david); + actions[0].data = hex""; + + assertEq(proposalActions.length, actions.length, "Actions length should match"); + for (uint256 i = 0; i < actions.length; i++) { + assertEq(proposalActions[i].to, actions[i].to, "Action to should match"); + assertEq(proposalActions[i].value, actions[i].value, "Action value should match"); + assertEq(proposalActions[i].data, actions[i].data, "Action data should match"); + } + } - assertEq(actions.length, 3, "Should be 3"); + function test_WhenCallingCanApproveOrApproveBeingPassed() external givenTheProposalPassed { + // It canApprove should return false (when listed on creation, self appointed now) + // It canApprove should return false (when listed on creation, appointing someone else now) + // It canApprove should return false (when currently appointed by a signer listed on creation) + // It canApprove should return false (when unlisted on creation, unappointed now) - assertEq(actions[0].to, alice, "Incorrect to"); - assertEq(actions[0].value, 1 ether, "Incorrect value"); - assertEq(actions[0].data, hex"001122334455", "Incorrect data"); - assertEq(actions[1].to, bob, "Incorrect to"); - assertEq(actions[1].value, 2 ether, "Incorrect value"); - assertEq(actions[1].data, hex"112233445566", "Incorrect data"); - assertEq(actions[2].to, carol, "Incorrect to"); - assertEq(actions[2].value, 3 ether, "Incorrect value"); - assertEq(actions[2].data, hex"223344556677", "Incorrect data"); + // It approve should revert (when listed on creation, self appointed now) + // It approve should revert (when listed on creation, appointing someone else now) + // It approve should revert (when currently appointed by a signer listed on creation) + // It approve should revert (when unlisted on creation, unappointed now) - assertEq(metadataURI, "ipfs://metadata", "Incorrect metadata URI"); - assertEq(address(destPlugin), address(optimisticPlugin), "Incorrect destPlugin"); + // When listed on creation, self appointed now + assertEq(multisig.canApprove(0, alice), false, "Alice should not be able to approve"); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, alice)); + multisig.approve(0, false); - // New multisig, new settings - vm.startPrank(alice); + // When listed on creation, appointing someone else now + assertEq(multisig.canApprove(0, bob), false, "Bob should not be able to approve"); + vm.startPrank(bob); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, bob)); + multisig.approve(0, false); - // Deploy new instances - (dao, optimisticPlugin, multisig,,,) = builder.withMinApprovals(2).build(); + // When currently appointed by a signer listed on creation + assertEq(multisig.canApprove(0, randomWallet), false, "Random wallet should not be able to approve"); + vm.startPrank(randomWallet); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, randomWallet)); + multisig.approve(0, false); - createActions = new IDAO.Action[](2); - createActions[1].to = alice; - createActions[1].value = 1 ether; - createActions[1].data = hex"001122334455"; - createActions[0].to = carol; - createActions[0].value = 3 ether; - createActions[0].data = hex"223344556677"; + // When unlisted on creation, unappointed now + assertEq(multisig.canApprove(0, address(0x1234)), false, "Unlisted address should not be able to approve"); + vm.startPrank(address(0x1234)); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, address(0x1234))); + multisig.approve(0, false); + } - vm.warp(15 days); + function test_WhenCallingHasApprovedBeingPassed() external givenTheProposalPassed { + // It hasApproved should return false until approved - pid = multisig.createProposal("ipfs://different-metadata", createActions, optimisticPlugin, true); - assertEq(pid, 0, "PID should be 0"); + assertEq(multisig.hasApproved(0, alice), true, "Alice should show as approved"); + assertEq(multisig.hasApproved(0, bob), true, "Bob should show as approved"); + assertEq(multisig.hasApproved(0, randomWallet), true, "Random wallet should show as approved"); + assertEq(multisig.hasApproved(0, carol), true, "Carol should show as approved"); + assertEq(multisig.hasApproved(0, david), false, "David should not show as approved"); + } - // Check round 1 - (executed, approvals, parameters, metadataURI, actions, destPlugin) = multisig.getProposal(pid); + function test_WhenCallingCanExecuteOrExecuteBeingPassed() external givenTheProposalPassed { + // It execute recreates the proposal on the destination plugin + // It The parameters of the recreated proposal match those of the executed one + // It The proposal duration on the destination plugin matches the multisig settings + // It A ProposalCreated event is emitted on the destination plugin - assertEq(executed, false, "Should not be executed"); - assertEq(approvals, 1, "Should be 1"); + // It canExecute should return true, always + // vm.startPrank(alice); + assertEq(multisig.canExecute(0), true, "Should be executable"); + vm.startPrank(randomWallet); + assertEq(multisig.canExecute(0), true, "Should be executable"); + vm.startPrank(carol); + assertEq(multisig.canExecute(0), true, "Should be executable"); + vm.startPrank(address(0x5555)); + assertEq(multisig.canExecute(0), true, "Should be executable"); - assertEq(parameters.minApprovals, 2, "Incorrect minApprovals"); - assertEq(parameters.snapshotBlock, block.number - 1, "Incorrect snapshotBlock"); - assertEq(parameters.expirationDate, block.timestamp + 10 days, "Incorrect expirationDate"); + // It execute should work, when called by anyone + vm.expectEmit(); + emit Executed(0); + + // It execute should emit an event, when called by anyone + IDAO.Action[] memory actions = new IDAO.Action[](1); + actions[0].value = 0.65 ether; + actions[0].to = address(david); + actions[0].data = hex""; - assertEq(actions.length, 2, "Should be 2"); + // It execute recreates the proposal on the destination plugin + uint256 targetPid = (block.timestamp << 128) | ((block.timestamp + DESTINATION_PROPOSAL_DURATION) << 64); + vm.expectEmit(); + emit ProposalCreated( + targetPid, + address(multisig), + uint64(block.timestamp), + uint64(block.timestamp + DESTINATION_PROPOSAL_DURATION), + "ipfs://proposal-metadata-here", + actions, + 0 + ); - assertEq(actions[1].to, alice, "Incorrect to"); - assertEq(actions[1].value, 1 ether, "Incorrect value"); - assertEq(actions[1].data, hex"001122334455", "Incorrect data"); - assertEq(actions[0].to, carol, "Incorrect to"); - assertEq(actions[0].value, 3 ether, "Incorrect value"); - assertEq(actions[0].data, hex"223344556677", "Incorrect data"); + multisig.execute(0); - assertEq(metadataURI, "ipfs://different-metadata", "Incorrect metadata URI"); - assertEq(address(destPlugin), address(optimisticPlugin), "Incorrect destPlugin"); + // Verify execution + (bool executed,,,,,) = multisig.getProposal(0); + assertEq(executed, true, "Should be executed"); - // Approve + // Verify proposal recreation in destination plugin + ( + bool open, + bool destExecuted, + , + uint256 vetoTally, + bytes memory metadataUri, + IDAO.Action[] memory destActions, + uint256 allowFailureMap + ) = optimisticPlugin.getProposal(targetPid); + + assertEq(open, true, "Destination proposal should be open"); + assertEq(destExecuted, false, "Destination proposal should not be executed"); + assertEq(vetoTally, 0, "Veto tally should be 0"); + assertEq(metadataUri, "ipfs://proposal-metadata-here", "Metadata URI should match"); + assertEq(destActions.length, actions.length, "Actions should match"); + assertEq(allowFailureMap, 0, "Allow failure map should be 0"); + } + + function test_GivenTaikoL1IsIncompatible() external givenTheProposalPassed { + // Recreate with L1 incompatible + (dao, optimisticPlugin, multisig,,, signerList, encryptionRegistry,) = builder.withIncompatibleTaikoL1().build(); + + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 1)); + + // Alice: listed on creation and self appointed + // Bob: listed on creation, appointing someone else now vm.startPrank(bob); - multisig.approve(pid, false); + encryptionRegistry.appointWallet(randomWallet); + // Random Wallet: appointed by a listed signer on creation + // 0x1234: unlisted and unappointed on creation - // Check round 2 - (executed, approvals, parameters, metadataURI, actions, destPlugin) = multisig.getProposal(pid); + vm.deal(address(dao), 1 ether); - assertEq(executed, false, "Should not be executed"); - assertEq(approvals, 2, "Should be 2"); + // Create proposal 0 + IDAO.Action[] memory actions = new IDAO.Action[](1); + actions[0].value = 0.65 ether; + actions[0].to = address(david); + actions[0].data = hex""; + uint256 pid = multisig.createProposal("ipfs://proposal-metadata-here", actions, optimisticPlugin, false); - assertEq(parameters.minApprovals, 2, "Incorrect minApprovals"); - assertEq(parameters.snapshotBlock, block.number - 1, "Incorrect snapshotBlock"); - assertEq(parameters.expirationDate, block.timestamp + 10 days, "Incorrect expirationDate"); + vm.startPrank(alice); + multisig.approve(pid, false); + vm.startPrank(randomWallet); + multisig.approve(pid, false); + vm.startPrank(carol); + multisig.approve(pid, false); + + vm.startPrank(alice); - assertEq(actions.length, 2, "Should be 2"); + // It executes successfully, regardless + vm.expectEmit(); + emit Executed(0); - assertEq(actions[1].to, alice, "Incorrect to"); - assertEq(actions[1].value, 1 ether, "Incorrect value"); - assertEq(actions[1].data, hex"001122334455", "Incorrect data"); - assertEq(actions[0].to, carol, "Incorrect to"); - assertEq(actions[0].value, 3 ether, "Incorrect value"); - assertEq(actions[0].data, hex"223344556677", "Incorrect data"); + vm.expectEmit(); + emit ProposalCreated( + ((uint256(block.timestamp) << 128) | (uint256(block.timestamp + DESTINATION_PROPOSAL_DURATION) << 64)), + address(multisig), + uint64(block.timestamp), + uint64(block.timestamp + DESTINATION_PROPOSAL_DURATION), + "ipfs://proposal-metadata-here", + actions, + 0 + ); - assertEq(metadataURI, "ipfs://different-metadata", "Incorrect metadata URI"); - assertEq(address(destPlugin), address(optimisticPlugin), "Incorrect destPlugin"); + multisig.execute(0); - // Approve - vm.startPrank(carol); - multisig.approve(pid, false); + // Verify execution + (bool executed,,,,,) = multisig.getProposal(0); + assertEq(executed, true, "Should be executed"); + } - // Check round 3 - (executed, approvals, parameters, metadataURI, actions, destPlugin) = multisig.getProposal(pid); + modifier givenTheProposalIsAlreadyExecuted() { + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 1)); - assertEq(executed, false, "Should not be executed"); - assertEq(approvals, 3, "Should be 3"); + // Alice: listed on creation and self appointed - assertEq(parameters.minApprovals, 2, "Incorrect minApprovals"); - assertEq(parameters.snapshotBlock, block.number - 1, "Incorrect snapshotBlock"); - assertEq(parameters.expirationDate, block.timestamp + 10 days, "Incorrect expirationDate"); + // Bob: listed on creation, appointing someone else now + vm.startPrank(bob); + encryptionRegistry.appointWallet(randomWallet); - assertEq(actions.length, 2, "Should be 2"); + // Random Wallet: appointed by a listed signer on creation + // 0x1234: unlisted and unappointed on creation - assertEq(actions[1].to, alice, "Incorrect to"); - assertEq(actions[1].value, 1 ether, "Incorrect value"); - assertEq(actions[1].data, hex"001122334455", "Incorrect data"); - assertEq(actions[0].to, carol, "Incorrect to"); - assertEq(actions[0].value, 3 ether, "Incorrect value"); - assertEq(actions[0].data, hex"223344556677", "Incorrect data"); + vm.deal(address(dao), 1 ether); - assertEq(metadataURI, "ipfs://different-metadata", "Incorrect metadata URI"); - assertEq(address(destPlugin), address(optimisticPlugin), "Incorrect destPlugin"); + // Create proposal 0 + IDAO.Action[] memory actions = new IDAO.Action[](1); + actions[0].value = 0; + actions[0].to = address(bob); + actions[0].data = hex""; + uint256 pid = multisig.createProposal("ipfs://the-metadata-here", actions, optimisticPlugin, false); - // Execute + // Remove (later) + vm.roll(block.number + 50); + address[] memory addrs = new address[](2); + addrs[0] = alice; + addrs[1] = bob; vm.startPrank(alice); - multisig.execute(pid); + signerList.removeSigners(addrs); - // Check round 4 - (executed, approvals, parameters, metadataURI, actions, destPlugin) = multisig.getProposal(pid); + multisig.approve(pid, false); - assertEq(executed, true, "Should be executed"); - assertEq(approvals, 3, "Should be 3"); + vm.startPrank(randomWallet); + multisig.approve(pid, false); - assertEq(parameters.minApprovals, 2, "Incorrect minApprovals"); - assertEq(parameters.snapshotBlock, block.number - 1, "Incorrect snapshotBlock"); - assertEq(parameters.expirationDate, block.timestamp + 10 days, "Incorrect expirationDate"); + vm.startPrank(carol); + multisig.approve(pid, false); - assertEq(actions.length, 2, "Should be 2"); + multisig.execute(pid); - assertEq(actions[1].to, alice, "Incorrect to"); - assertEq(actions[1].value, 1 ether, "Incorrect value"); - assertEq(actions[1].data, hex"001122334455", "Incorrect data"); - assertEq(actions[0].to, carol, "Incorrect to"); - assertEq(actions[0].value, 3 ether, "Incorrect value"); - assertEq(actions[0].data, hex"223344556677", "Incorrect data"); + vm.startPrank(alice); - assertEq(metadataURI, "ipfs://different-metadata", "Incorrect metadata URI"); - assertEq(address(destPlugin), address(optimisticPlugin), "Incorrect destPlugin"); + _; } - function testFuzz_GetProposalReturnsEmptyValuesForNonExistingOnes(uint256 randomProposalId) public view { + function test_WhenCallingGetProposalBeingExecuted() external givenTheProposalIsAlreadyExecuted { + // It should return the right values + ( bool executed, uint16 approvals, Multisig.ProposalParameters memory parameters, bytes memory metadataURI, - IDAO.Action[] memory destinationActions, + IDAO.Action[] memory proposalActions, OptimisticTokenVotingPlugin destinationPlugin - ) = multisig.getProposal(randomProposalId); - - assertEq(executed, false, "The proposal should not be executed"); - assertEq(approvals, 0, "The tally should be zero"); - assertEq(metadataURI, "", "Incorrect metadataURI"); - assertEq(parameters.expirationDate, 0, "Incorrect expirationDate"); - assertEq(parameters.snapshotBlock, 0, "Incorrect snapshotBlock"); - assertEq(parameters.minApprovals, 0, "Incorrect minApprovals"); - assertEq(destinationActions.length, 0, "Actions has should have 0 items"); - assertEq(address(destinationPlugin), address(0), "Incorrect destination plugin"); - } - - function test_ProxiedProposalHasTheSameSettingsAsTheOriginal() public { - // Recreated proposal has the same settings and actions as registered here + ) = multisig.getProposal(0); - bool open; - bool executed; - bytes memory metadataUri; - OptimisticTokenVotingPlugin.ProposalParameters memory parameters; - uint256 vetoTally; - IDAO.Action[] memory actions; - uint256 allowFailureMap; - - vm.warp(2 days); - - IDAO.Action[] memory createActions = new IDAO.Action[](3); - createActions[0].to = alice; - createActions[0].value = 1 ether; - createActions[0].data = hex"001122334455"; - createActions[1].to = bob; - createActions[1].value = 2 ether; - createActions[1].data = hex"112233445566"; - createActions[2].to = carol; - createActions[2].value = 3 ether; - createActions[2].data = hex"223344556677"; - - uint256 pid = multisig.createProposal("ipfs://metadata", createActions, optimisticPlugin, false); - - // Approve - multisig.approve(pid, false); - vm.startPrank(bob); - multisig.approve(pid, false); - vm.startPrank(carol); - multisig.approve(pid, false); + assertEq(executed, true, "Should be executed"); + assertEq(approvals, 3, "Should have 3 approvals"); + assertEq(parameters.minApprovals, 3, "Should require 3 approvals"); + assertEq(parameters.snapshotBlock, block.number - 1 - 50, "Incorrect snapshot block"); // -51 due to vm.roll(block.number + 50) + assertEq( + parameters.expirationDate, block.timestamp + MULTISIG_PROPOSAL_EXPIRATION_PERIOD, "Incorrect expiration" + ); + assertEq(metadataURI, "ipfs://the-metadata-here", "Incorrect metadata URI"); + assertEq(address(destinationPlugin), address(optimisticPlugin), "Incorrect destination plugin"); - vm.startPrank(alice); - multisig.execute(pid); + // Verify actions + IDAO.Action[] memory actions = new IDAO.Action[](1); + actions[0].value = 0; + actions[0].to = address(bob); + actions[0].data = hex""; - // Check round - // start=1d, end=10d, counter=0 - (open, executed, parameters, vetoTally, metadataUri, actions, allowFailureMap) = - optimisticPlugin.getProposal(uint256(2 days) << 128 | uint256(2 days + 10 days) << 64); + assertEq(proposalActions.length, actions.length, "Actions length should match"); + for (uint256 i = 0; i < actions.length; i++) { + assertEq(proposalActions[i].to, actions[i].to, "Action to should match"); + assertEq(proposalActions[i].value, actions[i].value, "Action value should match"); + assertEq(proposalActions[i].data, actions[i].data, "Action data should match"); + } + } - assertEq(open, true, "Should be open"); - assertEq(executed, false, "Should not be executed"); - assertEq(vetoTally, 0, "Should be 0"); + function test_WhenCallingCanApproveOrApproveBeingExecuted() external givenTheProposalIsAlreadyExecuted { + // It canApprove should return false (when listed on creation, self appointed now) + // It canApprove should return false (when listed on creation, appointing someone else now) + // It canApprove should return false (when currently appointed by a signer listed on creation) + // It canApprove should return false (when unlisted on creation, unappointed now) + // It approve should revert (when listed on creation, self appointed now) + // It approve should revert (when listed on creation, appointing someone else now) + // It approve should revert (when currently appointed by a signer listed on creation) + // It approve should revert (when unlisted on creation, unappointed now) - assertEq(metadataUri, "ipfs://metadata", "Incorrect target metadataUri"); - assertEq(parameters.vetoEndDate, 2 days + 10 days, "Incorrect target vetoEndDate"); + // When listed on creation, self appointed now + assertEq(multisig.canApprove(0, alice), false, "Alice should not be able to approve"); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, alice)); + multisig.approve(0, false); - assertEq(actions.length, 3, "Should be 3"); + // When listed on creation, appointing someone else now + assertEq(multisig.canApprove(0, bob), false, "Bob should not be able to approve"); + vm.startPrank(bob); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, bob)); + multisig.approve(0, false); - assertEq(actions[0].to, alice, "Incorrect to"); - assertEq(actions[0].value, 1 ether, "Incorrect value"); - assertEq(actions[0].data, hex"001122334455", "Incorrect data"); - assertEq(actions[1].to, bob, "Incorrect to"); - assertEq(actions[1].value, 2 ether, "Incorrect value"); - assertEq(actions[1].data, hex"112233445566", "Incorrect data"); - assertEq(actions[2].to, carol, "Incorrect to"); - assertEq(actions[2].value, 3 ether, "Incorrect value"); - assertEq(actions[2].data, hex"223344556677", "Incorrect data"); + // When currently appointed by a signer listed on creation + assertEq(multisig.canApprove(0, randomWallet), false, "Random wallet should not be able to approve"); + vm.startPrank(randomWallet); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, randomWallet)); + multisig.approve(0, false); + + // When unlisted on creation, unappointed now + assertEq(multisig.canApprove(0, address(0x1234)), false, "Unlisted address should not be able to approve"); + vm.startPrank(address(0x1234)); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, address(0x1234))); + multisig.approve(0, false); + } + + function test_WhenCallingHasApprovedBeingExecuted() external givenTheProposalIsAlreadyExecuted { + // It hasApproved should return false until approved + + assertEq(multisig.hasApproved(0, alice), true, "Alice should show as approved"); + assertEq(multisig.hasApproved(0, bob), true, "Bob should show as approved"); + assertEq(multisig.hasApproved(0, randomWallet), true, "Random wallet should show as approved"); + assertEq(multisig.hasApproved(0, carol), true, "Carol should show as approved"); + assertEq(multisig.hasApproved(0, david), false, "David should not show as approved"); + } + + function test_WhenCallingCanExecuteOrExecuteBeingExecuted() external givenTheProposalIsAlreadyExecuted { + // It canExecute should return false (when listed on creation, self appointed now) + // It canExecute should return false (when listed on creation, appointing someone else now) + // It canExecute should return false (when currently appointed by a signer listed on creation) + // It canExecute should return false (when unlisted on creation, unappointed now) + // It execute should revert (when listed on creation, self appointed now) + // It execute should revert (when listed on creation, appointing someone else now) + // It execute should revert (when currently appointed by a signer listed on creation) + // It execute should revert (when unlisted on creation, unappointed now) + + // When listed on creation, self appointed now + // vm.startPrank(alice); + assertEq(multisig.canExecute(0), false, "Should not be executable after execution"); + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, 0)); + multisig.execute(0); + + // When listed on creation, appointing someone else now + vm.startPrank(bob); + assertEq(multisig.canExecute(0), false, "Should not be executable after execution"); + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, 0)); + multisig.execute(0); - assertEq(allowFailureMap, 0, "Should be 0"); + // When currently appointed by a signer listed on creation + vm.startPrank(randomWallet); + assertEq(multisig.canExecute(0), false, "Should not be executable after execution"); + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, 0)); + multisig.execute(0); - // New proposal - vm.warp(3 days); + // When unlisted on creation, unappointed now + vm.startPrank(address(0x1234)); + assertEq(multisig.canExecute(0), false, "Should not be executable after execution"); + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, 0)); + multisig.execute(0); + } - createActions = new IDAO.Action[](2); - createActions[1].to = alice; - createActions[1].value = 1 ether; - createActions[1].data = hex"001122334455"; - createActions[0].to = carol; - createActions[0].value = 3 ether; - createActions[0].data = hex"223344556677"; + modifier givenTheProposalExpired() { + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 1)); - pid = multisig.createProposal("ipfs://more-metadata", createActions, optimisticPlugin, false); + // Alice: listed on creation and self appointed - // Approve - multisig.approve(pid, false); + // Bob: listed on creation, appointing someone else now vm.startPrank(bob); - multisig.approve(pid, false); - vm.startPrank(carol); - multisig.approve(pid, false); + encryptionRegistry.appointWallet(randomWallet); - vm.startPrank(alice); - multisig.execute(pid); - - // Check round - (open, executed, parameters, vetoTally, metadataUri, actions, allowFailureMap) = - optimisticPlugin.getProposal((uint256(3 days) << 128 | uint256(3 days + 10 days) << 64) + 1); - - assertEq(open, true, "Should be open"); - assertEq(executed, false, "Should not be executed"); - assertEq(vetoTally, 0, "Should be 0"); - - assertEq(metadataUri, "ipfs://more-metadata", "Incorrect target metadataUri"); - assertEq(parameters.vetoEndDate, 3 days + 10 days, "Incorrect target vetoEndDate"); + // Random Wallet: appointed by a listed signer on creation + // 0x1234: unlisted and unappointed on creation - assertEq(actions.length, 2, "Should be 2"); + vm.deal(address(dao), 1 ether); - assertEq(actions[1].to, alice, "Incorrect to"); - assertEq(actions[1].value, 1 ether, "Incorrect value"); - assertEq(actions[1].data, hex"001122334455", "Incorrect data"); - assertEq(actions[0].to, carol, "Incorrect to"); - assertEq(actions[0].value, 3 ether, "Incorrect value"); - assertEq(actions[0].data, hex"223344556677", "Incorrect data"); + // Create proposal 0 + IDAO.Action[] memory actions = new IDAO.Action[](1); + actions[0].value = 0; + actions[0].to = address(bob); + actions[0].data = hex""; + uint256 pid = multisig.createProposal("ipfs://", actions, optimisticPlugin, false); - assertEq(allowFailureMap, 0, "Should be 0"); - } + // Remove (later) + vm.roll(block.number + 50); + address[] memory addrs = new address[](2); + addrs[0] = alice; + addrs[1] = bob; + vm.startPrank(alice); + signerList.removeSigners(addrs); - // Upgrade multisig + multisig.approve(pid, false); - function test_UpgradeToRevertsWhenCalledFromNonUpgrader() public { - address initialImplementation = multisig.implementation(); - address _newImplementation = address(new Multisig()); + vm.startPrank(randomWallet); + multisig.approve(pid, false); - vm.expectRevert( - abi.encodeWithSelector( - DaoUnauthorized.selector, - address(dao), - address(multisig), - alice, - multisig.UPGRADE_PLUGIN_PERMISSION_ID() - ) - ); + vm.warp(block.timestamp + MULTISIG_PROPOSAL_EXPIRATION_PERIOD); - multisig.upgradeTo(_newImplementation); + vm.startPrank(alice); - assertEq(multisig.implementation(), initialImplementation); + _; } - function test_UpgradeToAndCallRevertsWhenCalledFromNonUpgrader() public { - address initialImplementation = multisig.implementation(); - dao.grant(address(multisig), alice, multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); - address _newImplementation = address(new Multisig()); + function test_WhenCallingGetProposalBeingExpired() external givenTheProposalExpired { + // It should return the right values - Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ - onlyListed: false, - minApprovals: 2, - destinationProposalDuration: 14 days, - proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); + ( + bool executed, + uint16 approvals, + Multisig.ProposalParameters memory parameters, + bytes memory metadataURI, + IDAO.Action[] memory proposalActions, + OptimisticTokenVotingPlugin destinationPlugin + ) = multisig.getProposal(0); - vm.expectRevert( - abi.encodeWithSelector( - DaoUnauthorized.selector, - address(dao), - address(multisig), - alice, - multisig.UPGRADE_PLUGIN_PERMISSION_ID() - ) + assertEq(executed, false, "Should not be executed"); + assertEq(approvals, 2, "Should have 2 approvals"); + assertEq(parameters.minApprovals, 3, "Should require 3 approvals"); + assertEq(parameters.snapshotBlock, block.number - 1 - 50, "Incorrect snapshot block"); + assertEq(parameters.expirationDate, block.timestamp, "Should be expired"); + assertEq(metadataURI, "ipfs://", "Incorrect metadata URI"); + assertEq(address(destinationPlugin), address(optimisticPlugin), "Incorrect destination plugin"); + + // Verify actions + assertEq(proposalActions.length, 1, "Should have 1 action"); + assertEq(proposalActions[0].to, address(bob), "Incorrect action target"); + assertEq(proposalActions[0].value, 0, "Incorrect action value"); + assertEq(proposalActions[0].data, "", "Incorrect action data"); + } + + function test_WhenCallingCanApproveOrApproveBeingExpired() external givenTheProposalExpired { + // It canApprove should return false (when listed on creation, self appointed now) + // It canApprove should return false (when listed on creation, appointing someone else now) + // It canApprove should return false (when currently appointed by a signer listed on creation) + // It canApprove should return false (when unlisted on creation, unappointed now) + // It approve should revert (when listed on creation, self appointed now) + // It approve should revert (when listed on creation, appointing someone else now) + // It approve should revert (when currently appointed by a signer listed on creation) + // It approve should revert (when unlisted on creation, unappointed now) + + // When listed on creation, self appointed now + assertEq(multisig.canApprove(0, alice), false, "Alice should not be able to approve expired proposal"); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, alice)); + multisig.approve(0, false); + + // When listed on creation, appointing someone else now + assertEq(multisig.canApprove(0, bob), false, "Bob should not be able to approve expired proposal"); + vm.startPrank(bob); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, bob)); + multisig.approve(0, false); + + // When currently appointed by a signer listed on creation + assertEq( + multisig.canApprove(0, randomWallet), false, "Random wallet should not be able to approve expired proposal" ); - multisig.upgradeToAndCall(_newImplementation, abi.encodeCall(Multisig.updateMultisigSettings, (settings))); + vm.startPrank(randomWallet); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, randomWallet)); + multisig.approve(0, false); - assertEq(multisig.implementation(), initialImplementation); + // When unlisted on creation, unappointed now + address unlistedAddress = address(0x1234); + assertEq( + multisig.canApprove(0, unlistedAddress), + false, + "Unlisted address should not be able to approve expired proposal" + ); + vm.startPrank(unlistedAddress); + vm.expectRevert(abi.encodeWithSelector(Multisig.ApprovalCastForbidden.selector, 0, unlistedAddress)); + multisig.approve(0, false); } - function test_UpgradeToSucceedsWhenCalledFromUpgrader() public { - dao.grant(address(multisig), alice, multisig.UPGRADE_PLUGIN_PERMISSION_ID()); - - address _newImplementation = address(new Multisig()); - - vm.expectEmit(); - emit Upgraded(_newImplementation); - - multisig.upgradeTo(_newImplementation); + function test_WhenCallingHasApprovedBeingExpired() external givenTheProposalExpired { + // It hasApproved should return false until approved - assertEq(multisig.implementation(), address(_newImplementation)); + assertEq(multisig.hasApproved(0, alice), true, "Alice should show as approved"); + assertEq(multisig.hasApproved(0, bob), true, "Bob should show as approved"); + assertEq(multisig.hasApproved(0, randomWallet), true, "Random wallet should show as approved"); + assertEq(multisig.hasApproved(0, carol), false, "Carol should not show as approved"); + assertEq(multisig.hasApproved(0, david), false, "David should not show as approved"); } - function test_UpgradeToAndCallSucceedsWhenCalledFromUpgrader() public { - dao.grant(address(multisig), alice, multisig.UPGRADE_PLUGIN_PERMISSION_ID()); - dao.grant(address(multisig), alice, multisig.UPDATE_MULTISIG_SETTINGS_PERMISSION_ID()); - - address _newImplementation = address(new Multisig()); + function test_WhenCallingCanExecuteOrExecuteBeingExpired() external givenTheProposalExpired { + // It canExecute should return false (when listed on creation, self appointed now) + // It execute should revert (when listed on creation, self appointed now) + // It canExecute should return false (when listed on creation, appointing someone else now) + // It execute should revert (when listed on creation, appointing someone else now) + // It canExecute should return false (when currently appointed by a signer listed on creation) + // It execute should revert (when currently appointed by a signer listed on creation) + // It canExecute should return false (when unlisted on creation, unappointed now) + // It execute should revert (when unlisted on creation, unappointed now) - vm.expectEmit(); - emit Upgraded(_newImplementation); + // When listed on creation, self appointed now + assertEq(multisig.canExecute(0), false, "Should not be executable when expired"); + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, 0)); + multisig.execute(0); - Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ - onlyListed: false, - minApprovals: 2, - destinationProposalDuration: 14 days, - proposalExpirationPeriod: MULTISIG_PROPOSAL_EXPIRATION_PERIOD - }); - multisig.upgradeToAndCall(_newImplementation, abi.encodeCall(Multisig.updateMultisigSettings, (settings))); + // When listed on creation, appointing someone else now + vm.startPrank(bob); + assertEq(multisig.canExecute(0), false, "Should not be executable when expired"); + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, 0)); + multisig.execute(0); - assertEq(multisig.implementation(), address(_newImplementation)); + // When currently appointed by a signer listed on creation + vm.startPrank(randomWallet); + assertEq(multisig.canExecute(0), false, "Should not be executable when expired"); + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, 0)); + multisig.execute(0); + + // When unlisted on creation, unappointed now + vm.startPrank(address(0x1234)); + assertEq(multisig.canExecute(0), false, "Should not be executable when expired"); + vm.expectRevert(abi.encodeWithSelector(Multisig.ProposalExecutionForbidden.selector, 0)); + multisig.execute(0); } } diff --git a/test/Multisig.t.yaml b/test/Multisig.t.yaml new file mode 100644 index 0000000..cad10df --- /dev/null +++ b/test/Multisig.t.yaml @@ -0,0 +1,331 @@ +MultisigTest: + # Plugin lifecycle + - given: a newly deployed contract + then: + - given: calling initialize + then: + - it: should initialize the first time + - it: should refuse to initialize again + - it: should set the DAO address + + # updateSettings below should have the same branches: + - it: should set the minApprovals + - it: should set onlyListed + - it: should set signerList + - it: should set destinationProposalDuration + - it: should set proposalExpirationPeriod + - it: should emit MultisigSettingsUpdated + - when: minApprovals is greater than signerList length [on initialize] + then: + - it: should revert + - it: should revert (with onlyListed false) + - it: should not revert otherwise + - when: minApprovals is zero [on initialize] + then: + - it: should revert + - it: should revert (with onlyListed false) + - it: should not revert otherwise + - when: signerList is invalid [on initialize] + then: + - it: should revert + + - when: calling upgradeTo + then: + - it: should revert when called without the permission + - it: should work when called with the permission + + - when: calling upgradeToAndCall + then: + - it: should revert when called without the permission + - it: should work when called with the permission + + # General methods + + - when: calling supportsInterface + then: + - it: does not support the empty interface + - it: supports IERC165Upgradeable + - it: supports IPlugin + - it: supports IProposal + - it: supports IMultisig + + - when: calling updateSettings + then: + - given: caller has no permission + then: + - it: should revert + - it: otherwise it should just work + + # initialize above should have the same branches: + - it: should set the minApprovals + - it: should set onlyListed + - it: should set signerList + - it: should set destinationProposalDuration + - it: should set proposalExpirationPeriod + - it: should emit MultisigSettingsUpdated + - when: minApprovals is greater than signerList length [on updateSettings] + then: + - it: should revert + - it: should revert (with onlyListed false) + - it: should not revert otherwise + - when: minApprovals is zero [on updateSettings] + then: + - it: should revert + - it: should revert (with onlyListed false) + - it: should not revert otherwise + - when: signerList is invalid [on updateSettings] + then: + - it: should revert + + - when: calling createProposal + then: + - it: increments the proposal counter + - it: creates and return unique proposal IDs + - it: emits the ProposalCreated event + - it: creates a proposal with the given values + - given: settings changed on the same block + then: + - it: reverts + - it: does not revert otherwise + - given: onlyListed is false + then: + - it: allows anyone to create + - given: onlyListed is true + and: + - given: creation caller is not listed or appointed + then: + - it: reverts + - it: reverts if listed before but not now + - given: creation caller is appointed by a former signer + then: + - it: reverts + - given: creation caller is listed and self appointed + then: + - it: creates the proposal + - given: creation caller is listed, appointing someone else now + then: + - it: creates the proposal + - given: creation caller is appointed by a current signer + then: + - it: creates the proposal + - given: approveProposal is true + then: + - it: creates and calls approval in one go + - given: approveProposal is false + then: + - it: only creates the proposal + + # Proposal lifecycle + + - given: The proposal is not created + then: + # Get proposal + - when: calling getProposal [being uncreated] + then: + - it: should return empty values + # Approval + - when: calling canApprove or approve [being uncreated] + then: + - it: canApprove should return false (when listed and self appointed) + - it: approve should revert (when listed and self appointed) + - it: canApprove should return false (when listed, appointing someone else now) + - it: approve should revert (when listed, appointing someone else now) + - it: canApprove should return false (when appointed by a listed signer) + - it: approve should revert (when appointed by a listed signer) + - it: canApprove should return false (when unlisted and unappointed) + - it: approve should revert (when unlisted and unappointed) + # Has approved + - when: calling hasApproved [being uncreated] + then: + - it: hasApproved should always return false + # Execution + - when: calling canExecute or execute [being uncreated] + then: + - it: canExecute should return false (when listed and self appointed) + - it: execute should revert (when listed and self appointed) + - it: canExecute should return false (when listed, appointing someone else now) + - it: execute should revert (when listed, appointing someone else now) + - it: canExecute should return false (when appointed by a listed signer) + - it: execute should revert (when appointed by a listed signer) + - it: canExecute should return false (when unlisted and unappointed) + - it: execute should revert (when unlisted and unappointed) + + - given: The proposal is open + then: + # Get proposal + - when: calling getProposal [being open] + then: + - it: should return the right values + # Approval + - when: calling canApprove or approve [being open] + then: + - it: canApprove should return true (when listed on creation, self appointed now) + - it: approve should work (when listed on creation, self appointed now) + - it: approve should emit an event (when listed on creation, self appointed now) + - it: canApprove should return false (when listed on creation, appointing someone else now) + - it: approve should revert (when listed on creation, appointing someone else now) + - it: canApprove should return true (when currently appointed by a signer listed on creation) + - it: approve should work (when currently appointed by a signer listed on creation) + - it: approve should emit an event (when currently appointed by a signer listed on creation) + - it: canApprove should return false (when unlisted on creation, unappointed now) + - it: approve should revert (when unlisted on creation, unappointed now) + # Auto execution + - when: calling approve with tryExecution and almost passed [being open] + then: + - it: approve should also execute the proposal + - it: approve should emit an Executed event + # Proposal forwarding on execution + - it: approve recreates the proposal on the destination plugin + - it: The parameters of the recreated proposal match those of the approved one + - it: A ProposalCreated event is emitted on the destination plugin + + # Has approved + - when: calling hasApproved [being open] + then: + - it: hasApproved should return false until approved + # Execution + - when: calling canExecute or execute [being open] + then: + - it: canExecute should return false (when listed on creation, self appointed now) + - it: execute should revert (when listed on creation, self appointed now) + - it: canExecute should return false (when listed on creation, appointing someone else now) + - it: execute should revert (when listed on creation, appointing someone else now) + - it: canExecute should return false (when currently appointed by a signer listed on creation) + - it: execute should revert (when currently appointed by a signer listed on creation) + - it: canExecute should return false (when unlisted on creation, unappointed now) + - it: execute should revert (when unlisted on creation, unappointed now) + + - given: The proposal was approved by the address + then: + # Get proposal + - when: calling getProposal [being approved] + then: + - it: should return the right values + # Approval + - when: calling canApprove or approve [being approved] + then: + - it: canApprove should return false (when listed on creation, self appointed now) + - it: approve should revert (when listed on creation, self appointed now) + # - it: canApprove should return false (when listed on creation, appointing someone else now) + # - it: approve should revert (when listed on creation, appointing someone else now) + - it: canApprove should return false (when currently appointed by a signer listed on creation) + - it: approve should revert (when currently appointed by a signer listed on creation) + # - it: canApprove should return false (when unlisted on creation, unappointed now) + # - it: approve should revert (when unlisted on creation, unappointed now) + # Has approved + - when: calling hasApproved [being approved] + then: + - it: hasApproved should return false until approved + # Execution + - when: calling canExecute or execute [being approved] + then: + - it: canExecute should return false (when listed on creation, self appointed now) + - it: execute should revert (when listed on creation, self appointed now) + # - it: canExecute should return false (when listed on creation, appointing someone else now) + # - it: execute should revert (when listed on creation, appointing someone else now) + - it: canExecute should return false (when currently appointed by a signer listed on creation) + - it: execute should revert (when currently appointed by a signer listed on creation) + # - it: canExecute should return false (when unlisted on creation, unappointed now) + # - it: execute should revert (when unlisted on creation, unappointed now) + + - given: The proposal passed + then: + # Get proposal + - when: calling getProposal [being passed] + then: + - it: should return the right values + # Approval + - when: calling canApprove or approve [being passed] + then: + - it: canApprove should return false (when listed on creation, self appointed now) + - it: approve should revert (when listed on creation, self appointed now) + - it: canApprove should return false (when listed on creation, appointing someone else now) + - it: approve should revert (when listed on creation, appointing someone else now) + - it: canApprove should return false (when currently appointed by a signer listed on creation) + - it: approve should revert (when currently appointed by a signer listed on creation) + - it: canApprove should return false (when unlisted on creation, unappointed now) + - it: approve should revert (when unlisted on creation, unappointed now) + # Has approved + - when: calling hasApproved [being passed] + then: + - it: hasApproved should return false until approved + # Execution + - when: calling canExecute or execute [being passed] + then: + - it: canExecute should return true, always + - it: execute should work, when called by anyone + - it: execute should emit an event, when called by anyone + # Proposal forwarding on execution + - it: execute recreates the proposal on the destination plugin + - it: The parameters of the recreated proposal match those of the executed one + - it: The proposal duration on the destination plugin matches the multisig settings + - it: A ProposalCreated event is emitted on the destination plugin + - given: TaikoL1 is incompatible + then: + - it: executes successfully, regardless + + - given: The proposal is already executed + then: + # Get proposal + - when: calling getProposal [being executed] + then: + - it: should return the right values + # Approval + - when: calling canApprove or approve [being executed] + then: + - it: canApprove should return false (when listed on creation, self appointed now) + - it: approve should revert (when listed on creation, self appointed now) + - it: canApprove should return false (when listed on creation, appointing someone else now) + - it: approve should revert (when listed on creation, appointing someone else now) + - it: canApprove should return false (when currently appointed by a signer listed on creation) + - it: approve should revert (when currently appointed by a signer listed on creation) + - it: canApprove should return false (when unlisted on creation, unappointed now) + - it: approve should revert (when unlisted on creation, unappointed now) + # Has approved + - when: calling hasApproved [being executed] + then: + - it: hasApproved should return false until approved + # Execution + - when: calling canExecute or execute [being executed] + then: + - it: canExecute should return false (when listed on creation, self appointed now) + - it: execute should revert (when listed on creation, self appointed now) + - it: canExecute should return false (when listed on creation, appointing someone else now) + - it: execute should revert (when listed on creation, appointing someone else now) + - it: canExecute should return false (when currently appointed by a signer listed on creation) + - it: execute should revert (when currently appointed by a signer listed on creation) + - it: canExecute should return false (when unlisted on creation, unappointed now) + - it: execute should revert (when unlisted on creation, unappointed now) + + - given: The proposal expired + then: + # Get proposal + - when: calling getProposal [being expired] + then: + - it: should return the right values + # Approval + - when: calling canApprove or approve [being expired] + then: + - it: canApprove should return false (when listed on creation, self appointed now) + - it: approve should revert (when listed on creation, self appointed now) + - it: canApprove should return false (when listed on creation, appointing someone else now) + - it: approve should revert (when listed on creation, appointing someone else now) + - it: canApprove should return false (when currently appointed by a signer listed on creation) + - it: approve should revert (when currently appointed by a signer listed on creation) + - it: canApprove should return false (when unlisted on creation, unappointed now) + - it: approve should revert (when unlisted on creation, unappointed now) + # Has approved + - when: calling hasApproved [being expired] + then: + - it: hasApproved should return false until approved + # Execution + - when: calling canExecute or execute [being expired] + then: + - it: canExecute should return false (when listed on creation, self appointed now) + - it: execute should revert (when listed on creation, self appointed now) + - it: canExecute should return false (when listed on creation, appointing someone else now) + - it: execute should revert (when listed on creation, appointing someone else now) + - it: canExecute should return false (when currently appointed by a signer listed on creation) + - it: execute should revert (when currently appointed by a signer listed on creation) + - it: canExecute should return false (when unlisted on creation, unappointed now) + - it: execute should revert (when unlisted on creation, unappointed now) diff --git a/test/MultisigPluginSetup.t.sol b/test/MultisigPluginSetup.t.sol index b047141..8bd2a13 100644 --- a/test/MultisigPluginSetup.t.sol +++ b/test/MultisigPluginSetup.t.sol @@ -1,40 +1,37 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.17; -import {Test} from "forge-std/Test.sol"; +import {AragonTest} from "./base/AragonTest.sol"; +import {DaoBuilder} from "./helpers/DaoBuilder.sol"; import {Multisig} from "../src/Multisig.sol"; import {MultisigPluginSetup} from "../src/setup/MultisigPluginSetup.sol"; -import {GovernanceERC20} from "@aragon/osx/token/ERC20/governance/GovernanceERC20.sol"; -import {GovernanceWrappedERC20} from "@aragon/osx/token/ERC20/governance/GovernanceWrappedERC20.sol"; -import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import { + SignerList, + UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID, + UPDATE_SIGNER_LIST_PERMISSION_ID +} from "../src/SignerList.sol"; import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; -import {RATIO_BASE} from "@aragon/osx/plugins/utils/Ratio.sol"; import {DAO} from "@aragon/osx/core/dao/DAO.sol"; import {IPluginSetup} from "@aragon/osx/framework/plugin/setup/PluginSetup.sol"; import {PermissionLib} from "@aragon/osx/core/permission/PermissionLib.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {ERC20Mock} from "./mocks/ERC20Mock.sol"; -import {ITaikoL1} from "../src/adapted-dependencies/ITaikoL1.sol"; -contract MultisigPluginSetupTest is Test { +contract MultisigPluginSetupTest is AragonTest { MultisigPluginSetup public pluginSetup; - GovernanceERC20 governanceERC20Base; - GovernanceWrappedERC20 governanceWrappedERC20Base; address immutable daoBase = address(new DAO()); + address immutable signerListBase = address(new SignerList()); DAO dao; // Recycled installation parameters Multisig.MultisigSettings multisigSettings; - address[] members; - - address alice = address(0xa11ce); - address bob = address(0xb0b); - address carol = address(0xc4601); - address dave = address(0xd473); - - error Unimplemented(); + address[] signers; + SignerList signerList; function setUp() public { + DaoBuilder builder = new DaoBuilder(); + (dao,,,,, signerList,,) = builder.withMultisigMember(alice).withMultisigMember(bob).withMultisigMember(carol) + .withMultisigMember(david).build(); + pluginSetup = new MultisigPluginSetup(); // Default params @@ -42,57 +39,48 @@ contract MultisigPluginSetupTest is Test { onlyListed: true, minApprovals: 3, destinationProposalDuration: 10 days, + signerList: signerList, proposalExpirationPeriod: 15 days }); - - members = new address[](4); - members[0] = alice; - members[1] = bob; - members[2] = carol; - members[3] = dave; } function test_ShouldEncodeInstallationParameters_1() public view { // 1 - bytes memory output = pluginSetup.encodeInstallationParameters(members, multisigSettings); + bytes memory output = pluginSetup.encodeInstallationParameters(multisigSettings); bytes memory expected = - hex"00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000d2f00000000000000000000000000000000000000000000000000000000000013c680000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000a11ce0000000000000000000000000000000000000000000000000000000000000b0b00000000000000000000000000000000000000000000000000000000000c4601000000000000000000000000000000000000000000000000000000000000d473"; + hex"0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000d2f00000000000000000000000000a0279152cf631d6c493901f9b576d88e2847bfa1000000000000000000000000000000000000000000000000000000000013c680"; assertEq(output, expected, "Incorrect encoded bytes"); } function test_ShouldEncodeInstallationParameters_2() public { // 2 + signers = new address[](2); + signers[0] = alice; + signers[1] = bob; + signerList = + SignerList(createProxyAndCall(signerListBase, abi.encodeCall(SignerList.initialize, (IDAO(dao), signers)))); + multisigSettings = Multisig.MultisigSettings({ onlyListed: true, minApprovals: 1, destinationProposalDuration: 5 days, + signerList: signerList, proposalExpirationPeriod: 33 days }); - members = new address[](2); - members[0] = alice; - members[1] = bob; - - bytes memory output = pluginSetup.encodeInstallationParameters(members, multisigSettings); + bytes memory output = pluginSetup.encodeInstallationParameters(multisigSettings); bytes memory expected = - hex"00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000006978000000000000000000000000000000000000000000000000000000000002b8180000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000a11ce0000000000000000000000000000000000000000000000000000000000000b0b"; + hex"00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000006978000000000000000000000000003a6a84cd762d9707a21605b548aaab891562aab00000000000000000000000000000000000000000000000000000000002b8180"; assertEq(output, expected, "Incorrect encoded bytes"); } function test_ShouldDecodeInstallationParameters_1() public view { // 1 - bytes memory installationParams = pluginSetup.encodeInstallationParameters(members, multisigSettings); + bytes memory installationParams = pluginSetup.encodeInstallationParameters(multisigSettings); // Decode - (address[] memory outMembers, Multisig.MultisigSettings memory outSettings) = - pluginSetup.decodeInstallationParameters(installationParams); - - assertEq(outMembers.length, 4, "Incorrect length"); - assertEq(outMembers[0], alice, "Incorrect member"); - assertEq(outMembers[1], bob, "Incorrect member"); - assertEq(outMembers[2], carol, "Incorrect member"); - assertEq(outMembers[3], dave, "Incorrect member"); + (Multisig.MultisigSettings memory outSettings) = pluginSetup.decodeInstallationParameters(installationParams); assertEq(outSettings.onlyListed, true, "Should be true"); assertEq(outSettings.minApprovals, 3, "Should be 3"); @@ -101,26 +89,24 @@ contract MultisigPluginSetupTest is Test { function test_ShouldDecodeInstallationParameters_2() public { // 2 + signers = new address[](2); + signers[0] = alice; + signers[1] = bob; + signerList = + SignerList(createProxyAndCall(signerListBase, abi.encodeCall(SignerList.initialize, (IDAO(dao), signers)))); + multisigSettings = Multisig.MultisigSettings({ onlyListed: false, minApprovals: 1, destinationProposalDuration: 5 days, + signerList: signerList, proposalExpirationPeriod: 55 days }); - members = new address[](2); - members[0] = alice; - members[1] = bob; - - bytes memory installationParams = pluginSetup.encodeInstallationParameters(members, multisigSettings); + bytes memory installationParams = pluginSetup.encodeInstallationParameters(multisigSettings); // Decode - (address[] memory outMembers, Multisig.MultisigSettings memory outSettings) = - pluginSetup.decodeInstallationParameters(installationParams); - - assertEq(outMembers.length, 2, "Incorrect length"); - assertEq(outMembers[0], alice, "Incorrect member"); - assertEq(outMembers[1], bob, "Incorrect member"); + (Multisig.MultisigSettings memory outSettings) = pluginSetup.decodeInstallationParameters(installationParams); assertEq(outSettings.onlyListed, false, "Should be false"); assertEq(outSettings.minApprovals, 1, "Should be 1"); @@ -128,7 +114,7 @@ contract MultisigPluginSetupTest is Test { } function test_PrepareInstallationReturnsTheProperPermissions() public { - bytes memory installationParams = pluginSetup.encodeInstallationParameters(members, multisigSettings); + bytes memory installationParams = pluginSetup.encodeInstallationParameters(multisigSettings); (address _plugin, IPluginSetup.PreparedSetupData memory _preparedSetupData) = pluginSetup.prepareInstallation(address(dao), installationParams); @@ -167,7 +153,7 @@ contract MultisigPluginSetupTest is Test { function test_PrepareUninstallationReturnsTheProperPermissions_1() public { // Prepare a dummy install - bytes memory installationParams = pluginSetup.encodeInstallationParameters(members, multisigSettings); + bytes memory installationParams = pluginSetup.encodeInstallationParameters(multisigSettings); (address _dummyPlugin, IPluginSetup.PreparedSetupData memory _preparedSetupData) = pluginSetup.prepareInstallation(address(dao), installationParams); diff --git a/test/OptimisticTokenVotingPlugin.t.sol b/test/OptimisticTokenVotingPlugin.t.sol index 259b60b..a01beb0 100644 --- a/test/OptimisticTokenVotingPlugin.t.sol +++ b/test/OptimisticTokenVotingPlugin.t.sol @@ -53,7 +53,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { builder = new DaoBuilder(); // alice has root permission on the DAO, is a multisig member, holds tokens and can create proposals // on the optimistic token voting plugin - (dao, optimisticPlugin,,, votingToken, taikoL1) = builder.build(); + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.build(); } // Initialize @@ -405,7 +405,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // 2 builder = new DaoBuilder(); - (, optimisticPlugin,,, votingToken,) = builder.withTokenHolder(alice, 5 ether).withTokenHolder(bob, 200 ether) + (, optimisticPlugin,,, votingToken,,,) = builder.withTokenHolder(alice, 5 ether).withTokenHolder(bob, 200 ether) .withTokenHolder(carol, 2.5 ether).build(); assertEq(optimisticPlugin.totalVotingPower(block.timestamp - 1), 207.5 ether, "Incorrect total voting power"); @@ -413,7 +413,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // 2 builder = new DaoBuilder(); - (, optimisticPlugin,,, votingToken,) = builder.withTokenHolder(alice, 50 ether).withTokenHolder(bob, 30 ether) + (, optimisticPlugin,,, votingToken,,,) = builder.withTokenHolder(alice, 50 ether).withTokenHolder(bob, 30 ether) .withTokenHolder(carol, 0.1234 ether).withTokenHolder(david, 100 ether).build(); assertEq(optimisticPlugin.totalVotingPower(block.timestamp - 1), 180.1234 ether, "Incorrect total voting power"); @@ -428,7 +428,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // 1 bridged tokens builder = new DaoBuilder(); - (, optimisticPlugin,,, votingToken,) = + (, optimisticPlugin,,, votingToken,,,) = builder.withTokenHolder(alice, 10 ether).withTokenHolder(taikoBridge, 10 ether).build(); assertEq(optimisticPlugin.bridgedVotingPower(block.timestamp - 1), 10 ether, "Incorrect bridged voting power"); @@ -437,7 +437,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // 2 bridged tokens builder = new DaoBuilder(); - (, optimisticPlugin,,, votingToken,) = builder.withTokenHolder(alice, 10 ether).withTokenHolder(bob, 1 ether) + (, optimisticPlugin,,, votingToken,,,) = builder.withTokenHolder(alice, 10 ether).withTokenHolder(bob, 1 ether) .withTokenHolder(taikoBridge, 1).build(); assertEq(optimisticPlugin.bridgedVotingPower(block.timestamp - 1), 1, "Incorrect bridged voting power"); @@ -460,7 +460,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // 1 bridged tokens builder = new DaoBuilder(); - (, optimisticPlugin,,, votingToken,) = + (, optimisticPlugin,,, votingToken,,,) = builder.withTokenHolder(alice, 10 ether).withTokenHolder(taikoBridge, 10 ether).build(); assertEq( @@ -476,7 +476,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // 2 bridged tokens builder = new DaoBuilder(); - (, optimisticPlugin,,, votingToken,) = builder.withTokenHolder(alice, 10 ether).withTokenHolder(bob, 1 ether) + (, optimisticPlugin,,, votingToken,,,) = builder.withTokenHolder(alice, 10 ether).withTokenHolder(bob, 1 ether) .withTokenHolder(taikoBridge, 1234).build(); assertEq( @@ -496,19 +496,19 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // 2 builder = new DaoBuilder(); - (, optimisticPlugin,,, votingToken,) = builder.withMinVetoRatio(1_000).build(); + (, optimisticPlugin,,, votingToken,,,) = builder.withMinVetoRatio(1_000).build(); assertEq(optimisticPlugin.minVetoRatio(), 1_000, "Incorrect minVetoRatio"); // 3 builder = new DaoBuilder(); - (, optimisticPlugin,,, votingToken,) = builder.withMinVetoRatio(500_000).build(); + (, optimisticPlugin,,, votingToken,,,) = builder.withMinVetoRatio(500_000).build(); assertEq(optimisticPlugin.minVetoRatio(), 500_000, "Incorrect minVetoRatio"); // 4 builder = new DaoBuilder(); - (, optimisticPlugin,,, votingToken,) = builder.withMinVetoRatio(300_000).build(); + (, optimisticPlugin,,, votingToken,,,) = builder.withMinVetoRatio(300_000).build(); assertEq(optimisticPlugin.minVetoRatio(), 300_000, "Incorrect minVetoRatio"); } @@ -552,16 +552,16 @@ contract OptimisticTokenVotingPluginTest is AragonTest { assertEq(optimisticPlugin.isL2Available(), true, "isL2Available should be true"); // skipL2 setting - (, optimisticPlugin,,, votingToken,) = builder.withSkipL2().build(); + (, optimisticPlugin,,, votingToken,,,) = builder.withSkipL2().build(); assertEq(optimisticPlugin.isL2Available(), false, "isL2Available should be false"); builder.withoutSkipL2(); // paused - (, optimisticPlugin,,, votingToken,) = builder.withPausedTaikoL1().build(); + (, optimisticPlugin,,, votingToken,,,) = builder.withPausedTaikoL1().build(); assertEq(optimisticPlugin.isL2Available(), false, "isL2Available should be false"); // out of sync - (, optimisticPlugin,,, votingToken,) = builder.withOutOfSyncTaikoL1().build(); + (, optimisticPlugin,,, votingToken,,,) = builder.withOutOfSyncTaikoL1().build(); assertEq(optimisticPlugin.isL2Available(), false, "isL2Available should be false"); // out of sync: diff below lowerl2InactivityPeriod @@ -570,12 +570,12 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // out of sync: still within the period vm.warp(50 days); - (, optimisticPlugin,,, votingToken,) = builder.withL2InactivityPeriod(50 days).build(); + (, optimisticPlugin,,, votingToken,,,) = builder.withL2InactivityPeriod(50 days).build(); assertEq(optimisticPlugin.isL2Available(), true, "isL2Available should be true"); // out of sync: over vm.warp(50 days + 1); - (, optimisticPlugin,,, votingToken,) = builder.withL2InactivityPeriod(50 days).build(); + (, optimisticPlugin,,, votingToken,,,) = builder.withL2InactivityPeriod(50 days).build(); assertEq(optimisticPlugin.isL2Available(), false, "isL2Available should be false"); } @@ -616,7 +616,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { function test_CreateProposalRevertsIfThereIsNoVotingPowerOnlyL1Tokens() public { // 1 // Paused L2 - (dao, optimisticPlugin,,, votingToken, taikoL1) = + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withPausedTaikoL1().withTokenHolder(alice, 0).withProposerOnOptimistic(alice).build(); IDAO.Action[] memory actions = new IDAO.Action[](0); @@ -631,7 +631,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // 2 // Out of sync L2 - (dao, optimisticPlugin,,, votingToken, taikoL1) = + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withOutOfSyncTaikoL1().withTokenHolder(alice, 0).withProposerOnOptimistic(alice).build(); vm.expectRevert(abi.encodeWithSelector(OptimisticTokenVotingPlugin.NoVotingPower.selector)); @@ -646,7 +646,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // 3 // Taiko Bridge now has voting power (should be ignored) // Paused L2 - (dao, optimisticPlugin,,, votingToken, taikoL1) = builder.withPausedTaikoL1().withTokenHolder( + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withPausedTaikoL1().withTokenHolder( taikoBridge, 10000 ether ).withProposerOnOptimistic(alice).build(); @@ -661,7 +661,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // 4 // Out of sync L2 - (dao, optimisticPlugin,,, votingToken, taikoL1) = builder.withOutOfSyncTaikoL1().withTokenHolder( + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withOutOfSyncTaikoL1().withTokenHolder( taikoBridge, 10000 ether ).withProposerOnOptimistic(alice).build(); @@ -677,7 +677,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { function test_CreateProposalRevertsIfThereIsNoVotingPowerWithL1L2Tokens() public { // 1 - (dao, optimisticPlugin,,, votingToken, taikoL1) = + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withOkTaikoL1().withTokenHolder(alice, 0).withProposerOnOptimistic(alice).build(); // Try to create @@ -692,7 +692,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { optimisticPlugin.createProposal("", actions, 0, 4 days); // 2 - (dao, optimisticPlugin,,, votingToken, taikoL1) = + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withOkTaikoL1().withTokenHolder(taikoBridge, 0).withProposerOnOptimistic(alice).build(); // Try to create @@ -707,7 +707,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { optimisticPlugin.createProposal("", actions, 0, 4 days); // 2 - (dao, optimisticPlugin,,, votingToken, taikoL1) = + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withOkTaikoL1().withTokenHolder(taikoBridge, 0).withProposerOnOptimistic(alice).build(); // Try to create @@ -724,7 +724,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { function test_CreateProposalRevertsIfDurationIsLowerThanMin() public { vm.startPrank(alice); - (dao, optimisticPlugin,,, votingToken, taikoL1) = + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withMinDuration(0).withProposerOnOptimistic(alice).build(); IDAO.Action[] memory actions = new IDAO.Action[](0); @@ -733,7 +733,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { optimisticPlugin.createProposal("", actions, 0, 0); // 2 ko - (dao, optimisticPlugin,,, votingToken, taikoL1) = + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withMinDuration(10 minutes).withProposerOnOptimistic(alice).build(); vm.expectRevert( @@ -745,7 +745,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { optimisticPlugin.createProposal("", actions, 0, 10 minutes); // 4 ko - (dao, optimisticPlugin,,, votingToken, taikoL1) = + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withMinDuration(10 hours).withProposerOnOptimistic(alice).build(); vm.expectRevert( @@ -768,7 +768,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { } function test_CreateProposalStartsDespiteRevertingTaikoL1() public { - (dao, optimisticPlugin,,,,) = builder.withIncompatibleTaikoL1().build(); + (dao, optimisticPlugin,,,,,,) = builder.withIncompatibleTaikoL1().build(); vm.warp(2 days); @@ -783,7 +783,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { vm.warp(50 days - 1); // L2 Paused - (dao, optimisticPlugin,,, votingToken, taikoL1) = builder.withPausedTaikoL1().build(); + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withPausedTaikoL1().build(); IDAO.Action[] memory actions = new IDAO.Action[](0); uint256 proposalId = optimisticPlugin.createProposal("", actions, 0, 10 days); @@ -806,7 +806,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // L2 out of sync vm.warp(50 days - 1); - (dao, optimisticPlugin,,, votingToken, taikoL1) = builder.withOutOfSyncTaikoL1().build(); + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withOutOfSyncTaikoL1().build(); actions = new IDAO.Action[](0); proposalId = optimisticPlugin.createProposal("", actions, 0, 10 days); @@ -829,7 +829,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { function test_CreateProposalEndsAfterMinDurationWithL1L2Tokens() public { // 1 vm.warp(50 days - 1); - (dao, optimisticPlugin,,, votingToken, taikoL1) = builder.build(); + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.build(); IDAO.Action[] memory actions = new IDAO.Action[](0); uint256 proposalId = optimisticPlugin.createProposal("", actions, 0, 10 days); @@ -852,7 +852,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // 2 // With tokens on the Taiko Bridge vm.warp(50 days - 1); - (dao, optimisticPlugin,,, votingToken, taikoL1) = + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withTokenHolder(alice, 10 ether).withTokenHolder(taikoBridge, 10 ether).build(); proposalId = optimisticPlugin.createProposal("", actions, 0, 10 days); @@ -1045,7 +1045,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // 2 - With L2 tokens vm.warp(3 days - 1); - (dao, optimisticPlugin,,, votingToken, taikoL1) = + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withTokenHolder(alice, 10 ether).withTokenHolder(taikoBridge, 10 ether).build(); vetoPeriod = 30 days; @@ -1094,7 +1094,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // 3 with L2 paused vm.warp(3 days - 1); - (dao, optimisticPlugin,,, votingToken, taikoL1) = builder.withPausedTaikoL1().build(); + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withPausedTaikoL1().build(); vetoPeriod = 15 days; actions[0].to = carol; @@ -1141,7 +1141,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // 4 with L2 out of sync vm.warp(3 days - 1); - (dao, optimisticPlugin,,, votingToken, taikoL1) = builder.withOutOfSyncTaikoL1().build(); + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withOutOfSyncTaikoL1().build(); proposalId = optimisticPlugin.createProposal("ipfs://my-uri", actions, failSafeBitmap, vetoPeriod); @@ -1261,7 +1261,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { } function test_CanVetoReturnsFalseForTheBridge() public { - (dao, optimisticPlugin,,, votingToken, taikoL1) = builder.withOkTaikoL1().withTokenHolder(alice, 10 ether) + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withOkTaikoL1().withTokenHolder(alice, 10 ether) .withTokenHolder(bob, 10 ether).withTokenHolder(taikoBridge, 10 ether).build(); IDAO.Action[] memory actions = new IDAO.Action[](0); @@ -1351,7 +1351,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { } function test_VetoRevertsForTheBridge() public { - (dao, optimisticPlugin,,, votingToken, taikoL1) = builder.withOkTaikoL1().withTokenHolder(alice, 10 ether) + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withOkTaikoL1().withTokenHolder(alice, 10 ether) .withTokenHolder(bob, 10 ether).withTokenHolder(taikoBridge, 10 ether).build(); IDAO.Action[] memory actions = new IDAO.Action[](0); @@ -1400,7 +1400,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { optimisticPlugin.veto(proposalId); // 2 - (dao, optimisticPlugin,,, votingToken, taikoL1) = builder.withTokenHolder(alice, 5 ether).withTokenHolder( + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withTokenHolder(alice, 5 ether).withTokenHolder( bob, 10 ether ).withTokenHolder(carol, 15 ether).build(); @@ -1423,7 +1423,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // Has vetoed function test_HasVetoedReturnsTheRightValues() public { - (dao, optimisticPlugin,,, votingToken, taikoL1) = builder.withTokenHolder(alice, 5 ether).withTokenHolder( + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withTokenHolder(alice, 5 ether).withTokenHolder( bob, 10 ether ).withTokenHolder(carol, 15 ether).build(); @@ -1491,7 +1491,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // 2 vetoes required when L1 only // 3 vetoes required when L1+L2 - (, optimisticPlugin,,, votingToken,) = builder.withTokenHolder(alice, 10 ether).withTokenHolder(bob, 10 ether) + (, optimisticPlugin,,, votingToken,,,) = builder.withTokenHolder(alice, 10 ether).withTokenHolder(bob, 10 ether) .withTokenHolder(taikoBridge, 10 ether).withMinVetoRatio(700_000).build(); IDAO.Action[] memory actions = new IDAO.Action[](0); @@ -1510,7 +1510,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // L2 paused, less token supply // Alice and Bob are now 100% - (, optimisticPlugin,,, votingToken,) = builder.withPausedTaikoL1().build(); + (, optimisticPlugin,,, votingToken,,,) = builder.withPausedTaikoL1().build(); proposalId = optimisticPlugin.createProposal("ipfs://", actions, 0, 4 days); @@ -1526,7 +1526,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // L2 out of sync, less token supply // Alice and Bob are now 100% - (, optimisticPlugin,,, votingToken,) = builder.withOutOfSyncTaikoL1().build(); + (, optimisticPlugin,,, votingToken,,,) = builder.withOutOfSyncTaikoL1().build(); proposalId = optimisticPlugin.createProposal("ipfs://", actions, 0, 4 days); @@ -1556,7 +1556,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { function test_CanExecuteReturnsFalseWhenEndedButL2GracePeriodUnmet() public { // An ended proposal with L2 enabled has an additional grace period - (, optimisticPlugin,,,,) = + (, optimisticPlugin,,,,,,) = builder.withTokenHolder(alice, 10 ether).withTokenHolder(taikoBridge, 10 ether).build(); IDAO.Action[] memory actions = new IDAO.Action[](0); @@ -1617,7 +1617,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // 2 vetoes required when L1 only // 3 vetoes required when L1+L2 - (, optimisticPlugin,,, votingToken,) = builder.withTokenHolder(alice, 10 ether).withTokenHolder(bob, 10 ether) + (, optimisticPlugin,,, votingToken,,,) = builder.withTokenHolder(alice, 10 ether).withTokenHolder(bob, 10 ether) .withTokenHolder(taikoBridge, 10 ether).withMinVetoRatio(700_000).build(); IDAO.Action[] memory actions = new IDAO.Action[](0); @@ -1640,7 +1640,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // L2 paused, less token supply // Alice and Bob are now 100% - (, optimisticPlugin,,, votingToken,) = builder.withPausedTaikoL1().build(); + (, optimisticPlugin,,, votingToken,,,) = builder.withPausedTaikoL1().build(); proposalId = optimisticPlugin.createProposal("ipfs://", actions, 0, 4 days); @@ -1660,7 +1660,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // L2 out of sync, less token supply // Alice and Bob are now 100% - (, optimisticPlugin,,, votingToken,) = builder.withOutOfSyncTaikoL1().build(); + (, optimisticPlugin,,, votingToken,,,) = builder.withOutOfSyncTaikoL1().build(); proposalId = optimisticPlugin.createProposal("ipfs://", actions, 0, 4 days); @@ -1681,7 +1681,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { function test_CanExecuteReturnsTrueWhenSkipL2AndFullyEnded() public { // An ended proposal with L2 skipped - (, optimisticPlugin,,,,) = + (, optimisticPlugin,,,,,,) = builder.withTokenHolder(alice, 10 ether).withTokenHolder(taikoBridge, 10 ether).withSkipL2().build(); IDAO.Action[] memory actions = new IDAO.Action[](0); @@ -1723,7 +1723,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { } function test_CanExecuteReturnsTrueOnDurationZero_WithoutL2GracePeriodOrExitWindow() public { - (, optimisticPlugin,,,,) = builder.withMinDuration(0).build(); + (, optimisticPlugin,,,,,,) = builder.withMinDuration(0).build(); IDAO.Action[] memory actions = new IDAO.Action[](0); uint256 proposalId = optimisticPlugin.createProposal("ipfs://", actions, 0, 0 days); @@ -1746,7 +1746,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { assertEq(optimisticPlugin.canExecute(proposalId), false, "The proposal shouldn't be executable yet"); - vm.warp(block.timestamp + 4 days - 1) ; + vm.warp(block.timestamp + 4 days - 1); assertEq(optimisticPlugin.canExecute(proposalId), false, "The proposal shouldn't be executable yet"); // Ended @@ -1754,7 +1754,8 @@ contract OptimisticTokenVotingPluginTest is AragonTest { assertEq(optimisticPlugin.canExecute(proposalId), false, "The proposal shouldn't be executable yet"); // No L2 votes available, so no L2 aggregation period - (,, OptimisticTokenVotingPlugin.ProposalParameters memory parameters,,,,) = optimisticPlugin.getProposal(proposalId); + (,, OptimisticTokenVotingPlugin.ProposalParameters memory parameters,,,,) = + optimisticPlugin.getProposal(proposalId); assertEq(parameters.unavailableL2, true, "unavailableL2 should be true"); vm.warp(block.timestamp + optimisticPlugin.EXIT_WINDOW() - 1); @@ -1767,7 +1768,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // Veto threshold reached function test_IsMinVetoRatioReachedReturnsTheAppropriateValuesOnlyL1Tokens() public { - (dao, optimisticPlugin,,, votingToken, taikoL1) = builder.withMinVetoRatio(250_000).withTokenHolder( + (dao, optimisticPlugin,,, votingToken,,, taikoL1) = builder.withMinVetoRatio(250_000).withTokenHolder( alice, 24 ether ).withTokenHolder(bob, 1 ether).withTokenHolder(randomWallet, 75 ether).build(); @@ -1798,7 +1799,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { function test_IsMinVetoRatioReachedReturnsTheAppropriateValuesWithL1L2Tokens() public { // 200/300 vs 200/400 scenario builder = new DaoBuilder(); - (, optimisticPlugin,,,,) = builder.withOkTaikoL1().withMinVetoRatio(510_000).withTokenHolder(alice, 100 ether) + (, optimisticPlugin,,,,,,) = builder.withOkTaikoL1().withMinVetoRatio(510_000).withTokenHolder(alice, 100 ether) .withTokenHolder(bob, 100 ether).withTokenHolder(carol, 100 ether).withTokenHolder(taikoBridge, 100 ether).build( ); @@ -1822,7 +1823,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // L2 paused vm.startPrank(alice); - (, optimisticPlugin,,,,) = builder.withPausedTaikoL1().build(); + (, optimisticPlugin,,,,,,) = builder.withPausedTaikoL1().build(); proposalId = optimisticPlugin.createProposal("ipfs://", actions, 0, 4 days); (,, parameters,,,,) = optimisticPlugin.getProposal(proposalId); @@ -1843,7 +1844,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // L2 out of sync vm.startPrank(alice); - (, optimisticPlugin,,,,) = builder.withOutOfSyncTaikoL1().build(); + (, optimisticPlugin,,,,,,) = builder.withOutOfSyncTaikoL1().build(); proposalId = optimisticPlugin.createProposal("ipfs://", actions, 0, 4 days); (,, parameters,,,,) = optimisticPlugin.getProposal(proposalId); @@ -1961,7 +1962,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // 2 vetoes required when L1 only // 3 vetoes required when L1+L2 - (, optimisticPlugin,,,,) = builder.withTokenHolder(alice, 10 ether).withTokenHolder(bob, 10 ether) + (, optimisticPlugin,,,,,,) = builder.withTokenHolder(alice, 10 ether).withTokenHolder(bob, 10 ether) .withTokenHolder(taikoBridge, 10 ether).withMinVetoRatio(700_000).build(); IDAO.Action[] memory actions = new IDAO.Action[](0); @@ -1989,7 +1990,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // L2 paused, less token supply // Alice and Bob are now 100% - (, optimisticPlugin,,,,) = builder.withPausedTaikoL1().build(); + (, optimisticPlugin,,,,,,) = builder.withPausedTaikoL1().build(); proposalId = optimisticPlugin.createProposal("ipfs://", actions, 0, 4 days); @@ -2016,7 +2017,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { // L2 out of sync, less token supply // Alice and Bob are now 100% - (, optimisticPlugin,,,,) = builder.withOutOfSyncTaikoL1().build(); + (, optimisticPlugin,,,,,,) = builder.withOutOfSyncTaikoL1().build(); proposalId = optimisticPlugin.createProposal("ipfs://", actions, 0, 4 days); @@ -2115,7 +2116,7 @@ contract OptimisticTokenVotingPluginTest is AragonTest { function test_ExecuteRevertsWhenEndedBeforeGracePeriodOrExitWindow_L1L2Tokens() public { // An ended proposal with L2 enabled has an additional grace period - (, optimisticPlugin,,,,) = + (, optimisticPlugin,,,,,,) = builder.withTokenHolder(alice, 10 ether).withTokenHolder(taikoBridge, 10 ether).build(); IDAO.Action[] memory actions = new IDAO.Action[](0); diff --git a/test/PublicKeyRegistry.t.sol b/test/PublicKeyRegistry.t.sol deleted file mode 100644 index fcea597..0000000 --- a/test/PublicKeyRegistry.t.sol +++ /dev/null @@ -1,233 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.17; - -import {AragonTest} from "./base/AragonTest.sol"; -import {Addresslist} from "@aragon/osx/plugins/utils/Addresslist.sol"; -import {PublicKeyRegistry} from "../src/PublicKeyRegistry.sol"; -import {DaoBuilder} from "./helpers/DaoBuilder.sol"; -import {DAO} from "@aragon/osx/core/dao/DAO.sol"; -import {Multisig} from "../src/Multisig.sol"; - -contract EmergencyMultisigTest is AragonTest { - PublicKeyRegistry registry; - DaoBuilder builder; - Multisig multisig; - - // Events/errors to be tested here (duplicate) - event PublicKeyRegistered(address wallet, bytes32 publicKey); - - function setUp() public { - builder = new DaoBuilder(); - (,, multisig,,,) = builder.withMultisigMember(alice).withMultisigMember(bob).withMultisigMember(carol) - .withMultisigMember(david).build(); - - registry = new PublicKeyRegistry(multisig); - } - - function test_ShouldRegisterAPublicKey() public { - assertEq(registry.publicKeys(alice), 0x0000000000000000000000000000000000000000000000000000000000000000); - - // Alice - vm.startPrank(alice); - registry.setPublicKey(0x1234000000000000000000000000000000000000000000000000000000000000); - - assertEq(registry.publicKeys(alice), 0x1234000000000000000000000000000000000000000000000000000000000000); - - // Bob - vm.startPrank(bob); - registry.setPublicKey(0x0000567800000000000000000000000000000000000000000000000000000000); - - assertEq(registry.publicKeys(alice), 0x1234000000000000000000000000000000000000000000000000000000000000); - assertEq(registry.publicKeys(bob), 0x0000567800000000000000000000000000000000000000000000000000000000); - - // Carol - vm.startPrank(carol); - registry.setPublicKey(0x0000000090ab0000000000000000000000000000000000000000000000000000); - - assertEq(registry.publicKeys(alice), 0x1234000000000000000000000000000000000000000000000000000000000000); - assertEq(registry.publicKeys(bob), 0x0000567800000000000000000000000000000000000000000000000000000000); - assertEq(registry.publicKeys(carol), 0x0000000090ab0000000000000000000000000000000000000000000000000000); - - // David - vm.startPrank(david); - registry.setPublicKey(0x000000000000cdef000000000000000000000000000000000000000000000000); - - assertEq(registry.publicKeys(alice), 0x1234000000000000000000000000000000000000000000000000000000000000); - assertEq(registry.publicKeys(bob), 0x0000567800000000000000000000000000000000000000000000000000000000); - assertEq(registry.publicKeys(carol), 0x0000000090ab0000000000000000000000000000000000000000000000000000); - assertEq(registry.publicKeys(david), 0x000000000000cdef000000000000000000000000000000000000000000000000); - } - - function test_ShouldRevertIfNotASigner() public { - (,, multisig,,,) = new DaoBuilder().withMultisigMember(alice).build(); - - registry = new PublicKeyRegistry(multisig); - - // OK - assertEq(registry.publicKeys(alice), 0x0000000000000000000000000000000000000000000000000000000000000000); - - // Alice - vm.startPrank(alice); - assertEq(multisig.isMember(alice), true); - registry.setPublicKey(0x5678000000000000000000000000000000000000000000000000000000000000); - - assertEq(registry.publicKeys(alice), 0x5678000000000000000000000000000000000000000000000000000000000000); - - // NOT OK - - // Bob - vm.startPrank(bob); - vm.expectRevert(abi.encodeWithSelector(PublicKeyRegistry.RegistrationForbidden.selector)); - registry.setPublicKey(0x0000567800000000000000000000000000000000000000000000000000000000); - - assertEq(registry.publicKeys(alice), 0x5678000000000000000000000000000000000000000000000000000000000000); - assertEq(registry.publicKeys(bob), 0x0000000000000000000000000000000000000000000000000000000000000000); - - // Carol - vm.startPrank(carol); - vm.expectRevert(abi.encodeWithSelector(PublicKeyRegistry.RegistrationForbidden.selector)); - registry.setPublicKey(0x0000000090ab0000000000000000000000000000000000000000000000000000); - - assertEq(registry.publicKeys(alice), 0x5678000000000000000000000000000000000000000000000000000000000000); - assertEq(registry.publicKeys(bob), 0x0000000000000000000000000000000000000000000000000000000000000000); - assertEq(registry.publicKeys(carol), 0x0000000000000000000000000000000000000000000000000000000000000000); - - // David - vm.startPrank(david); - vm.expectRevert(abi.encodeWithSelector(PublicKeyRegistry.RegistrationForbidden.selector)); - registry.setPublicKey(0x000000000000cdef000000000000000000000000000000000000000000000000); - - assertEq(registry.publicKeys(alice), 0x5678000000000000000000000000000000000000000000000000000000000000); - assertEq(registry.publicKeys(bob), 0x0000000000000000000000000000000000000000000000000000000000000000); - assertEq(registry.publicKeys(carol), 0x0000000000000000000000000000000000000000000000000000000000000000); - assertEq(registry.publicKeys(david), 0x0000000000000000000000000000000000000000000000000000000000000000); - } - - function test_ShouldEmitARegistrationEvent() public { - vm.startPrank(alice); - vm.expectEmit(); - emit PublicKeyRegistered(alice, 0x000000000000cdef000000000000000000000000000000000000000000000000); - registry.setPublicKey(0x000000000000cdef000000000000000000000000000000000000000000000000); - - vm.startPrank(bob); - vm.expectEmit(); - emit PublicKeyRegistered(bob, 0x0000000090ab0000000000000000000000000000000000000000000000000000); - registry.setPublicKey(0x0000000090ab0000000000000000000000000000000000000000000000000000); - - vm.startPrank(carol); - vm.expectEmit(); - emit PublicKeyRegistered(carol, 0x0000567800000000000000000000000000000000000000000000000000000000); - registry.setPublicKey(0x0000567800000000000000000000000000000000000000000000000000000000); - - vm.startPrank(david); - vm.expectEmit(); - emit PublicKeyRegistered(david, 0x1234000000000000000000000000000000000000000000000000000000000000); - registry.setPublicKey(0x1234000000000000000000000000000000000000000000000000000000000000); - } - - function test_ShouldRevertIfReRegistering() public { - vm.startPrank(alice); - registry.setPublicKey(0x000000000000cdef000000000000000000000000000000000000000000000000); - vm.expectRevert(abi.encodeWithSelector(PublicKeyRegistry.AlreadySet.selector)); - registry.setPublicKey(0x000000000000cdef000000000000000000000000000000000000000000000000); - vm.expectRevert(abi.encodeWithSelector(PublicKeyRegistry.AlreadySet.selector)); - registry.setPublicKey(0x1234000000000000000000000000000000000000000000000000000000000000); - - vm.startPrank(bob); - registry.setPublicKey(0x1234000000000000000000000000000000000000000000000000000000000000); - vm.expectRevert(abi.encodeWithSelector(PublicKeyRegistry.AlreadySet.selector)); - registry.setPublicKey(0x000000000000cdef000000000000000000000000000000000000000000000000); - vm.expectRevert(abi.encodeWithSelector(PublicKeyRegistry.AlreadySet.selector)); - registry.setPublicKey(0x1234000000000000000000000000000000000000000000000000000000000000); - - vm.startPrank(carol); - registry.setPublicKey(0x0000567800000000000000000000000000000000000000000000000000000000); - vm.expectRevert(abi.encodeWithSelector(PublicKeyRegistry.AlreadySet.selector)); - registry.setPublicKey(0x0000000090ab0000000000000000000000000000000000000000000000000000); - vm.expectRevert(abi.encodeWithSelector(PublicKeyRegistry.AlreadySet.selector)); - registry.setPublicKey(0x1234000000000000000000000000000000000000000000000000000000000000); - - vm.startPrank(david); - registry.setPublicKey(0x0000000090ab0000000000000000000000000000000000000000000000000000); - vm.expectRevert(abi.encodeWithSelector(PublicKeyRegistry.AlreadySet.selector)); - registry.setPublicKey(0x0000567800000000000000000000000000000000000000000000000000000000); - vm.expectRevert(abi.encodeWithSelector(PublicKeyRegistry.AlreadySet.selector)); - registry.setPublicKey(0x000000000000cdef000000000000000000000000000000000000000000000000); - } - - function test_ShouldCountRegisteredCandidates() public { - assertEq(registry.registeredWalletCount(), 0, "Incorrect count"); - - // Alice - vm.startPrank(alice); - registry.setPublicKey(bytes32(uint256(1234))); - assertEq(registry.registeredWalletCount(), 1, "Incorrect count"); - - // Bob - vm.startPrank(bob); - registry.setPublicKey(bytes32(uint256(2345))); - assertEq(registry.registeredWalletCount(), 2, "Incorrect count"); - - // Carol - vm.startPrank(carol); - registry.setPublicKey(bytes32(uint256(3456))); - assertEq(registry.registeredWalletCount(), 3, "Incorrect count"); - - // David - vm.startPrank(david); - registry.setPublicKey(bytes32(uint256(4567))); - assertEq(registry.registeredWalletCount(), 4, "Incorrect count"); - } - - function test_ShouldEnumerateRegisteredCandidates() public { - // Register - vm.startPrank(alice); - registry.setPublicKey(bytes32(uint256(1234))); - vm.startPrank(bob); - registry.setPublicKey(bytes32(uint256(2345))); - vm.startPrank(carol); - registry.setPublicKey(bytes32(uint256(3456))); - vm.startPrank(david); - registry.setPublicKey(bytes32(uint256(4567))); - - assertEq(registry.registeredWalletCount(), 4, "Incorrect count"); - - assertEq(registry.registeredWallets(0), alice); - assertEq(registry.registeredWallets(1), bob); - assertEq(registry.registeredWallets(2), carol); - assertEq(registry.registeredWallets(3), david); - } - - function test_ShouldLoadTheRegisteredAddresses() public { - vm.startPrank(alice); - registry.setPublicKey(bytes32(uint256(1234))); - vm.startPrank(bob); - registry.setPublicKey(bytes32(uint256(2345))); - vm.startPrank(carol); - registry.setPublicKey(bytes32(uint256(3456))); - vm.startPrank(david); - registry.setPublicKey(bytes32(uint256(4567))); - - address[] memory candidates = registry.getRegisteredWallets(); - assertEq(candidates.length, 4); - assertEq(candidates[0], alice); - assertEq(candidates[1], bob); - assertEq(candidates[2], carol); - assertEq(candidates[3], david); - } - - function test_TheConstructorShouldRevertIfInvalidAddressList() public { - // Fail - vm.expectRevert(abi.encodeWithSelector(PublicKeyRegistry.InvalidAddressList.selector)); - new PublicKeyRegistry(Addresslist(address(this))); - - // OK - (,, multisig,,,) = new DaoBuilder().withMultisigMember(alice).build(); - new PublicKeyRegistry(multisig); - } - - /// @dev mock function for test_TheConstructorShouldRevertIfInvalidAddressList() - function supportsInterface(bytes4) public pure returns (bool) { - return false; - } -} diff --git a/test/SignerList.t.sol b/test/SignerList.t.sol new file mode 100644 index 0000000..e0b7e53 --- /dev/null +++ b/test/SignerList.t.sol @@ -0,0 +1,1198 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.17; + +import {AragonTest} from "./base/AragonTest.sol"; +import {Addresslist} from "@aragon/osx/plugins/utils/Addresslist.sol"; +import {EncryptionRegistry} from "../src/EncryptionRegistry.sol"; +import { + SignerList, + ISignerList, + UPDATE_SIGNER_LIST_PERMISSION_ID, + UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID +} from "../src/SignerList.sol"; +import {DaoBuilder} from "./helpers/DaoBuilder.sol"; +import {DAO} from "@aragon/osx/core/dao/DAO.sol"; +import {Multisig} from "../src/Multisig.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {IERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol"; +import {createProxyAndCall} from "../src/helpers/proxy.sol"; + +contract SignerListTest is AragonTest { + SignerList signerList; + EncryptionRegistry encryptionRegistry; + DaoBuilder builder; + DAO dao; + Multisig multisig; + address[] signers; + + address immutable SIGNER_LIST_BASE = address(new SignerList()); + + // Events/errors to be tested here (duplicate) + error SignerListLengthOutOfBounds(uint16 limit, uint256 actual); + error InvalidEncryptionRegitry(address givenAddress); + error DaoUnauthorized(address dao, address where, address who, bytes32 permissionId); + error InvalidAddresslistUpdate(address member); + + event SignerListSettingsUpdated(EncryptionRegistry encryptionRegistry, uint16 minSignerListLength); + event SignersAdded(address[] signers); + event SignersRemoved(address[] signers); + + function setUp() public { + vm.startPrank(alice); + + builder = new DaoBuilder(); + (dao,, multisig,,, signerList, encryptionRegistry,) = builder.withMultisigMember(alice).withMultisigMember(bob) + .withMultisigMember(carol).withMultisigMember(david).build(); + + vm.roll(block.number + 1); + + signers = new address[](4); + signers[0] = alice; + signers[1] = bob; + signers[2] = carol; + signers[3] = david; + } + + function test_WhenDeployingTheContract() external { + // It should initialize normally + signerList = SignerList( + createProxyAndCall(address(SIGNER_LIST_BASE), abi.encodeCall(SignerList.initialize, (dao, signers))) + ); + } + + function test_GivenADeployedContract() external { + // It should refuse to initialize again + signerList = SignerList( + createProxyAndCall(address(SIGNER_LIST_BASE), abi.encodeCall(SignerList.initialize, (dao, signers))) + ); + + vm.expectRevert(bytes("Initializable: contract is already initialized")); + signerList.initialize(dao, signers); + } + + modifier givenANewInstance() { + _; + } + + modifier givenCallingInitialize() { + _; + } + + function test_GivenCallingInitialize() external givenANewInstance givenCallingInitialize { + signerList = SignerList( + createProxyAndCall(address(SIGNER_LIST_BASE), abi.encodeCall(SignerList.initialize, (dao, signers))) + ); + + // It should set the DAO address + vm.assertEq(address(signerList.dao()), address(dao), "Incorrect DAO addres"); + + (EncryptionRegistry reg, uint16 minSignerListLength) = signerList.settings(); + + // It should append the new addresses to the list + // It should return true on isListed + vm.assertEq(signerList.addresslistLength(), 4, "Incorrect length"); + vm.assertEq(signerList.isListed(alice), true, "Should be a signer"); + vm.assertEq(signerList.isListed(bob), true, "Should be a signer"); + vm.assertEq(signerList.isListed(carol), true, "Should be a signer"); + vm.assertEq(signerList.isListed(david), true, "Should be a signer"); + vm.assertEq(signerList.isListed(address(100)), false, "Should not be a signer"); + vm.assertEq(signerList.isListed(address(200)), false, "Should not be a signer"); + + // It the encryption registry should be empty + vm.assertEq(address(reg), address(0), "Incorrect address"); + + // It minSignerListLength should be zero + vm.assertEq(minSignerListLength, 0); + + // It should emit the SignersAdded event + vm.expectEmit(); + emit SignersAdded({signers: signers}); + signerList = SignerList( + createProxyAndCall(address(SIGNER_LIST_BASE), abi.encodeCall(SignerList.initialize, (dao, signers))) + ); + + // It should set the right values in general + + // 2 + signerList = SignerList( + createProxyAndCall(address(SIGNER_LIST_BASE), abi.encodeCall(SignerList.initialize, (dao, signers))) + ); + + (reg, minSignerListLength) = signerList.settings(); + vm.assertEq(address(reg), address(0), "Incorrect address"); + vm.assertEq(minSignerListLength, 0); + vm.assertEq(signerList.addresslistLength(), 4, "Incorrect length"); + vm.assertEq(signerList.isListed(alice), true, "Should be a signer"); + vm.assertEq(signerList.isListed(bob), true, "Should be a signer"); + vm.assertEq(signerList.isListed(carol), true, "Should be a signer"); + vm.assertEq(signerList.isListed(david), true, "Should be a signer"); + vm.assertEq(signerList.isListed(address(100)), false, "Should not be a signer"); + vm.assertEq(signerList.isListed(address(200)), false, "Should not be a signer"); + + // 3 + signers = new address[](2); + signers[0] = address(100); + signers[1] = address(200); + + // It should emit the SignersAdded event + vm.expectEmit(); + emit SignersAdded({signers: signers}); + signerList = SignerList( + createProxyAndCall(address(SIGNER_LIST_BASE), abi.encodeCall(SignerList.initialize, (dao, signers))) + ); + + (reg, minSignerListLength) = signerList.settings(); + vm.assertEq(address(reg), address(0), "Incorrect address"); + vm.assertEq(minSignerListLength, 0); + vm.assertEq(signerList.addresslistLength(), 2, "Incorrect length"); + vm.assertEq(signerList.isListed(alice), false, "Should not be a signer"); + vm.assertEq(signerList.isListed(bob), false, "Should not be a signer"); + vm.assertEq(signerList.isListed(carol), false, "Should not be a signer"); + vm.assertEq(signerList.isListed(david), false, "Should not be a signer"); + vm.assertEq(signerList.isListed(address(100)), true, "Should be a signer"); + vm.assertEq(signerList.isListed(address(200)), true, "Should be a signer"); + } + + function test_RevertGiven_PassingMoreAddressesThanSupportedOnInitialize() + external + givenANewInstance + givenCallingInitialize + { + // It should revert + + // 1 + signers = new address[](uint256(type(uint16).max) + 1); + vm.expectRevert( + abi.encodeWithSelector( + SignerListLengthOutOfBounds.selector, type(uint16).max, uint256(type(uint16).max) + 1 + ) + ); + signerList = SignerList( + createProxyAndCall(address(SIGNER_LIST_BASE), abi.encodeCall(SignerList.initialize, (dao, signers))) + ); + + // 2 + signers = new address[](uint256(type(uint16).max) + 10); + vm.expectRevert( + abi.encodeWithSelector( + SignerListLengthOutOfBounds.selector, type(uint16).max, uint256(type(uint16).max) + 10 + ) + ); + signerList = SignerList( + createProxyAndCall(address(SIGNER_LIST_BASE), abi.encodeCall(SignerList.initialize, (dao, signers))) + ); + } + + function test_RevertGiven_DuplicateAddressesOnInitialize() external givenANewInstance givenCallingInitialize { + // It should revert + + // 1 + signers[2] = signers[1]; + + vm.expectRevert(abi.encodeWithSelector(InvalidAddresslistUpdate.selector, signers[2])); + signerList = SignerList( + createProxyAndCall(address(SIGNER_LIST_BASE), abi.encodeCall(SignerList.initialize, (dao, signers))) + ); + } + + modifier whenCallingUpdateSettings() { + // Initialize + signerList = SignerList( + createProxyAndCall(address(SIGNER_LIST_BASE), abi.encodeCall(SignerList.initialize, (dao, signers))) + ); + + // Grant update permission to Alice + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + + _; + } + + function test_WhenCallingUpdateSettings() external whenCallingUpdateSettings { + encryptionRegistry = new EncryptionRegistry(signerList); + + // 1 + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 0)); + + (EncryptionRegistry reg, uint16 minSignerListLength) = signerList.settings(); + + // It sets the new encryption registry + vm.assertEq(address(reg), address(encryptionRegistry), "Incorrect encryptionRegistry"); + + // It sets the new minSignerListLength + vm.assertEq(minSignerListLength, 0); + + // It should emit a SignerListSettingsUpdated event + vm.expectEmit(); + emit SignerListSettingsUpdated({encryptionRegistry: encryptionRegistry, minSignerListLength: 0}); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 0)); + + // 2 + encryptionRegistry = new EncryptionRegistry(signerList); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 3)); + + (reg, minSignerListLength) = signerList.settings(); + + // It sets the new encryption registry + vm.assertEq(address(reg), address(encryptionRegistry), "Incorrect encryptionRegistry"); + + // It sets the new minSignerListLength + vm.assertEq(minSignerListLength, 3); + + // It should emit a SignerListSettingsUpdated event + vm.expectEmit(); + emit SignerListSettingsUpdated({encryptionRegistry: encryptionRegistry, minSignerListLength: 4}); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 4)); + } + + function test_RevertWhen_UpdateSettingsWithoutThePermission() external whenCallingUpdateSettings { + // It should revert + + vm.startPrank(bob); + vm.expectRevert( + abi.encodeWithSelector( + DaoUnauthorized.selector, + address(dao), + address(signerList), + bob, + UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID + ) + ); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 0)); + } + + function test_RevertWhen_EncryptionRegistryIsNotCompatible() external whenCallingUpdateSettings { + // It should revert + + vm.expectRevert(abi.encodeWithSelector(InvalidEncryptionRegitry.selector, address(dao))); + signerList.updateSettings(SignerList.Settings(EncryptionRegistry(address(dao)), 0)); + + vm.expectRevert(); + signerList.updateSettings(SignerList.Settings(EncryptionRegistry(bob), 0)); + } + + function test_RevertWhen_MinSignerListLengthIsBiggerThanTheListSize() external whenCallingUpdateSettings { + // It should revert + + // 1 + vm.expectRevert(abi.encodeWithSelector(SignerListLengthOutOfBounds.selector, 4, 15)); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 15)); + + // 2 + vm.expectRevert(abi.encodeWithSelector(SignerListLengthOutOfBounds.selector, 4, 20)); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 20)); + + // 3 + signers = new address[](1); + signerList.addSigners(signers); + vm.expectRevert(abi.encodeWithSelector(SignerListLengthOutOfBounds.selector, 5, 50)); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 50)); + } + + function test_WhenCallingSupportsInterface() external view { + // It does not support the empty interface + bool supported = signerList.supportsInterface(bytes4(0xffffffff)); + assertEq(supported, false, "Should not support the empty interface"); + + // It supports IERC165Upgradeable + supported = signerList.supportsInterface(type(IERC165Upgradeable).interfaceId); + assertEq(supported, true, "Should support IERC165Upgradeable"); + + // It supports Addresslist + supported = signerList.supportsInterface(type(Addresslist).interfaceId); + assertEq(supported, true, "Should support Addresslist"); + + // It supports ISignerList + supported = signerList.supportsInterface(type(ISignerList).interfaceId); + assertEq(supported, true, "Should support ISignerList"); + } + + modifier whenCallingAddSigners() { + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + + _; + } + + function test_WhenCallingAddSigners() external whenCallingAddSigners { + // It should append the new addresses to the list + // It should return true on isListed + + // 0 + vm.assertEq(signerList.addresslistLength(), 4, "Incorrect length"); + vm.assertEq(signerList.isListed(alice), true, "Should be a signer"); + vm.assertEq(signerList.isListed(bob), true, "Should be a signer"); + vm.assertEq(signerList.isListed(carol), true, "Should be a signer"); + vm.assertEq(signerList.isListed(david), true, "Should be a signer"); + vm.assertEq(signerList.isListed(address(100)), false, "Should not be a signer"); + vm.assertEq(signerList.isListed(address(200)), false, "Should not be a signer"); + + // 1 + address[] memory newSigners = new address[](1); + newSigners[0] = address(100); + signerList.addSigners(newSigners); + + vm.assertEq(signerList.addresslistLength(), 5, "Incorrect length"); + vm.assertEq(signerList.isListed(alice), true, "Should be a signer"); + vm.assertEq(signerList.isListed(bob), true, "Should be a signer"); + vm.assertEq(signerList.isListed(carol), true, "Should be a signer"); + vm.assertEq(signerList.isListed(david), true, "Should be a signer"); + vm.assertEq(signerList.isListed(address(100)), true, "Should be a signer"); + vm.assertEq(signerList.isListed(address(200)), false, "Should not be a signer"); + + // 2 + newSigners[0] = address(200); + signerList.addSigners(newSigners); + + vm.assertEq(signerList.addresslistLength(), 6, "Incorrect length"); + vm.assertEq(signerList.isListed(alice), true, "Should be a signer"); + vm.assertEq(signerList.isListed(bob), true, "Should be a signer"); + vm.assertEq(signerList.isListed(carol), true, "Should be a signer"); + vm.assertEq(signerList.isListed(david), true, "Should be a signer"); + vm.assertEq(signerList.isListed(address(100)), true, "Should be a signer"); + vm.assertEq(signerList.isListed(address(200)), true, "Should be a signer"); + + // It should emit the SignersAdded event + newSigners[0] = address(300); + vm.expectEmit(); + emit SignersAdded({signers: newSigners}); + signerList.addSigners(newSigners); + } + + function test_RevertWhen_AddingWithoutThePermission() external whenCallingAddSigners { + dao.revoke(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + + // It should revert + + address[] memory newSigners = new address[](1); + newSigners[0] = address(100); + + // 1 + vm.expectRevert( + abi.encodeWithSelector( + DaoUnauthorized.selector, address(dao), address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID + ) + ); + signerList.addSigners(newSigners); + + // 2 + vm.startPrank(bob); + vm.expectRevert( + abi.encodeWithSelector( + DaoUnauthorized.selector, address(dao), address(signerList), bob, UPDATE_SIGNER_LIST_PERMISSION_ID + ) + ); + signerList.addSigners(newSigners); + } + + function test_RevertGiven_PassingMoreAddressesThanSupportedOnAddSigners() external whenCallingAddSigners { + // It should revert + + uint256 addedSize = uint256(type(uint16).max); + + // 1 + address[] memory newSigners = new address[](addedSize); + vm.expectRevert(abi.encodeWithSelector(SignerListLengthOutOfBounds.selector, type(uint16).max, addedSize + 4)); + signerList.addSigners(newSigners); + + // 2 + addedSize = uint256(type(uint16).max) + 10; + newSigners = new address[](addedSize); + vm.expectRevert(abi.encodeWithSelector(SignerListLengthOutOfBounds.selector, type(uint16).max, addedSize + 4)); + signerList.addSigners(newSigners); + } + + function test_RevertGiven_DuplicateAddressesOnAddSigners() external whenCallingAddSigners { + // It should revert + + // 1 + address[] memory newSigners = new address[](1); + newSigners[0] = alice; // Alice is a signer already + vm.expectRevert(abi.encodeWithSelector(InvalidAddresslistUpdate.selector, newSigners[0])); + signerList.addSigners(newSigners); + + // 2 + newSigners[0] = bob; // Bob is a signer already + vm.expectRevert(abi.encodeWithSelector(InvalidAddresslistUpdate.selector, newSigners[0])); + signerList.addSigners(newSigners); + + // OK + newSigners[0] = address(1234); + signerList.addSigners(newSigners); + } + + modifier whenCallingRemoveSigners() { + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + + _; + } + + function test_WhenCallingRemoveSigners() external whenCallingRemoveSigners { + // It should remove the given addresses + // It should return false on isListed + + address[] memory newSigners = new address[](3); + newSigners[0] = address(100); + newSigners[1] = address(200); + newSigners[2] = address(300); + signerList.addSigners(newSigners); + + // 0 + vm.assertEq(signerList.addresslistLength(), 7, "Incorrect length"); + vm.assertEq(signerList.isListed(alice), true, "Should be a signer"); + vm.assertEq(signerList.isListed(bob), true, "Should be a signer"); + vm.assertEq(signerList.isListed(carol), true, "Should be a signer"); + vm.assertEq(signerList.isListed(david), true, "Should be a signer"); + vm.assertEq(signerList.isListed(address(100)), true, "Should be a signer"); + vm.assertEq(signerList.isListed(address(200)), true, "Should be a signer"); + + // 1 + address[] memory rmSigners = new address[](1); + rmSigners[0] = david; + signerList.removeSigners(rmSigners); + + vm.assertEq(signerList.addresslistLength(), 6, "Incorrect length"); + vm.assertEq(signerList.isListed(alice), true, "Should be a signer"); + vm.assertEq(signerList.isListed(bob), true, "Should be a signer"); + vm.assertEq(signerList.isListed(carol), true, "Should be a signer"); + vm.assertEq(signerList.isListed(david), false, "Should not be a signer"); + vm.assertEq(signerList.isListed(address(100)), true, "Should be a signer"); + vm.assertEq(signerList.isListed(address(200)), true, "Should be a signer"); + + // 2 + rmSigners[0] = carol; + signerList.removeSigners(rmSigners); + + vm.assertEq(signerList.addresslistLength(), 5, "Incorrect length"); + vm.assertEq(signerList.isListed(alice), true, "Should be a signer"); + vm.assertEq(signerList.isListed(bob), true, "Should be a signer"); + vm.assertEq(signerList.isListed(carol), false, "Should not be a signer"); + vm.assertEq(signerList.isListed(david), false, "Should not be a signer"); + vm.assertEq(signerList.isListed(address(100)), true, "Should be a signer"); + vm.assertEq(signerList.isListed(address(200)), true, "Should be a signer"); + + // It should emit the SignersRemoved event + rmSigners[0] = bob; + vm.expectEmit(); + emit SignersRemoved({signers: rmSigners}); + signerList.removeSigners(rmSigners); + } + + function test_RevertWhen_RemovingWithoutThePermission() external whenCallingRemoveSigners { + address[] memory newSigners = new address[](3); + newSigners[0] = address(100); + newSigners[1] = address(200); + newSigners[2] = address(300); + signerList.addSigners(newSigners); + + dao.revoke(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + + address[] memory rmSigners = new address[](2); + rmSigners[0] = david; + + // It should revert + + // 1 + vm.expectRevert( + abi.encodeWithSelector( + DaoUnauthorized.selector, address(dao), address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID + ) + ); + signerList.removeSigners(newSigners); + + // 2 + vm.startPrank(bob); + vm.expectRevert( + abi.encodeWithSelector( + DaoUnauthorized.selector, address(dao), address(signerList), bob, UPDATE_SIGNER_LIST_PERMISSION_ID + ) + ); + signerList.removeSigners(newSigners); + } + + function test_RevertWhen_RemovingAnUnlistedAddress() external whenCallingRemoveSigners { + address[] memory newSigners = new address[](1); + newSigners[0] = address(100); + signerList.addSigners(newSigners); + + // It should revert + + // 1 + address[] memory rmSigners = new address[](1); + rmSigners[0] = address(200); + vm.expectRevert(abi.encodeWithSelector(InvalidAddresslistUpdate.selector, rmSigners[0])); + signerList.removeSigners(rmSigners); + + // 2 + rmSigners[0] = address(500); + vm.expectRevert(abi.encodeWithSelector(InvalidAddresslistUpdate.selector, rmSigners[0])); + signerList.removeSigners(rmSigners); + } + + function test_RevertGiven_RemovingTooManyAddresses() external whenCallingRemoveSigners { + address[] memory newSigners = new address[](1); + newSigners[0] = address(100); + signerList.addSigners(newSigners); + + // It should revert + // NOTE: The new list will be smaller than minSignerListLength + + // 1 + address[] memory rmSigners = new address[](2); + rmSigners[0] = david; + rmSigners[1] = carol; + vm.expectRevert(abi.encodeWithSelector(SignerListLengthOutOfBounds.selector, 4, 3)); + signerList.removeSigners(rmSigners); + + // 2 + rmSigners = new address[](3); + rmSigners[0] = david; + rmSigners[1] = carol; + rmSigners[2] = bob; + vm.expectRevert(abi.encodeWithSelector(SignerListLengthOutOfBounds.selector, 4, 2)); + signerList.removeSigners(rmSigners); + + // OK + rmSigners = new address[](1); + rmSigners[0] = david; + signerList.removeSigners(rmSigners); + } + + modifier whenCallingIsListed() { + signerList = SignerList( + createProxyAndCall(address(SIGNER_LIST_BASE), abi.encodeCall(SignerList.initialize, (dao, signers))) + ); + + _; + } + + function test_GivenTheMemberIsListed() external whenCallingIsListed { + // It returns true + + vm.assertEq(signerList.isListed(alice), true, "Should be a signer"); + vm.assertEq(signerList.isListed(bob), true, "Should be a signer"); + vm.assertEq(signerList.isListed(carol), true, "Should be a signer"); + vm.assertEq(signerList.isListed(david), true, "Should be a signer"); + } + + function test_GivenTheMemberIsNotListed() external whenCallingIsListed { + // It returns false + + vm.assertEq(signerList.isListed(address(100)), false, "Should not be a signer"); + vm.assertEq(signerList.isListed(address(200)), false, "Should not be a signer"); + vm.assertEq(signerList.isListed(address(400)), false, "Should not be a signer"); + vm.assertEq(signerList.isListed(address(800)), false, "Should not be a signer"); + vm.assertEq(signerList.isListed(address(1234)), false, "Should not be a signer"); + } + + function testFuzz_GivenTheMemberIsNotListed(address random) external whenCallingIsListed { + if (random == alice || random == bob || random == carol || random == david) return; + + // It returns false + + vm.assertEq(signerList.isListed(random), false, "Should not be a signer"); + } + + modifier whenCallingIsListedAtBlock() { + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + + _; + } + + modifier givenTheMemberWasListed() { + vm.assertEq(signerList.isListed(alice), true, "Should be a signer"); + vm.assertEq(signerList.isListed(bob), true, "Should be a signer"); + vm.assertEq(signerList.isListed(carol), true, "Should be a signer"); + vm.assertEq(signerList.isListed(david), true, "Should be a signer"); + + _; + } + + function test_GivenTheMemberIsNotListedNow() external whenCallingIsListedAtBlock givenTheMemberWasListed { + vm.roll(block.number + 1); + + // Replace the list + address[] memory newSigners = new address[](4); + newSigners[0] = address(0); + newSigners[1] = address(1); + newSigners[2] = address(2); + newSigners[3] = address(3); + signerList.addSigners(newSigners); + address[] memory rmSigners = new address[](4); + rmSigners[0] = alice; + rmSigners[1] = bob; + rmSigners[2] = carol; + rmSigners[3] = david; + signerList.removeSigners(rmSigners); + + // It returns true + vm.assertEq(signerList.isListedAtBlock(alice, block.number - 1), true, "Should be a signer"); + vm.assertEq(signerList.isListedAtBlock(bob, block.number - 1), true, "Should be a signer"); + vm.assertEq(signerList.isListedAtBlock(carol, block.number - 1), true, "Should be a signer"); + vm.assertEq(signerList.isListedAtBlock(david, block.number - 1), true, "Should be a signer"); + } + + function test_GivenTheMemberIsListedNow() external whenCallingIsListedAtBlock givenTheMemberWasListed { + vm.roll(block.number + 1); + + // It returns true + vm.assertEq(signerList.isListedAtBlock(alice, block.number - 1), true, "Should be a signer"); + vm.assertEq(signerList.isListedAtBlock(bob, block.number - 1), true, "Should be a signer"); + vm.assertEq(signerList.isListedAtBlock(carol, block.number - 1), true, "Should be a signer"); + vm.assertEq(signerList.isListedAtBlock(david, block.number - 1), true, "Should be a signer"); + } + + modifier givenTheMemberWasNotListed() { + // Replace the list + address[] memory newSigners = new address[](4); + newSigners[0] = address(0); + newSigners[1] = address(1); + newSigners[2] = address(2); + newSigners[3] = address(3); + signerList.addSigners(newSigners); + address[] memory rmSigners = new address[](4); + rmSigners[0] = alice; + rmSigners[1] = bob; + rmSigners[2] = carol; + rmSigners[3] = david; + signerList.removeSigners(rmSigners); + + // +1 + vm.roll(block.number + 1); + + _; + } + + function test_GivenTheMemberIsDelistedNow() external whenCallingIsListedAtBlock givenTheMemberWasNotListed { + // It returns false + vm.assertEq(signerList.isListedAtBlock(alice, block.number - 1), false, "Should not be a signer"); + vm.assertEq(signerList.isListedAtBlock(bob, block.number - 1), false, "Should not be a signer"); + vm.assertEq(signerList.isListedAtBlock(carol, block.number - 1), false, "Should not be a signer"); + vm.assertEq(signerList.isListedAtBlock(david, block.number - 1), false, "Should not be a signer"); + } + + function test_GivenTheMemberIsEnlistedNow() external whenCallingIsListedAtBlock givenTheMemberWasNotListed { + // Add again + address[] memory newSigners = new address[](4); + newSigners[0] = alice; + newSigners[1] = bob; + newSigners[2] = carol; + newSigners[3] = david; + signerList.addSigners(newSigners); + + // It returns false + vm.assertEq(signerList.isListedAtBlock(alice, block.number - 1), false, "Should not be a signer"); + vm.assertEq(signerList.isListedAtBlock(bob, block.number - 1), false, "Should not be a signer"); + vm.assertEq(signerList.isListedAtBlock(carol, block.number - 1), false, "Should not be a signer"); + vm.assertEq(signerList.isListedAtBlock(david, block.number - 1), false, "Should not be a signer"); + } + + modifier whenCallingIsListedOrAppointedByListed() { + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 2)); + + // Remove Carol and David + address[] memory rmSigners = new address[](2); + rmSigners[0] = carol; + rmSigners[1] = david; + signerList.removeSigners(rmSigners); + + // Alice (owner) appoints david + encryptionRegistry.appointWallet(david); + + // Bob is the owner + + _; + } + + function test_GivenTheCallerIsAListedSigner() external whenCallingIsListedOrAppointedByListed { + // 1 + bool listedOrAppointedByListed = signerList.isListedOrAppointedByListed(alice); + + // It listedOrAppointedByListed should be true + assertEq(listedOrAppointedByListed, true, "listedOrAppointedByListed should be true"); + + // 2 + listedOrAppointedByListed = signerList.isListedOrAppointedByListed(bob); + + // It listedOrAppointedByListed should be true + assertEq(listedOrAppointedByListed, true, "listedOrAppointedByListed should be true"); + } + + function test_GivenTheCallerIsAppointedByASigner() external whenCallingIsListedOrAppointedByListed { + bool listedOrAppointedByListed = signerList.isListedOrAppointedByListed(david); + + // It listedOrAppointedByListed should be true + assertEq(listedOrAppointedByListed, true, "listedOrAppointedByListed should be true"); + } + + function test_GivenTheCallerIsNotListedOrAppointed() external whenCallingIsListedOrAppointedByListed { + // 1 + bool listedOrAppointedByListed = signerList.isListedOrAppointedByListed(carol); + + // It listedOrAppointedByListed should be false + assertEq(listedOrAppointedByListed, false, "listedOrAppointedByListed should be false"); + + // 2 + listedOrAppointedByListed = signerList.isListedOrAppointedByListed(address(1234)); + + // It listedOrAppointedByListed should be false + assertEq(listedOrAppointedByListed, false, "listedOrAppointedByListed should be false"); + } + + modifier whenCallingGetListedEncryptionOwnerAtBlock() { + // Alice (owner) appoints address(0x1234) + encryptionRegistry.appointWallet(address(0x1234)); + + // Bob (owner) appoints address(0x2345) + vm.startPrank(bob); + encryptionRegistry.appointWallet(address(0x2345)); + + vm.startPrank(alice); + + // Carol is owner + // David is owner + + _; + } + + modifier givenTheResolvedOwnerIsListedOnGetListedEncryptionOwnerAtBlock() { + _; + } + + function test_WhenTheGivenAddressIsTheOwner() + external + whenCallingGetListedEncryptionOwnerAtBlock + givenTheResolvedOwnerIsListedOnGetListedEncryptionOwnerAtBlock + { + address resolvedOwner; + + // It should return the given address + resolvedOwner = signerList.getListedEncryptionOwnerAtBlock(alice, block.number - 1); + assertEq(resolvedOwner, alice, "Should be alice"); + + resolvedOwner = signerList.getListedEncryptionOwnerAtBlock(bob, block.number - 1); + assertEq(resolvedOwner, bob, "Should be bob"); + + resolvedOwner = signerList.getListedEncryptionOwnerAtBlock(carol, block.number - 1); + assertEq(resolvedOwner, carol, "Should be carol"); + + resolvedOwner = signerList.getListedEncryptionOwnerAtBlock(david, block.number - 1); + assertEq(resolvedOwner, david, "Should be david"); + } + + function test_WhenTheGivenAddressIsAppointedByTheOwner() + external + whenCallingGetListedEncryptionOwnerAtBlock + givenTheResolvedOwnerIsListedOnGetListedEncryptionOwnerAtBlock + { + address resolvedOwner; + + // It should return the resolved owner + resolvedOwner = signerList.getListedEncryptionOwnerAtBlock(address(0x1234), block.number - 1); + assertEq(resolvedOwner, alice, "Should be alice"); + + resolvedOwner = signerList.getListedEncryptionOwnerAtBlock(address(0x2345), block.number - 1); + assertEq(resolvedOwner, bob, "Should be bob"); + } + + modifier givenTheResolvedOwnerWasListedOnGetListedEncryptionOwnerAtBlock() { + // But not listed now + // Prior appointments are still in place + + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + + address[] memory mvSigners = new address[](4); + mvSigners[0] = address(0x5555); + mvSigners[1] = address(0x6666); + mvSigners[2] = address(0x7777); + mvSigners[3] = address(0x8888); + signerList.addSigners(mvSigners); + + vm.roll(block.number + 1); + + mvSigners[0] = alice; + mvSigners[1] = bob; + mvSigners[2] = carol; + mvSigners[3] = david; + signerList.removeSigners(mvSigners); + + _; + } + + function test_WhenTheGivenAddressIsTheOwner2() + external + whenCallingGetListedEncryptionOwnerAtBlock + givenTheResolvedOwnerWasListedOnGetListedEncryptionOwnerAtBlock + { + address resolvedOwner; + + // It should return the given address + resolvedOwner = signerList.getListedEncryptionOwnerAtBlock(alice, block.number - 1); + assertEq(resolvedOwner, alice, "Should be alice"); + + resolvedOwner = signerList.getListedEncryptionOwnerAtBlock(bob, block.number - 1); + assertEq(resolvedOwner, bob, "Should be bob"); + + resolvedOwner = signerList.getListedEncryptionOwnerAtBlock(carol, block.number - 1); + assertEq(resolvedOwner, carol, "Should be carol"); + + resolvedOwner = signerList.getListedEncryptionOwnerAtBlock(david, block.number - 1); + assertEq(resolvedOwner, david, "Should be david"); + } + + function test_WhenTheGivenAddressIsAppointedByTheOwner2() + external + whenCallingGetListedEncryptionOwnerAtBlock + givenTheResolvedOwnerWasListedOnGetListedEncryptionOwnerAtBlock + { + address resolvedOwner; + + // It should return the resolved owner + resolvedOwner = signerList.getListedEncryptionOwnerAtBlock(address(0x1234), block.number - 1); + assertEq(resolvedOwner, alice, "Should be alice"); + + resolvedOwner = signerList.getListedEncryptionOwnerAtBlock(address(0x2345), block.number - 1); + assertEq(resolvedOwner, bob, "Should be bob"); + } + + function test_GivenTheResolvedOwnerWasNotListedOnGetListedEncryptionOwnerAtBlock() + external + whenCallingGetListedEncryptionOwnerAtBlock + { + address resolvedOwner; + + // It should return a zero value + resolvedOwner = signerList.getListedEncryptionOwnerAtBlock(address(0x3456), block.number - 1); + assertEq(resolvedOwner, address(0), "Should be zero"); + + resolvedOwner = signerList.getListedEncryptionOwnerAtBlock(address(0x4567), block.number - 1); + assertEq(resolvedOwner, address(0), "Should be zero"); + } + + modifier whenCallingResolveEncryptionAccountAtBlock() { + // Alice (owner) appoints address(0x1234) + encryptionRegistry.appointWallet(address(0x1234)); + + // Bob (owner) appoints address(0x2345) + vm.startPrank(bob); + encryptionRegistry.appointWallet(address(0x2345)); + + vm.startPrank(alice); + + // Carol is owner + // David is owner + + _; + } + + modifier givenTheResolvedOwnerIsListedOnResolveEncryptionAccountAtBlock() { + _; + } + + function test_WhenTheGivenAddressIsOwner() + external + whenCallingResolveEncryptionAccountAtBlock + givenTheResolvedOwnerIsListedOnResolveEncryptionAccountAtBlock + { + address resolvedOwner; + address votingWallet; + + // 1 - owner appoints + + // It owner should be the given address + // It votingWallet should be the resolved appointed wallet + (resolvedOwner, votingWallet) = signerList.resolveEncryptionAccountAtBlock(alice, block.number - 1); + assertEq(resolvedOwner, alice, "Should be alice"); + assertEq(votingWallet, address(0x1234), "Should be 0x1234"); + + (resolvedOwner, votingWallet) = signerList.resolveEncryptionAccountAtBlock(bob, block.number - 1); + assertEq(resolvedOwner, bob, "Should be bob"); + assertEq(votingWallet, address(0x2345), "Should be 0x2345"); + + // 2 - No appointed wallet + + // It owner should be the given address + // It votingWallet should be the resolved appointed wallet + (resolvedOwner, votingWallet) = signerList.resolveEncryptionAccountAtBlock(carol, block.number - 1); + assertEq(resolvedOwner, carol, "Should be carol"); + assertEq(votingWallet, carol, "Should be carol"); + + (resolvedOwner, votingWallet) = signerList.resolveEncryptionAccountAtBlock(david, block.number - 1); + assertEq(resolvedOwner, david, "Should be david"); + assertEq(votingWallet, david, "Should be david"); + } + + function test_WhenTheGivenAddressIsAppointed() + external + whenCallingResolveEncryptionAccountAtBlock + givenTheResolvedOwnerIsListedOnResolveEncryptionAccountAtBlock + { + address resolvedOwner; + address votingWallet; + + // It owner should be the resolved owner + // It votingWallet should be the given address + (resolvedOwner, votingWallet) = signerList.resolveEncryptionAccountAtBlock(address(0x1234), block.number - 1); + assertEq(resolvedOwner, alice, "Should be alice"); + assertEq(votingWallet, address(0x1234), "Should be 0x1234"); + + (resolvedOwner, votingWallet) = signerList.resolveEncryptionAccountAtBlock(address(0x2345), block.number - 1); + assertEq(resolvedOwner, bob, "Should be bob"); + assertEq(votingWallet, address(0x2345), "Should be 0x2345"); + } + + modifier givenTheResolvedOwnerWasListedOnResolveEncryptionAccountAtBlock() { + // But not listed now + // Prior appointments are still in place + + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + + address[] memory mvSigners = new address[](4); + mvSigners[0] = address(0x5555); + mvSigners[1] = address(0x6666); + mvSigners[2] = address(0x7777); + mvSigners[3] = address(0x8888); + signerList.addSigners(mvSigners); + + vm.roll(block.number + 1); + + mvSigners[0] = alice; + mvSigners[1] = bob; + mvSigners[2] = carol; + mvSigners[3] = david; + signerList.removeSigners(mvSigners); + + _; + } + + function test_WhenTheGivenAddressIsOwner2() + external + whenCallingResolveEncryptionAccountAtBlock + givenTheResolvedOwnerWasListedOnResolveEncryptionAccountAtBlock + { + address resolvedOwner; + address votingWallet; + + // 1 - owner appoints + + // It owner should be the given address + // It votingWallet should be the resolved appointed wallet + (resolvedOwner, votingWallet) = signerList.resolveEncryptionAccountAtBlock(alice, block.number - 1); + assertEq(resolvedOwner, alice, "Should be alice"); + assertEq(votingWallet, address(0x1234), "Should be 0x1234"); + + (resolvedOwner, votingWallet) = signerList.resolveEncryptionAccountAtBlock(bob, block.number - 1); + assertEq(resolvedOwner, bob, "Should be bob"); + assertEq(votingWallet, address(0x2345), "Should be 0x2345"); + + // 2 - No appointed wallet + + // It owner should be the given address + // It votingWallet should be the resolved appointed wallet + (resolvedOwner, votingWallet) = signerList.resolveEncryptionAccountAtBlock(carol, block.number - 1); + assertEq(resolvedOwner, carol, "Should be carol"); + assertEq(votingWallet, carol, "Should be carol"); + + (resolvedOwner, votingWallet) = signerList.resolveEncryptionAccountAtBlock(david, block.number - 1); + assertEq(resolvedOwner, david, "Should be david"); + assertEq(votingWallet, david, "Should be david"); + } + + function test_WhenTheGivenAddressIsAppointed2() + external + whenCallingResolveEncryptionAccountAtBlock + givenTheResolvedOwnerWasListedOnResolveEncryptionAccountAtBlock + { + address resolvedOwner; + address votingWallet; + + // It owner should be the resolved owner + // It votingWallet should be the given address + (resolvedOwner, votingWallet) = signerList.resolveEncryptionAccountAtBlock(address(0x1234), block.number - 1); + assertEq(resolvedOwner, alice, "Should be alice"); + assertEq(votingWallet, address(0x1234), "Should be 0x1234"); + + (resolvedOwner, votingWallet) = signerList.resolveEncryptionAccountAtBlock(address(0x2345), block.number - 1); + assertEq(resolvedOwner, bob, "Should be bob"); + assertEq(votingWallet, address(0x2345), "Should be 0x2345"); + } + + function test_GivenTheResolvedOwnerWasNotListedOnResolveEncryptionAccountAtBlock() + external + whenCallingResolveEncryptionAccountAtBlock + { + address resolvedOwner; + address votingWallet; + + // It should return a zero owner + // It should return a zero votingWallet + + (resolvedOwner, votingWallet) = signerList.resolveEncryptionAccountAtBlock(address(0), block.number - 1); + assertEq(resolvedOwner, address(0), "Should be 0"); + assertEq(votingWallet, address(0), "Should be 0"); + + (resolvedOwner, votingWallet) = signerList.resolveEncryptionAccountAtBlock(address(0x5555), block.number - 1); + assertEq(resolvedOwner, address(0), "Should be 0"); + assertEq(votingWallet, address(0), "Should be 0"); + + (resolvedOwner, votingWallet) = signerList.resolveEncryptionAccountAtBlock(address(0xaaaa), block.number - 1); + assertEq(resolvedOwner, address(0), "Should be 0"); + assertEq(votingWallet, address(0), "Should be 0"); + } + + modifier whenCallingGetEncryptionRecipients() { + _; + } + + function test_GivenTheEncryptionRegistryHasNoAccounts() external whenCallingGetEncryptionRecipients { + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + + // No accounts registered a public key + + // It returns an empty list, even with signers + address[] memory recipients = signerList.getEncryptionRecipients(); + assertEq(recipients.length, 0, "Should be empty"); + + // Empty the list + signerList.updateSettings(SignerList.Settings(encryptionRegistry, 0)); + + address[] memory rmSigners = new address[](4); + rmSigners[0] = alice; + rmSigners[1] = bob; + rmSigners[2] = carol; + rmSigners[3] = david; + signerList.removeSigners(rmSigners); + + // It returns an empty list, without signers + recipients = signerList.getEncryptionRecipients(); + assertEq(recipients.length, 0, "Should be empty"); + } + + modifier givenTheEncryptionRegistryHasAccounts() { + _; + } + + function test_GivenNoOverlapBetweenRegistryAndSignerList() + external + whenCallingGetEncryptionRecipients + givenTheEncryptionRegistryHasAccounts + { + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + + // Old accounts register a public key or appoint + vm.startPrank(alice); + encryptionRegistry.setOwnPublicKey(bytes32(uint256(0x5555))); + vm.startPrank(bob); + encryptionRegistry.appointWallet(address(0x1234)); + vm.startPrank(address(0x1234)); + encryptionRegistry.setPublicKey(bob, bytes32(uint256(0x1234))); + vm.startPrank(carol); + encryptionRegistry.setOwnPublicKey(bytes32(uint256(0x5555))); + vm.startPrank(david); + encryptionRegistry.appointWallet(address(0x2345)); + vm.startPrank(address(0x2345)); + encryptionRegistry.setPublicKey(david, bytes32(uint256(0x2345))); + + // It returns an empty list + address[] memory recipients = signerList.getEncryptionRecipients(); + assertEq(recipients.length, 4, "Should have 4 members"); + assertEq(recipients[0], alice, "Should be alice"); + assertEq(recipients[1], address(0x1234), "Should be 1234"); + assertEq(recipients[2], carol, "Should be carol"); + assertEq(recipients[3], address(0x2345), "Should be 2345"); + + vm.startPrank(alice); + + // Replace the list of signers + address[] memory newSigners = new address[](4); + newSigners[0] = address(0); + newSigners[1] = address(1); + newSigners[2] = address(2); + newSigners[3] = address(3); + signerList.addSigners(newSigners); + + address[] memory rmSigners = new address[](4); + rmSigners[0] = alice; + rmSigners[1] = bob; + rmSigners[2] = carol; + rmSigners[3] = david; + signerList.removeSigners(rmSigners); + + // It returns an empty list + recipients = signerList.getEncryptionRecipients(); + assertEq(recipients.length, 0, "Should be empty"); + } + + function test_GivenSomeAddressesAreRegisteredEverywhere() + external + whenCallingGetEncryptionRecipients + givenTheEncryptionRegistryHasAccounts + { + // It returns a list containing the overlapping addresses + // It the result has the correct resolved addresses + // It result does not contain unregistered addresses + // It result does not contain unlisted addresses + // It result does not contain non appointed addresses + + dao.grant(address(signerList), alice, UPDATE_SIGNER_LIST_PERMISSION_ID); + + address[] memory newSigners = new address[](4); + newSigners[0] = address(0x10); + newSigners[1] = address(0x11); + newSigners[2] = address(0x12); + newSigners[3] = address(0x13); + signerList.addSigners(newSigners); + + // Owner + vm.startPrank(alice); + encryptionRegistry.setOwnPublicKey(bytes32(uint256(0x5555))); + // Appointing 1234 + vm.startPrank(bob); + encryptionRegistry.appointWallet(address(0x1234)); + // Appointed + vm.startPrank(address(0x1234)); + encryptionRegistry.setPublicKey(bob, bytes32(uint256(0x1234))); + // Owner with no pubKey + // vm.startPrank(carol); + // encryptionRegistry.setOwnPublicKey(bytes32(uint256(0))); + // Appointing 2345 + vm.startPrank(david); + encryptionRegistry.appointWallet(address(0x2345)); + // Appointed with no pubKey + // vm.startPrank(address(0x2345)); + // encryptionRegistry.setPublicKey(david, bytes32(uint256(0))); + + address[] memory recipients = signerList.getEncryptionRecipients(); + assertEq(recipients.length, 3, "Should have 3 members"); + assertEq(recipients[0], alice, "Should be alice"); + assertEq(recipients[1], address(0x1234), "Should be 1234"); + // Carol didn't interact yet + assertEq(recipients[2], address(0x2345), "Should be 2345"); + + // Register the missing public keys + vm.startPrank(carol); + encryptionRegistry.setOwnPublicKey(bytes32(uint256(0x7777))); + // Appointed by david + vm.startPrank(address(0x2345)); + encryptionRegistry.setPublicKey(david, bytes32(uint256(0x2345))); + + // Updated list + recipients = signerList.getEncryptionRecipients(); + assertEq(recipients.length, 4, "Should have 4 members"); + assertEq(recipients[0], alice, "Should be alice"); + assertEq(recipients[1], address(0x1234), "Should be 1234"); + assertEq(recipients[2], address(0x2345), "Should be 2345"); + assertEq(recipients[3], carol, "Should be carol"); + } + + // Additional tests beyond SignerListTree.t.yaml + + function testFuzz_IsMemberIsFalseByDefault(uint256 _randomEntropy) public view { + assertEq( + signerList.isListed(vm.addr(uint256(keccak256(abi.encodePacked(_randomEntropy))))), false, "Should be false" + ); + } +} diff --git a/test/SignerList.t.yaml b/test/SignerList.t.yaml new file mode 100644 index 0000000..24644cc --- /dev/null +++ b/test/SignerList.t.yaml @@ -0,0 +1,196 @@ +SignerListTest: + # contract lifecycle + - when: deploying the contract + then: + - it: should initialize normally + - given: a deployed contract + then: + - it: should refuse to initialize again + - given: a new instance + and: + - given: calling initialize + and: + - it: should set the DAO address + # Same checks as addSigners below + - given: passing more addresses than supported [on initialize] + then: + - it: should revert + - given: duplicate addresses [on initialize] + then: + - it: should revert + - it: should append the new addresses to the list + - it: should return true on isListed + - it: should emit the SignersAdded event + - it: the encryption registry should be empty + - it: minSignerListLength should be zero + + - when: calling updateSettings + and: + - when: updateSettings without the permission + then: + - it: should revert + - when: encryptionRegistry is not compatible + then: + - it: should revert + - when: minSignerListLength is bigger than the list size + then: + - it: should revert + - it: sets the new encryption registry + - it: sets the new minSignerListLength + - it: should emit a SignerListSettingsUpdated event + + - when: calling supportsInterface + then: + - it: does not support the empty interface + - it: supports IERC165Upgradeable + - it: supports Addresslist + - it: supports ISignerList + + # List lifecycle + - when: calling addSigners + and: + - when: adding without the permission + then: + - it: should revert + - given: passing more addresses than supported [on addSigners] + then: + - it: should revert + - given: duplicate addresses [on addSigners] + then: + - it: should revert + - it: should append the new addresses to the list + - it: should return true on isListed + - it: should emit the SignersAdded event + + - when: calling removeSigners + and: + - when: removing without the permission + then: + - it: should revert + - when: removing an unlisted address + then: + - it: should revert + - given: removing too many addresses + comment: The new list will be smaller than minSignerListLength + then: + - it: should revert + - it: should remove the given addresses + - it: should return false on isListed + - it: should emit the SignersRemoved event + + # Getters + - when: calling isListed + and: + - given: the member is listed + then: + - it: returns true + - given: the member is not listed + then: + - it: returns false + + - when: calling isListedAtBlock + and: + - given: the member was listed + and: + - given: the member is not listed now + then: + - it: returns true + - given: the member is listed now + then: + - it: returns true + - given: the member was not listed + and: + - given: the member is delisted now + then: + - it: returns false + - given: the member is enlisted now + then: + - it: returns false + + # Encryption getters + - when: calling isListedOrAppointedByListed + and: + - given: the caller is a listed signer + then: + - it: listedOrAppointedByListed should be true + - given: the caller is appointed by a signer + then: + - it: listedOrAppointedByListed should be true + - given: the caller is not listed or appointed + then: + - it: listedOrAppointedByListed should be false + + - when: calling getListedEncryptionOwnerAtBlock + and: + - given: the resolved owner is listed [on getListedEncryptionOwnerAtBlock] + and: + - when: the given address is the owner + then: + - it: should return the given address + - when: the given address is appointed by the owner + then: + - it: should return the resolved owner + + - given: the resolved owner was listed [on getListedEncryptionOwnerAtBlock] + and: + - when: the given address is the owner 2 + then: + - it: should return the given address + - when: the given address is appointed by the owner 2 + then: + - it: should return the resolved owner + + - given: the resolved owner was not listed [on getListedEncryptionOwnerAtBlock] + then: + - it: should return a zero value + + - when: calling resolveEncryptionAccountAtBlock + and: + - given: the resolved owner is listed [on resolveEncryptionAccountAtBlock] + and: + - when: the given address is owner + then: + - it: owner should be itself + - it: votingWallet should be the appointed address + - when: the given address is appointed + then: + - it: owner should be the resolved owner + - it: votingWallet should be the given address + + - given: the resolved owner was listed [on resolveEncryptionAccountAtBlock] + and: + - when: the given address is owner 2 + then: + - it: owner should be itself + - it: votingWallet should be the appointed address + - when: the given address is appointed 2 + then: + - it: owner should be the resolved owner + - it: votingWallet should be the given address + + - given: the resolved owner was not listed [on resolveEncryptionAccountAtBlock] + then: + - it: should return a zero owner + - it: should return a zero appointedWallet + + - when: calling getEncryptionRecipients + and: + - given: the encryption registry has no accounts + then: + - it: returns an empty list, even with signers + - it: returns an empty list, without signers + + - given: the encryption registry has accounts + then: + - given: no overlap between registry and signerList + comment: Some are on the encryption registry only and some are on the signerList only + then: + - it: returns an empty list + - given: some addresses are registered everywhere + then: + - it: returns a list containing the overlapping addresses + - it: the result has the correct resolved addresses + comment: appointed wallets are present, not the owner + - it: result does not contain unregistered addresses + - it: result does not contain unlisted addresses + - it: result does not contain non appointed addresses diff --git a/test/helpers/DaoBuilder.sol b/test/helpers/DaoBuilder.sol index 94b8fc9..c51c5b8 100644 --- a/test/helpers/DaoBuilder.sol +++ b/test/helpers/DaoBuilder.sol @@ -6,6 +6,8 @@ import {DAO} from "@aragon/osx/core/dao/DAO.sol"; import {Multisig} from "../../src/Multisig.sol"; import {EmergencyMultisig} from "../../src/EmergencyMultisig.sol"; import {OptimisticTokenVotingPlugin} from "../../src/OptimisticTokenVotingPlugin.sol"; +import {SignerList, UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID} from "../../src/SignerList.sol"; +import {EncryptionRegistry} from "../../src/EncryptionRegistry.sol"; import {createProxyAndCall} from "../../src/helpers/proxy.sol"; import {RATIO_BASE} from "@aragon/osx/plugins/utils/Ratio.sol"; import {TaikoL1Mock, TaikoL1PausedMock, TaikoL1WithOldLastBlock, TaikoL1Incompatible} from "../mocks/TaikoL1Mock.sol"; @@ -18,6 +20,7 @@ contract DaoBuilder is Test { address immutable MULTISIG_BASE = address(new Multisig()); address immutable EMERGENCY_MULTISIG_BASE = address(new EmergencyMultisig()); address immutable OPTIMISTIC_BASE = address(new OptimisticTokenVotingPlugin()); + address immutable SIGNER_LIST_BASE = address(new SignerList()); enum TaikoL1Status { Standard, @@ -147,7 +150,9 @@ contract DaoBuilder is Test { } function withMinApprovals(uint16 newMinApprovals) public returns (DaoBuilder) { - if (newMinApprovals > multisigMembers.length) revert("You should add enough multisig members first"); + if (newMinApprovals > multisigMembers.length) { + revert("You should add enough multisig members first"); + } minApprovals = newMinApprovals; return this; } @@ -162,6 +167,8 @@ contract DaoBuilder is Test { Multisig multisig, EmergencyMultisig emergencyMultisig, GovernanceERC20Mock votingToken, + SignerList signerList, + EncryptionRegistry encryptionRegistry, ITaikoL1 taikoL1 ) { @@ -217,15 +224,8 @@ contract DaoBuilder is Test { ); } - // Standard multisig + // Encryption registry and signer list { - Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ - onlyListed: onlyListed, - minApprovals: minApprovals, - destinationProposalDuration: stdProposalDuration, - proposalExpirationPeriod: multisigProposalExpirationPeriod - }); - address[] memory signers; if (multisigMembers.length > 0) { signers = multisigMembers; @@ -234,10 +234,28 @@ contract DaoBuilder is Test { signers = new address[](1); signers[0] = owner; } + + signerList = SignerList( + createProxyAndCall(address(SIGNER_LIST_BASE), abi.encodeCall(SignerList.initialize, (dao, signers))) + ); + encryptionRegistry = new EncryptionRegistry(signerList); + dao.grant(address(signerList), address(this), UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + signerList.updateSettings(SignerList.Settings(encryptionRegistry, uint16(signers.length))); + dao.revoke(address(signerList), address(this), UPDATE_SIGNER_LIST_SETTINGS_PERMISSION_ID); + } + + // Standard multisig + { + Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({ + onlyListed: onlyListed, + minApprovals: minApprovals, + destinationProposalDuration: stdProposalDuration, + signerList: signerList, + proposalExpirationPeriod: multisigProposalExpirationPeriod + }); + multisig = Multisig( - createProxyAndCall( - address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, signers, settings)) - ) + createProxyAndCall(address(MULTISIG_BASE), abi.encodeCall(Multisig.initialize, (dao, settings))) ); } @@ -246,7 +264,7 @@ contract DaoBuilder is Test { EmergencyMultisig.MultisigSettings memory settings = EmergencyMultisig.MultisigSettings({ onlyListed: onlyListed, minApprovals: minApprovals, - addresslistSource: multisig, + signerList: signerList, proposalExpirationPeriod: multisigProposalExpirationPeriod }); diff --git a/test/integration/TaikoDaoFactory.t.sol b/test/integration/TaikoDaoFactory.t.sol index 5b8a681..2c1024d 100644 --- a/test/integration/TaikoDaoFactory.t.sol +++ b/test/integration/TaikoDaoFactory.t.sol @@ -22,6 +22,7 @@ import {createProxyAndCall} from "../../src/helpers/proxy.sol"; import {MultisigPluginSetup} from "../../src/setup/MultisigPluginSetup.sol"; import {EmergencyMultisigPluginSetup} from "../../src/setup/EmergencyMultisigPluginSetup.sol"; import {OptimisticTokenVotingPluginSetup} from "../../src/setup/OptimisticTokenVotingPluginSetup.sol"; +import {SignerList} from "../../src/SignerList.sol"; contract TaikoDaoFactoryTest is AragonTest { function test_ShouldStoreTheSettings_1() public { @@ -393,6 +394,16 @@ contract TaikoDaoFactoryTest is AragonTest { "The DAO should have REGISTER_STANDARD_CALLBACK_PERMISSION_ID on itself" ); + // Signer list + + assertEq(deployment.signerList.addresslistLength(), 13, "Invalid addresslistLength"); + for (uint256 i = 0; i < 13; i++) { + assertEq(deployment.signerList.isListed(multisigMembers[i]), true, "Should be a member"); + } + for (uint256 i = 14; i < 50; i++) { + assertEq(deployment.signerList.isListed(address(uint160(i))), false, "Should not be a member"); + } + // Multisig plugin assertNotEq(address(deployment.multisigPlugin), address(0), "Empty multisig field"); @@ -402,20 +413,20 @@ contract TaikoDaoFactoryTest is AragonTest { "Invalid lastMultisigSettingsChange" ); assertEq(deployment.multisigPlugin.proposalCount(), 0, "Invalid proposal count"); - assertEq(deployment.multisigPlugin.addresslistLength(), 13, "Invalid addresslistLength"); - for (uint256 i = 0; i < 13; i++) { - assertEq(deployment.multisigPlugin.isMember(multisigMembers[i]), true, "Should be a member"); - } - for (uint256 i = 14; i < 50; i++) { - assertEq(deployment.multisigPlugin.isMember(address(uint160(i))), false, "Should not be a member"); - } + { - (bool onlyListed, uint16 minApprovals, uint64 destinationProposalDuration, uint64 expirationPeriod) = - deployment.multisigPlugin.multisigSettings(); + ( + bool onlyListed, + uint16 minApprovals, + uint64 destinationProposalDuration, + SignerList signerList, + uint64 expirationPeriod + ) = deployment.multisigPlugin.multisigSettings(); assertEq(onlyListed, true, "Invalid onlyListed"); assertEq(minApprovals, 7, "Invalid minApprovals"); assertEq(destinationProposalDuration, 10 days, "Invalid destinationProposalDuration"); + assertEq(address(signerList), address(deployment.signerList), "Incorrect signerList"); assertEq(expirationPeriod, 15 days, "Invalid expirationPeriod"); } @@ -428,19 +439,13 @@ contract TaikoDaoFactoryTest is AragonTest { "Invalid lastMultisigSettingsChange" ); assertEq(deployment.emergencyMultisigPlugin.proposalCount(), 0, "Invalid proposal count"); - for (uint256 i = 0; i < 13; i++) { - assertEq(deployment.emergencyMultisigPlugin.isMember(multisigMembers[i]), true, "Should be a member"); - } - for (uint256 i = 14; i < 50; i++) { - assertEq(deployment.emergencyMultisigPlugin.isMember(address(uint160(i))), false, "Should not be a member"); - } { - (bool onlyListed, uint16 minApprovals, Addresslist addresslistSource, uint64 expirationPeriod) = + (bool onlyListed, uint16 minApprovals, Addresslist signerList, uint64 expirationPeriod) = deployment.emergencyMultisigPlugin.multisigSettings(); assertEq(onlyListed, true, "Invalid onlyListed"); assertEq(minApprovals, 11, "Invalid minApprovals"); - assertEq(address(addresslistSource), address(deployment.multisigPlugin), "Invalid addresslistSource"); + assertEq(address(signerList), address(deployment.signerList), "Invalid signerList"); assertEq(expirationPeriod, 15 days, "Invalid expirationPeriod"); } @@ -516,9 +521,11 @@ contract TaikoDaoFactoryTest is AragonTest { "Invalid optimisticTokenVotingPluginSetup" ); - // PUBLIC KEY REGISTRY - assertNotEq(address(deployment.publicKeyRegistry), address(0), "Empty publicKeyRegistry field"); - assertEq(deployment.publicKeyRegistry.registeredWalletCount(), 0, "Invalid registeredWalletCount"); + // ENCRYPTION REGISTRY + assertNotEq(address(deployment.encryptionRegistry), address(0), "Empty encryptionRegistry field"); + assertEq( + deployment.encryptionRegistry.getRegisteredAccounts().length, 0, "Invalid getRegisteredAccounts().length" + ); } function test_StandardDeployment_2() public { @@ -630,6 +637,16 @@ contract TaikoDaoFactoryTest is AragonTest { "The DAO should have REGISTER_STANDARD_CALLBACK_PERMISSION_ID on itself" ); + // Signer List + + assertEq(deployment.signerList.addresslistLength(), 16, "Invalid addresslistLength"); + for (uint256 i = 0; i < 16; i++) { + assertEq(deployment.signerList.isListed(multisigMembers[i]), true, "Should be a member"); + } + for (uint256 i = 17; i < 50; i++) { + assertEq(deployment.signerList.isListed(address(uint160(i))), false, "Should not be a member"); + } + // Multisig plugin assertNotEq(address(deployment.multisigPlugin), address(0), "Empty multisig field"); @@ -639,20 +656,20 @@ contract TaikoDaoFactoryTest is AragonTest { "Invalid lastMultisigSettingsChange" ); assertEq(deployment.multisigPlugin.proposalCount(), 0, "Invalid proposal count"); - assertEq(deployment.multisigPlugin.addresslistLength(), 16, "Invalid addresslistLength"); - for (uint256 i = 0; i < 16; i++) { - assertEq(deployment.multisigPlugin.isMember(multisigMembers[i]), true, "Should be a member"); - } - for (uint256 i = 17; i < 50; i++) { - assertEq(deployment.multisigPlugin.isMember(address(uint160(i))), false, "Should not be a member"); - } + { - (bool onlyListed, uint16 minApprovals, uint64 destinationProposalDuration, uint64 expirationPeriod) = - deployment.multisigPlugin.multisigSettings(); + ( + bool onlyListed, + uint16 minApprovals, + uint64 destinationProposalDuration, + SignerList signerList, + uint64 expirationPeriod + ) = deployment.multisigPlugin.multisigSettings(); assertEq(onlyListed, true, "Invalid onlyListed"); assertEq(minApprovals, 9, "Invalid minApprovals"); assertEq(destinationProposalDuration, 21 days, "Invalid destinationProposalDuration"); + assertEq(address(signerList), address(deployment.signerList), "Incorrect signerList"); assertEq(expirationPeriod, 22 days, "Invalid expirationPeriod"); } @@ -665,19 +682,13 @@ contract TaikoDaoFactoryTest is AragonTest { "Invalid lastMultisigSettingsChange" ); assertEq(deployment.emergencyMultisigPlugin.proposalCount(), 0, "Invalid proposal count"); - for (uint256 i = 0; i < 16; i++) { - assertEq(deployment.emergencyMultisigPlugin.isMember(multisigMembers[i]), true, "Should be a member"); - } - for (uint256 i = 17; i < 50; i++) { - assertEq(deployment.emergencyMultisigPlugin.isMember(address(uint160(i))), false, "Should not be a member"); - } { - (bool onlyListed, uint16 minApprovals, Addresslist addresslistSource, uint64 expirationPeriod) = + (bool onlyListed, uint16 minApprovals, Addresslist signerList, uint64 expirationPeriod) = deployment.emergencyMultisigPlugin.multisigSettings(); assertEq(onlyListed, true, "Invalid onlyListed"); assertEq(minApprovals, 15, "Invalid minApprovals"); - assertEq(address(addresslistSource), address(deployment.multisigPlugin), "Invalid addresslistSource"); + assertEq(address(signerList), address(deployment.signerList), "Invalid signerList"); assertEq(expirationPeriod, 22 days, "Invalid expirationPeriod"); } @@ -753,9 +764,11 @@ contract TaikoDaoFactoryTest is AragonTest { "Invalid optimisticTokenVotingPluginSetup" ); - // PUBLIC KEY REGISTRY - assertNotEq(address(deployment.publicKeyRegistry), address(0), "Empty publicKeyRegistry field"); - assertEq(deployment.publicKeyRegistry.registeredWalletCount(), 0, "Invalid registeredWalletCount"); + // ENCRYPTION REGISTRY + assertNotEq(address(deployment.encryptionRegistry), address(0), "Empty encryptionRegistry field"); + assertEq( + deployment.encryptionRegistry.getRegisteredAccounts().length, 0, "Invalid getRegisteredAccounts().length" + ); } function test_MultipleDeploysDoNothing() public { diff --git a/test/script/make-test-tree.ts b/test/script/make-test-tree.ts index 119ff14..84713b4 100644 --- a/test/script/make-test-tree.ts +++ b/test/script/make-test-tree.ts @@ -57,9 +57,9 @@ function parseRuleChildren(lines: Array): Array { let content = ""; if (rule.given) { - content = "Given " + rule.given; + content = "Given " + cleanText(rule.given); } else if (rule.when) { - content = "When " + rule.when; + content = "When " + cleanText(rule.when); } else if (rule.it) { content = "It " + rule.it; } @@ -134,6 +134,10 @@ function renderTreeItem( return result; } +function cleanText(input: string): string { + return input.replace(/[^a-zA-Z0-9 ]/g, "").trim(); +} + async function readStdinText() { let result = ""; const decoder = new TextDecoder();