diff --git a/.env.example b/.env.example index 9845036..08bf36f 100644 --- a/.env.example +++ b/.env.example @@ -1,23 +1,14 @@ -# Fork Test mode: -# "fork-deploy" will run against the live network fork, deploying new contracts via a new instance of the factory -# "fork-existing" will run against the live network fork, using the existing factory & therefore the existing contracts -FORK_TEST_MODE="fork-deploy" +# NETWORK AND ACCOUNT(s) +DEPLOYMENT_PRIVATE_KEY="0x..." +REFUND_ADDRESS="0x..." -# With false, the script will deploy mock tokens with open mint functions -DEPLOY_AS_PRODUCTION=false +# The name of the networks to use for test/production +TESTNET_NETWORK="holesky" +PRODNET_NETWORK="mainnet" -# If deploying against a fork, pass the address of a large token holder who will be -# impersonated to distribute tokens to addresses inside test cases -# the whale should have >= 3000 tokens -TOKEN_TEST_WHALE="0x" - -# If the factory singleton is deployed, pass the address. "Fork existing" mode will use this previously -# deployed factory, and "fork deploy" will deploy a new factory instance -FACTORY="0x0000000000000000000000000000000000000000" - -# NETWORK AND DEPLOYMENT WALLET -DEPLOYMENT_PRIVATE_KEY="..." -NETWORK="sepolia" +# 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 @@ -25,21 +16,21 @@ NETWORK="sepolia" # ETHERSCAN_API_KEY="..." # ALCHEMY_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" -MIN_APPROVALS="1" # How many multisig approvals are required +MIN_APPROVALS="1" # How many multisig approvals are required MULTISIG_PROPOSAL_EXPIRATION_PERIOD="864000" # How long until a pending proposal expires (10 days) # GAUGE VOTER PARAMETERS -# The token to be used for the escrow +# The main token for the escrow and the escrow details TOKEN1_ADDRESS="0x0000000000000000000000000000000000000000" VE_TOKEN1_NAME="Voting Escrow Token 1" VE_TOKEN1_SYMBOL="veTK1" -# Additional tokens these will have secondary escrow contracts -TOKEN2_ADDRESS="0x0000000000000000000000000000000000000000" # Ignored if 0x0 +# Additional tokens (optional) +# Each token gets its own escrow +TOKEN2_ADDRESS="0x0000000000000000000000000000000000000000" # Ignored when zero VE_TOKEN2_NAME="Voting Escrow Token 2" VE_TOKEN2_SYMBOL="veTK2" @@ -59,7 +50,7 @@ MIN_LOCK_DURATION="3600" # 1 hour VOTING_PAUSED=true # Initial minimum amount needed (in wei) to create a lock -MIN_DEPOSIT="1000000000000000000" # 1 ether +MIN_DEPOSIT="1000000000000000000" # 1 ether (in token terms) # PLUGIN REPO PARAMETERS (per-network) # SEPOLIA diff --git a/.env.test.example b/.env.test.example new file mode 100644 index 0000000..475a364 --- /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="0x0000000000000000000000000000000000000000" + +# 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="0x0000000000000000000000000000000000000000" + +# The block number to run fork tests on +# 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= diff --git a/.gitignore b/.gitignore index c72c6f7..09d3b94 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ -^.env.example .env* +!.env.example +!.env.test.example + +# Logs +*.log # Foundry cache/ diff --git a/Makefile b/Makefile index a760692..212c43a 100644 --- a/Makefile +++ b/Makefile @@ -1,68 +1,189 @@ +.DEFAULT_TARGET: help -# include .env file and export its env vars -# (-include to ignore error if it does not exist) +# Import the .env files and export their values (ignore any error if missing) -include .env +-include .env.test -# linux: allow shell scripts to be executed -allow-scripts:; chmod +x ./coverage.sh +# RULE SPECIFIC ENV VARS [optional] -# init the repo -install :; make allow-scripts && forge build +# Override the verifier and block explorer parameters (network dependent) +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://sepolia.explorer.mode.network/api\?" -# create an HTML coverage report in ./report (requires lcov & genhtml) -coverage:; ./coverage.sh - # -# run unit tests -test-unit :; forge test --no-match-path "test/fork/**/*.sol" +# CONSTANTS + +TEST_COVERAGE_SRC_FILES:=$(wildcard test/*.sol test/**/*.sol script/*.sol script/**/*.sol src/escrow/increasing/delegation/*.sol src/libs/ProxyLib.sol) +FORK_TEST_WILDCARD:="test/fork/**/*.sol" +E2E_TEST_NAME:=TestE2EV2 +DEPLOY_SCRIPT:=script/DeployGauges.s.sol:DeployGauges +VERBOSITY:=-vvv +SHELL:=/bin/bash + +# TARGETS + +.PHONY: help +help: + @echo "Available targets:" + @echo + @grep -E '^[a-zA-Z0-9_-]*:.*?## .*$$' Makefile \ + | sed -n 's/^\(.*\): \(.*\)##\(.*\)/- make \1 \3/p' \ + | sed 's/^- make $$//g' + +.PHONY: init +init: .env .env.test ## Check the required tools and dependencies + @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 -Rf ./out/* lcov.info* ./report/* + +# Copy the .env files if not present +.env: + cp .env.example .env + @echo "NOTE: Edit the correct values of .env before you continue" + +.env.test: + cp .env.test.example .env.test + @echo "NOTE: Edit the correct values of .env.test before you continue" + +: ## + +.PHONY: test +test: ## Run unit tests, locally + forge test --no-match-path $(FORK_TEST_WILDCARD) + +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 + +: ## #### Fork testing #### -# Fork testing - mode sepolia -ft-mode-sepolia-fork :; forge test --match-contract TestE2EV2 \ - --rpc-url https://sepolia.mode.network \ - -vv - -# Fork testing - mode mainnet -ft-mode-fork :; forge test --match-contract TestE2EV2 \ - --rpc-url https://mainnet.mode.network/ \ - -vvvvv - -# Fork testing - holesky -ft-holesky-fork :; forge test --match-contract TestE2EV2 \ - --rpc-url https://holesky.drpc.org \ - -vvvvv - -# Fork testing - sepolia -ft-sepolia-fork :; forge test --match-contract TestE2EV2 \ - --rpc-url https://sepolia.drpc.org \ - -vvvvv - -#### Deployments #### - -deploy-preview-mode-sepolia :; forge script script/Deploy.s.sol:Deploy \ - --rpc-url https://sepolia.mode.network \ - --private-key $(DEPLOYMENT_PRIVATE_KEY) \ - -vvvvv - -deploy-mode-sepolia :; forge script script/Deploy.s.sol:Deploy \ - --rpc-url https://sepolia.mode.network \ - --private-key $(DEPLOYMENT_PRIVATE_KEY) \ - --broadcast \ - --verify \ - --verifier blockscout \ - --verifier-url https://sepolia.explorer.mode.network/api\? \ - -vvvvv - -deploy-preview-mode :; forge script script/Deploy.s.sol:Deploy \ - --rpc-url https://mainnet.mode.network \ - --private-key $(DEPLOYMENT_PRIVATE_KEY) \ - -vvvvv - -deploy-mode :; forge script script/Deploy.s.sol:Deploy \ - --rpc-url https://mainnet.mode.network \ - --private-key $(DEPLOYMENT_PRIVATE_KEY) \ - --broadcast \ - --verify \ - --etherscan-api-key $(ETHERSCAN_API_KEY) \ - -vvv +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-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-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" + forge script $(DEPLOY_SCRIPT) \ + --chain $(NETWORK) \ + --rpc-url $(RPC_URL) \ + --broadcast \ + --verify \ + $(VERIFIER_TYPE_PARAM) \ + $(VERIFIER_URL_PARAM) \ + $(ETHERSCAN_API_KEY_PARAM) \ + $(VERBOSITY) | tee $(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 9df21cf..4bfff9b 100644 --- a/README.md +++ b/README.md @@ -4,59 +4,80 @@ Welcome to Aragon's veGovernance Plugin - a flexible, modular and secure system ## Setup -To get started, ensure that [Foundry](https://getfoundry.sh/) is installed on your computer, then copy `.env.example` into `.env` and define the parameters +To get started, ensure that [Foundry](https://getfoundry.sh/) is installed on your computer. -### Understanding `.env.example` +
+ Also make sure to install [GNU Make](https://www.gnu.org/software/make/). + + ```sh + # debian + sudo apt install build-essential -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 + # arch + sudo pacman -S base-devel -## Using the Makefile + # nix + nix-env -iA nixpkgs.gnumake -The `Makefile` functions as a script runner for common tasks. It's recommended to start there. Ensure you have the required tools installed to run the `make` command on your system: + # macOS + brew install make + ``` -```sh -# debian -sudo apt install build-essential +
-# arch -sudo pacman -S base-devel +### Using the Makefile -# nix -nix-env -iA nixpkgs.gnumake +The `Makefile` as 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. -# macOS -brew install make ``` +$ make +Available targets: -Then run the commands as needed +- make init Check the required tools and dependencies +- make clean Clean the build artifacts -```sh -# Setup the repo -make install +- make test Run unit tests, locally +- make test-coverage Generate an HTML coverage report under ./report -# run unit tests -make unit-test +- make test-fork-mint-testnet Clean fork test, minting test tokens (testnet) +- make test-fork-mint-prodnet Clean fork test, minting test tokens (production network) + +- make test-fork-testnet Fork test using the existing token(s), new factory (testnet) +- make test-fork-prodnet Fork test using the existing token(s), new factory (production network) -# generate coverage report in the `report` directory -# requires lcov and genhtml -# serve the report/index.html in browser to view -make coverage +- make test-fork-factory-testnet Fork test using an existing factory (testnet) +- make test-fork-factory-prodnet Fork test using an existing factory (production network) -# the .env.example is set to work with sepolia -make ft-sepolia-fork +- 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 ``` +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 + ## Running fork tests Fork testing has 2 modes: -1. "fork-deploy" will run against the live network fork, deploying new contracts via a new instance of the factory +1. "new-factory" will run against the live network fork, deploying new contracts via a new instance of the factory. See `make test-fork-testnet`, `make test-fork-prodnet` and simmilar -2. "fork-existing" will run against the live network fork, using the existing factory & therefore the existing contracts +2. "existing-factory" will run against the live network fork, using the existing factory & therefore the existing contracts. See `make test-fork-factory-testnet`, `make test-fork-factory-prodnet` and simmilar In both cases, you will need to find the correct Aragon OSx contracts for the chain you wish to fork against. These can be found in the [OSx commons repo](https://github.com/aragon/osx-commons/tree/main/configs/src/deployments/json) -> If running frequent fork tests it's recommended you pass a block number to enable caching +> If running frequent fork tests it's recommended to pass a block number to enable caching ## Deployment @@ -65,25 +86,99 @@ Deployments are done using the deployment factory. This is a singleton contract - Deploy all contracts - Set permissions - Transfer ownership to a freshly deployed multisig -- Store the addresses of the deployment in a single, queriable place. +- 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: -Check the `Makefile` for examples of deployments on different networks. +``` +- 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 reviewed the parameters for the veDAO I want to deploy -- [] I have reviewed the multisig file for the correct addresses - - [] 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 updated the `.env` with these parameters -- [] I have updated the `CurveConstantLib` and `Clock` with any new constants. -- [] All my unit tests pass -- [] I have run a fork test in `fork-deploy` mode against the OSx contracts on my target testnet -- [] I have deployed my contracts successfully to a target testnet -- [] I have confirmed my tests still work in `fork-existing` mode with the live tokens and the factory. -- [] I have run the same workflow against the mainnet I wish to deploy on -- [] I have previewed my deploy -- [] My deployer address is a fresh wallet or setup for repeat production deploys in a safe manner. -- [] My wallet has sufficient native token for gas +- [ ] 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_APPROVALS` and `MULTISIG_PROPOSAL_EXPIRATION_PERIOD` are correct + - [ ] I have verified that `TOKEN1_ADDRESS` corresponds to an ERC20 contract on the target chain (same for TOKEN2 if applicable) + - [ ] I have checked that `VE_TOKEN1_NAME` and `VE_TOKEN1_SYMBOL` are correct (same for TOKEN2 if applicable) + - I have checked that fee percent, warmup period, cooldown period, min lock duration, and min deposit: + - [ ] Have the expected values + - [ ] Cannot leave the voting contract or user tokens locked out + - [ ] I have checked that `VOTING_PAUSED` is true, should voting not be active right away + - [ ] The multisig plugin repo and version: + - [ ] Correspond to the official contract on the target network + - [ ] Point to the latest stable release available + - 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/` +- [ ] I have updated the `CurveConstantLib` and `Clock` with any new constants. +- [ ] All my unit tests pass (`make test`) +- **Target test network** + - [ ] I have defined `FORK_TESTNET_BLOCK_NUMBER` on `.env.test`, with the current block number + - [ ] I have run a fork test in `new-factory` mode with minted tokens against the official OSx contracts on the testnet + - `make test-fork-mint-testnet` + - [ ] I have deployed my contracts successfully to the target testnet + - `make deploy-testnet` + - [ ] I have updated `FACTORY_ADDRESS` on `.env.test` with the address of the deployed factory + - If there is a live token with an address holding ≥ 3000 tokens on the testnet: + - [ ] I have defined `TEST_TOKEN_WHALE` on `.env.test` + - [ ] I have run a fork test in `new-factory` mode with the live token on the testnet + - `make test-fork-testnet` + - [ ] I have confirmed that tests still work in `existing-factory` mode with the live token(s) and the already deployed factory on the testnet. + - `make test-fork-factory-testnet` +- **Target production network** + - [ ] I have defined `FORK_PRODNET_BLOCK_NUMBER` on `.env.test`, with the current block number + - [ ] I have run a fork test in `new-factory` mode with minted tokens against the official OSx contracts on the prodnet + - `make test-fork-mint-prodnet` + - If the live token has an address holding ≥ 3000 tokens on the prodnet: + - [ ] I have defined `TEST_TOKEN_WHALE` on `.env.test` + - [ ] I have run a fork test in `new-factory` mode with the live token on the prodnet + - `make test-fork-prodnet` + - [ ] I have confirmed that tests still work in `existing-factory` mode with the live token(s) and the already deployed factory on the prodnet. + - `make test-fork-factory-prodnet` +- [ ] 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 @@ -108,22 +203,22 @@ RPC_URL="https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}" # Run the deployment script # If using Etherscan -forge script --chain "$NETWORK" script/Deploy.s.sol:Deploy --rpc-url "$RPC_URL" --broadcast --verify +forge script --chain "$NETWORK" script/DeployGauges.s.sol:Deploy --rpc-url "$RPC_URL" --broadcast --verify # If using BlockScout -forge script --chain "$NETWORK" script/Deploy.s.sol:Deploy --rpc-url "$RPC_URL" --broadcast --verify --verifier blockscout --verifier-url "https://sepolia.explorer.mode.network/api\?" +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/Deploy.s.sol:Deploy --rpc-url "$RPC_URL" --broadcast --verify --legacy +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/Deploy.s.sol:Deploy --rpc-url "$RPC_URL" --verify --legacy --private-key "$DEPLOYMENT_PRIVATE_KEY" --resume +forge script --chain "$NETWORK" script/DeployGauges.s.sol:Deploy --rpc-url "$RPC_URL" --verify --legacy --private-key "$DEPLOYMENT_PRIVATE_KEY" --resume ``` ## Contracts Overview diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index 8a42732..0000000 Binary files a/bun.lockb and /dev/null differ diff --git a/coverage.sh b/coverage.sh deleted file mode 100755 index 34ba4be..0000000 --- a/coverage.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -echo "Removing test, script and src/escrow/increasing/delegation and proxylib files from coverage report" -forge coverage --no-match-path "test/fork/**/*.sol" --report lcov && - lcov --remove ./lcov.info -o ./lcov.info.pruned \ - 'test/**/*.sol' 'script/**/*.sol' 'test/*.sol' \ - 'script/*.sol' 'src/escrow/increasing/delegation/*.sol' \ - 'src/libs/ProxyLib.sol' && - genhtml lcov.info.pruned -o report --branch-coverage diff --git a/script/Deploy.s.sol b/script/DeployGauges.s.sol similarity index 92% rename from script/Deploy.s.sol rename to script/DeployGauges.s.sol index 1f861f3..c25ddc5 100644 --- a/script/Deploy.s.sol +++ b/script/DeployGauges.s.sol @@ -12,7 +12,7 @@ import {PluginSetupProcessor} from "@aragon/osx/framework/plugin/setup/PluginSet import {MockERC20} from "@mocks/MockERC20.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -contract Deploy is Script { +contract DeployGauges is Script { using SafeCast for uint256; SimpleGaugeVoterSetup simpleGaugeVoterSetup; @@ -33,8 +33,8 @@ contract Deploy is Script { /// @notice Runs the deployment flow, records the given parameters and artifacts, and it becomes read only function run() public broadcast { // Prepare all parameters - bool isProduction = vm.envBool("DEPLOY_AS_PRODUCTION"); - DeploymentParameters memory parameters = getDeploymentParameters(isProduction); + bool mintTestTokens = vm.envOr("MINT_TEST_TOKENS", false); + DeploymentParameters memory parameters = getDeploymentParameters(mintTestTokens); // Create the DAO GaugesDaoFactory factory = new GaugesDaoFactory(parameters); @@ -45,10 +45,10 @@ contract Deploy is Script { } function getDeploymentParameters( - bool isProduction + bool mintTestTokens ) public returns (DeploymentParameters memory parameters) { address[] memory multisigMembers = readMultisigMembers(); - TokenParameters[] memory tokenParameters = getTokenParameters(isProduction); + TokenParameters[] memory tokenParameters = getTokenParameters(mintTestTokens); // NOTE: Multisig is already deployed, using the existing Aragon's repo // NOTE: Deploying the plugin setup from the current script to avoid code size constraints @@ -86,6 +86,10 @@ contract Deploy is Script { 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(); @@ -103,15 +107,31 @@ contract Deploy is Script { } function getTokenParameters( - bool isProduction + bool mintTestTokens ) internal returns (TokenParameters[] memory tokenParameters) { - if (isProduction) { + if (mintTestTokens) { + // MINT + console.log("Deploying 2 token contracts (testing)"); + + address[] memory multisigMembers = readMultisigMembers(); + tokenParameters = new TokenParameters[](2); + tokenParameters[0] = TokenParameters({ + token: createTestToken(multisigMembers), + veTokenName: "VE Token 1", + veTokenSymbol: "veTK1" + }); + tokenParameters[1] = TokenParameters({ + token: createTestToken(multisigMembers), + veTokenName: "VE Token 2", + veTokenSymbol: "veTK2" + }); + } else { // USE TOKEN(s) - console.log("Using production parameters"); bool hasTwoTokens = vm.envAddress("TOKEN2_ADDRESS") != address(0); tokenParameters = new TokenParameters[](hasTwoTokens ? 2 : 1); + console.log("Using token", vm.envAddress("TOKEN1_ADDRESS")); tokenParameters[0] = TokenParameters({ token: vm.envAddress("TOKEN1_ADDRESS"), veTokenName: vm.envString("VE_TOKEN1_NAME"), @@ -119,28 +139,13 @@ contract Deploy is Script { }); if (hasTwoTokens) { + console.log("Using token", vm.envAddress("TOKEN2_ADDRESS")); tokenParameters[1] = TokenParameters({ token: vm.envAddress("TOKEN2_ADDRESS"), veTokenName: vm.envString("VE_TOKEN2_NAME"), veTokenSymbol: vm.envString("VE_TOKEN2_SYMBOL") }); } - } else { - // MINT TEST TOKEN - console.log("Using testing parameters (minting 2 dev tokens)"); - - address[] memory multisigMembers = readMultisigMembers(); - tokenParameters = new TokenParameters[](2); - tokenParameters[0] = TokenParameters({ - token: createTestToken(multisigMembers), - veTokenName: "VE Token 1", - veTokenSymbol: "veTK1" - }); - tokenParameters[1] = TokenParameters({ - token: createTestToken(multisigMembers), - veTokenName: "VE Token 2", - veTokenSymbol: "veTK2" - }); } } @@ -149,8 +154,8 @@ contract Deploy is Script { MockERC20 newToken = new MockERC20(); for (uint i = 0; i < holders.length; ) { - newToken.mint(holders[i], 50 ether); - console.log("Minting 50 eth for", holders[i]); + newToken.mint(holders[i], 5000 ether); + console.log("Minting 5000 tokens for", holders[i]); unchecked { i++; diff --git a/test/base/AragonTest.sol b/test/base/AragonTest.sol new file mode 100644 index 0000000..0ac85f1 --- /dev/null +++ b/test/base/AragonTest.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.17; + +import {IPluginSetup, PluginSetup} from "@aragon/osx/framework/plugin/setup/PluginSetup.sol"; +import {DAO} from "@aragon/osx/core/dao/DAO.sol"; +import {RATIO_BASE} from "@aragon/osx/plugins/utils/Ratio.sol"; +import {ALICE_ADDRESS, BOB_ADDRESS, CAROL_ADDRESS, DAVID_ADDRESS} from "../constants.sol"; +import {Test} from "forge-std/Test.sol"; + +contract AragonTest is Test { + address immutable alice = ALICE_ADDRESS; + address immutable bob = BOB_ADDRESS; + address immutable carol = CAROL_ADDRESS; + address immutable david = DAVID_ADDRESS; + address immutable randomWallet = vm.addr(1234567890); + + address immutable DAO_BASE = address(new DAO()); + + bytes internal constant EMPTY_BYTES = ""; + + constructor() { + vm.label(alice, "Alice"); + vm.label(bob, "Bob"); + vm.label(carol, "Carol"); + vm.label(david, "David"); + vm.label(randomWallet, "Random wallet"); + } + + /// @notice Returns the address and private key associated to the given name. + /// @param name The name to get the address and private key for. + /// @return addr The address associated with the name. + /// @return pk The private key associated with the name. + function getWallet(string memory name) internal returns (address addr, uint256 pk) { + pk = uint256(keccak256(abi.encodePacked(name))); + addr = vm.addr(pk); + vm.label(addr, name); + } +} diff --git a/test/constants.sol b/test/constants.sol new file mode 100644 index 0000000..09ba147 --- /dev/null +++ b/test/constants.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +uint64 constant MAX_UINT64 = uint64(2 ** 64 - 1); +address constant ADDRESS_ZERO = address(0x0); +address constant NO_CONDITION = ADDRESS_ZERO; + +// Actors +address constant ALICE_ADDRESS = address(0xa11ce); +address constant BOB_ADDRESS = address(0xB0B); +address constant CAROL_ADDRESS = address(0xc4601); +address constant DAVID_ADDRESS = address(0xd471d); diff --git a/test/fork/e2eV2.t.sol b/test/fork/e2eV2.t.sol index ca7468d..0dcc4ba 100644 --- a/test/fork/e2eV2.t.sol +++ b/test/fork/e2eV2.t.sol @@ -1,6 +1,6 @@ pragma solidity ^0.8.17; -import {Test} from "forge-std/Test.sol"; +import {AragonTest} from "../base/AragonTest.sol"; import {console2 as console} from "forge-std/console2.sol"; import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; @@ -18,15 +18,18 @@ import {IGaugeVote} from "src/voting/ISimpleGaugeVoter.sol"; import {VotingEscrow, Lock, QuadraticIncreasingEscrow, ExitQueue, SimpleGaugeVoter, SimpleGaugeVoterSetup, ISimpleGaugeVoterSetupParams} from "src/voting/SimpleGaugeVoterSetup.sol"; import {GaugesDaoFactory, GaugePluginSet, Deployment} from "src/factory/GaugesDaoFactory.sol"; -import {Deploy, DeploymentParameters} from "script/Deploy.s.sol"; +import {DeployGauges, DeploymentParameters} from "script/DeployGauges.s.sol"; interface IERC20Mint is IERC20 { function mint(address _to, uint256 _amount) external; } contract GhettoMultisig { - function approveCallerToSpendTokenWithID(address _token, uint256 _id) external { - _token.call(abi.encodeWithSignature("approve(address,uint256)", msg.sender, _id)); + function approveCallerToSpendTokenWithID( + address _token, + uint256 _id + ) external returns (bool, bytes memory) { + return _token.call(abi.encodeWithSignature("approve(address,uint256)", msg.sender, _id)); } } @@ -49,7 +52,7 @@ contract MultisigReceiver is GhettoMultisig { * 4. A more robust suite for admininstration of the contracts * 5. Ability to connect to an existing deployment and test on the real network */ -contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveTokenStorage { +contract TestE2EV2 is AragonTest, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveTokenStorage { error VotingInactive(); error OnlyEscrow(); error GaugeDoesNotExist(address _pool); @@ -79,7 +82,7 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke DAO dao; IERC20Mint token; - MultisigReceiver jordisMultisig; + MultisigReceiver carolsMultisig; address[] signers; @@ -94,7 +97,7 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke /*/////////////////////////////////////////////////////////////// Setup - /////////////////////////////////////////////balanceCarlos////////////////*/ + /////////////////////////////////////////////////////////////*/ /// The test here will run in 2 modes: /// 1. Local Mode (Not yet supported): we deploy the OSx contracts locally using mocks to expedite testing @@ -102,11 +105,11 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke /// 3. Fork Mode: Existing (Supported): we don't deploy via the factory, we use the existing contract for everything function setUp() public { // deploy the deploy script - Deploy deploy = new Deploy(); + DeployGauges deploy = new DeployGauges(); // fetch the deployment parameters DeploymentParameters memory deploymentParameters = deploy.getDeploymentParameters( - vm.envBool("DEPLOY_AS_PRODUCTION") + vm.envOr("MINT_TEST_TOKENS", false) ); signers = deploy.readMultisigMembers(); @@ -118,9 +121,8 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke // setup OSx mocks // write the addresses } - // deploy the contracts via the factory - if (_getTestMode() == TestMode.ForkDeploy) { + else if (_getTestMode() == TestMode.ForkDeploy) { // random ens domain deploymentParameters.voterEnsSubdomain = _hToS( keccak256(abi.encodePacked("gauges", block.timestamp)) @@ -134,7 +136,7 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke } // connect to the existing factory to fetch the contract addresses else if (_getTestMode() == TestMode.ForkExisting) { - address factoryAddress = vm.envAddress("FACTORY"); + address factoryAddress = vm.envOr("FACTORY_ADDRESS", address(0)); if (factoryAddress == address(0)) { revert("Factory address not set"); } @@ -462,30 +464,24 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke //////////////////////////////////////////////////////////////*/ /// here we walkthrough a user journey with 3 users - /// carlos will have 2 locks, holding both at the same time - /// javi will have 1 lock minted at the same time as user 1 holds both - /// carlos will mint his lock for him - /// jordi will have 1 lock minted after user 1 has exited one of their locks - /// jordi will have a smart contract wallet + /// alice will have 2 locks, holding both at the same time + /// bob will have 1 lock minted at the same time as user 1 holds both + /// alice will mint his lock for him + /// carol will have 1 lock minted after user 1 has exited one of their locks + /// carol will have a smart contract wallet /// we will have them create the lock, vote across a couple epochs, and then exit /// we will also have them attempt to circumvent the system and fail /// finally we will define one attacker who will attempt to attack the system and fail - // 3 caballeros - address carlos = address(0xca7105); - address javi = address(0x7af1); - address jordi = address(0x707d1); - - uint balanceCarlos = 1000 ether; - uint balanceJavi = 0 ether; - uint balanceJordi = 1_234 ether; + uint balanceAlice = 1000 ether; + uint balanceBob = 0 ether; + uint balanceCarol = 1_234 ether; - uint depositCarlos0 = 250 ether; - uint depositCarlos1 = 500 ether; - uint depositCarlosJavi = 250 ether; + uint depositAlice0 = 250 ether; + uint depositAlice1 = 500 ether; + uint depositAliceBob = 250 ether; - // 1 attacker - address jordan = address(0x707da); + // 1 attacker (david) uint epochStartTime; @@ -500,102 +496,94 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke vm.warp(nextEpoch); epochStartTime = block.timestamp; - // set up labels - { - vm.label(carlos, "Carlos"); - vm.label(javi, "Javi"); - vm.label(jordi, "Jordi"); - vm.label(jordan, "Jordan"); - } - // first we give the guys each some tokens of the underlying { vm.startPrank(distributor); { - token.transfer(carlos, balanceCarlos); - token.transfer(jordi, balanceJordi); + token.transfer(alice, balanceAlice); + token.transfer(carol, balanceCarol); } vm.stopPrank(); } - // carlos goes first and makes the first deposit, it's at the start of the + // alice goes first and makes the first deposit, it's at the start of the // week, so we would expect him to be warm by the end of the week if using <6 day - // we wait a couple of days and he makes a deposit for javi + // we wait a couple of days and he makes a deposit for bob // we expect his warmup to carryover to the next week // we expect both of their locks to start accruing voting power on the same day { goToEpochStartPlus(1 days); - vm.startPrank(carlos); + vm.startPrank(alice); { - token.approve(address(escrow), balanceCarlos); + token.approve(address(escrow), balanceAlice); - escrow.createLock(depositCarlos0); + escrow.createLock(depositAlice0); goToEpochStartPlus(6 days); - escrow.createLockFor(depositCarlosJavi, javi); + escrow.createLockFor(depositAliceBob, bob); } vm.stopPrank(); - // check carlos has token 1, javi has token 2 - assertEq(lock.ownerOf(1), carlos, "Carlos should own token 1"); - assertEq(lock.ownerOf(2), javi, "Javi should own token 2"); + // check alice has token 1, bob has token 2 + assertEq(lock.ownerOf(1), alice, "Alice should own token 1"); + assertEq(lock.ownerOf(2), bob, "Bob should own token 2"); // check the token points written TokenPoint memory tp1_1 = curve.tokenPointHistory(1, 1); TokenPoint memory tp2_1 = curve.tokenPointHistory(2, 1); - assertEq(tp1_1.bias, depositCarlos0, "Carlos point 1 should have the correct bias"); - assertEq(tp2_1.bias, depositCarlosJavi, "Javi point should have the correct bias"); + assertEq(tp1_1.bias, depositAlice0, "Alice point 1 should have the correct bias"); + assertEq(tp2_1.bias, depositAliceBob, "Bob point should have the correct bias"); assertEq( tp1_1.checkpointTs, epochStartTime + clock.checkpointInterval(), - "Carlos point should have the correct checkpoint" + "Alice point should have the correct checkpoint" ); assertEq( tp2_1.checkpointTs, epochStartTime + clock.checkpointInterval(), - "Javi point should have the correct checkpoint" + "Bob point should have the correct checkpoint" ); assertEq( tp1_1.writtenTs, epochStartTime + 1 days, - "Carlos point should have the correct written timestamp" + "Alice point should have the correct written timestamp" ); assertEq( tp2_1.writtenTs, epochStartTime + 6 days, - "Javi point should have the correct written timestamp" + "Bob point should have the correct written timestamp" ); // check the contract has the correct total assertEq( escrow.totalLocked(), - depositCarlos0 + depositCarlosJavi, + depositAlice0 + depositAliceBob, "Total locked should be the sum of the two deposits" ); // checked the locked balances and the token points assertEq( escrow.locked(1).amount, - depositCarlos0, - "Carlos should have the correct amount locked" + depositAlice0, + "Alice should have the correct amount locked" ); assertEq( escrow.locked(2).amount, - depositCarlosJavi, - "Javi should have the correct amount locked" + depositAliceBob, + "Bob should have the correct amount locked" ); - // start date in the future - carlos will not be warm as his lock is not active yet - assertFalse(curve.isWarm(1), "Carlos should not be warm"); - assertFalse(curve.isWarm(2), "Javi should not be warm"); + // start date in the future - alice will not be warm as his lock is not active yet + assertFalse(curve.isWarm(1), "Alice should not be warm"); + assertFalse(curve.isWarm(2), "Bob should not be warm"); - assertEq(escrow.votingPower(1), 0, "Carlos should have no voting power"); - assertEq(escrow.votingPower(2), 0, "Javi should have no voting power"); + assertEq(escrow.votingPower(1), 0, "Alice should have no voting power"); + assertEq(escrow.votingPower(2), 0, "Bob should have no voting power"); assertEq( escrow.locked(1).start, @@ -608,14 +596,14 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke "Both locks should start at the next checkpoint" ); - // fast forward to the checkpoint interval carlos is warm and has voting power, javi is not + // fast forward to the checkpoint interval alice is warm and has voting power, bob is not goToEpochStartPlus(clock.checkpointInterval()); - assertEq(escrow.votingPower(1), depositCarlos0, "Carlos should have voting power"); - assertTrue(curve.isWarm(1), "Carlos should not be warm"); + assertEq(escrow.votingPower(1), depositAlice0, "Alice should have voting power"); + assertTrue(curve.isWarm(1), "Alice should not be warm"); - assertEq(escrow.votingPower(2), 0, "Javi should not have the correct voting power"); - assertFalse(curve.isWarm(2), "Javi should not be warm"); + assertEq(escrow.votingPower(2), 0, "Bob should not have the correct voting power"); + assertFalse(curve.isWarm(2), "Bob should not be warm"); } // we fast forward 4 weeks and check the expected balances @@ -626,41 +614,41 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke // we could check a < x < b, but checking x exactly is tedious } - // we have carlos make a second deposit and validate that his total voting power is initially unchanged + // we have alice make a second deposit and validate that his total voting power is initially unchanged { - vm.startPrank(carlos); + vm.startPrank(alice); { - escrow.createLock(depositCarlos1); + escrow.createLock(depositAlice1); } vm.stopPrank(); // check the token points written TokenPoint memory tp1_2 = curve.tokenPointHistory(3, 1); - assertEq(tp1_2.bias, depositCarlos1, "Carlos point 2 should have the correct bias"); + assertEq(tp1_2.bias, depositAlice1, "Alice point 2 should have the correct bias"); assertEq( tp1_2.checkpointTs, epochStartTime + 4 weeks + clock.checkpointInterval(), - "Carlos point should have the correct checkpoint" + "Alice point should have the correct checkpoint" ); assertEq( tp1_2.writtenTs, epochStartTime + 4 weeks, - "Carlos point should have the correct written timestamp" + "Alice point should have the correct written timestamp" ); // check the voting power is unchanged (my boi aint warm) assertEq( escrow.votingPower(3), 0, - "Carlos should have no voting power on the second lock" + "Alice should have no voting power on the second lock" ); - assertFalse(curve.isWarm(3), "Carlos should not be warm on the second lock"); + assertFalse(curve.isWarm(3), "Alice should not be warm on the second lock"); // check the total voting power on the escrow assertEq( escrow.totalLocked(), - depositCarlos0 + depositCarlos1 + depositCarlosJavi, + depositAlice0 + depositAlice1 + depositAliceBob, "Total locked should be the sum of the two deposits" ); @@ -669,9 +657,9 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke curve.tokenPointHistory(1, 1).checkpointTs; assertEq( - escrow.votingPowerForAccount(carlos), - curve.getBias(timeElapsedSinceFirstLock, depositCarlos0), - "Carlos should only have the first lock active" + escrow.votingPowerForAccount(alice), + curve.getBias(timeElapsedSinceFirstLock, depositAlice0), + "Alice should only have the first lock active" ); } // we then fast forward 1 week and check that his voting power has increased as expected with the new lock @@ -684,20 +672,20 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke // elased time is zero so should be exactly equal to the bias assertEq( - escrow.votingPowerForAccount(carlos), - curve.getBias(timeElapsedSinceFirstLock, depositCarlos0) + depositCarlos1, - "Carlos should now have the correct aggregate voting power" + escrow.votingPowerForAccount(alice), + curve.getBias(timeElapsedSinceFirstLock, depositAlice0) + depositAlice1, + "Alice should now have the correct aggregate voting power" ); } - // jordan tries to enter the queue with one of their locks + // david tries to enter the queue with one of their locks { - vm.startPrank(jordan); + vm.startPrank(david); { bytes memory erc721ownererr = "ERC721: caller is not token owner or approved"; for (uint i = 1; i <= 3; i++) { vm.expectRevert(OnlyEscrow.selector); - queue.queueExit(i, jordan); + queue.queueExit(i, david); vm.expectRevert(erc721ownererr); escrow.beginWithdrawal(i); @@ -710,7 +698,7 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke { assertFalse(clock.votingActive(), "Voting should not be active"); - address[3] memory stakers = [carlos, javi, jordi]; + address[3] memory stakers = [alice, bob, carol]; GaugeVote[] memory votes = new GaugeVote[](0); for (uint i = 0; i < 3; i++) { address staker = stakers[i]; @@ -765,7 +753,7 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke GaugeVote[] memory incorrectVotes = new GaugeVote[](1); incorrectVotes[0] = GaugeVote({gauge: address(123), weight: 1}); - vm.startPrank(carlos); + vm.startPrank(alice); { vm.expectRevert(abi.encodeWithSelector(GaugeDoesNotExist.selector, address(123))); voter.vote(1, incorrectVotes); @@ -807,13 +795,13 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke ); } - // jordan tries voting for someone else and fails + // david tries voting for someone else and fails { GaugeVote[] memory votes = new GaugeVote[](2); votes[0] = GaugeVote({gauge: gauge0, weight: 1}); votes[1] = GaugeVote({gauge: gauge1, weight: 1}); - vm.startPrank(jordan); + vm.startPrank(david); { vm.expectRevert(NotApprovedOrOwner.selector); voter.vote(1, votes); @@ -825,61 +813,61 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke vm.stopPrank(); } - // the boys vote: carlos votes with multiple and jord with a single + // the boys vote: alice votes with multiple and carol with a single { GaugeVote[] memory votes = new GaugeVote[](2); - // carlos is 50 50 + // alice is 50 50 votes[0] = GaugeVote({gauge: gauge0, weight: 1}); votes[1] = GaugeVote({gauge: gauge1, weight: 1}); uint[] memory ids = new uint[](2); ids[0] = 1; ids[1] = 3; - vm.startPrank(carlos); + vm.startPrank(alice); { voter.voteMultiple(ids, votes); } vm.stopPrank(); - // javi votes for the one gauge + // bob votes for the one gauge votes = new GaugeVote[](1); votes[0] = GaugeVote({gauge: gauge0, weight: 1}); - vm.startPrank(javi); + vm.startPrank(bob); { voter.vote(2, votes); } vm.stopPrank(); - // check the votes - we should have all of javi's votes (id 2) for gauge 0 - // carlos' votes should be split between the two gauges - // in total the second gauge should have 50% of the votes of carlos' votes - // and the first 100% of javi's votes + 50% of carlos' votes + // check the votes - we should have all of bob's votes (id 2) for gauge 0 + // alice' votes should be split between the two gauges + // in total the second gauge should have 50% of the votes of alice' votes + // and the first 100% of bob's votes + 50% of alice' votes assertEq( voter.votes(1, gauge0), escrow.votingPower(1) / 2, - "Carlos 1 g 0 should have the correct votes" + "Alice 1 g 0 should have the correct votes" ); assertEq( voter.votes(1, gauge1), escrow.votingPower(1) / 2, - "Carlos 1 g 1 should have the correct votes" + "Alice 1 g 1 should have the correct votes" ); assertEq( voter.votes(2, gauge0), escrow.votingPower(2), - "Javi should have the correct votes" + "Bob should have the correct votes" ); assertEq( voter.votes(3, gauge0), escrow.votingPower(3) / 2, - "Carlos 3 g 0 should have the correct votes" + "Alice 3 g 0 should have the correct votes" ); assertEq( voter.votes(3, gauge1), escrow.votingPower(3) / 2, - "Carlos 3 g 1 should have the correct votes" + "Alice 3 g 1 should have the correct votes" ); // check the gauge votes @@ -894,25 +882,25 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke } } - // jordi create a deposit mid vote and tries to vote - he should have no voting power + // carol create a deposit mid vote and tries to vote - he should have no voting power { - vm.startPrank(jordi); + vm.startPrank(carol); { - token.approve(address(escrow), balanceJordi); + token.approve(address(escrow), balanceCarol); // bad contract first GhettoMultisig badMultisig = new GhettoMultisig(); vm.expectRevert("ERC721: transfer to non ERC721Receiver implementer"); - escrow.createLockFor(balanceJordi, address(badMultisig)); + escrow.createLockFor(balanceCarol, address(badMultisig)); // he fixes it - jordisMultisig = new MultisigReceiver(); + carolsMultisig = new MultisigReceiver(); - escrow.createLockFor(balanceJordi, address(jordisMultisig)); + escrow.createLockFor(balanceCarol, address(carolsMultisig)); - // allow jordi to vote on behalf of his msig - jordisMultisig.approveCallerToSpendTokenWithID(address(lock), 4); + // allow carol to vote on behalf of his msig + carolsMultisig.approveCallerToSpendTokenWithID(address(lock), 4); GaugeVote[] memory votes = new GaugeVote[](2); votes[0] = GaugeVote({gauge: gauge0, weight: 1}); @@ -924,51 +912,51 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke vm.stopPrank(); } - // javi updates his vote + // bob updates his vote { GaugeVote[] memory votes = new GaugeVote[](1); votes[0] = GaugeVote({gauge: gauge1, weight: 1}); - vm.startPrank(javi); + vm.startPrank(bob); { voter.vote(2, votes); } vm.stopPrank(); - // check the votes - we should have all of javi's votes (id 2) for gauge 1 - // carlos' votes should be split between the two gauges - // in total the second gauge should have 100% of Javi's votes + 50% of the votes of carlos' votes - // and the first 50% of carlos' votes + // check the votes - we should have all of bob's votes (id 2) for gauge 1 + // alice' votes should be split between the two gauges + // in total the second gauge should have 100% of Bob's votes + 50% of the votes of alice' votes + // and the first 50% of alice' votes assertEq( voter.votes(1, gauge0), escrow.votingPower(1) / 2, - "Carlos 1 g 0 should have the correct votes" + "Alice 1 g 0 should have the correct votes" ); assertEq( voter.votes(1, gauge1), escrow.votingPower(1) / 2, - "Carlos 1 g 1 should have the correct votes" + "Alice 1 g 1 should have the correct votes" ); - assertEq(voter.votes(2, gauge0), 0, "Javi should have the correct votes"); + assertEq(voter.votes(2, gauge0), 0, "Bob should have the correct votes"); assertEq( voter.votes(2, gauge1), escrow.votingPower(2), - "Javi should have the correct votes" + "Bob should have the correct votes" ); assertEq( voter.votes(3, gauge0), escrow.votingPower(3) / 2, - "Carlos 3 g 0 should have the correct votes" + "Alice 3 g 0 should have the correct votes" ); assertEq( voter.votes(3, gauge1), escrow.votingPower(3) / 2, - "Carlos 3 g 1 should have the correct votes" + "Alice 3 g 1 should have the correct votes" ); // check the gauge votes @@ -996,8 +984,8 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke assertFalse(clock.votingActive(), "Voting should not be active"); - // carlos tries to exit - vm.startPrank(carlos); + // alice tries to exit + vm.startPrank(alice); { vm.expectRevert(VotingInactive.selector); escrow.resetVotesAndBeginWithdrawal(1); @@ -1008,46 +996,46 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke vm.stopPrank(); } - // we wait till voting is over and they begin the exit - carlos does anyhow + // we wait till voting is over and they begin the exit - alice does anyhow // we check he can't exit early and someone can't exit for him { goToEpochStartPlus(8 weeks + 1 hours); - vm.startPrank(carlos); + vm.startPrank(alice); { lock.approve(address(escrow), 1); escrow.resetVotesAndBeginWithdrawal(1); } vm.stopPrank(); - // carlos doesnt have the nft - its in the queue but he has a ticket - assertEq(lock.ownerOf(1), address(escrow), "Carlos should not own the nft"); - assertEq(queue.queue(1).holder, carlos, "Carlos should be in the queue"); + // alice doesnt have the nft - its in the queue but he has a ticket + assertEq(lock.ownerOf(1), address(escrow), "Alice should not own the nft"); + assertEq(queue.queue(1).holder, alice, "Alice should be in the queue"); // exit date should be the next checkpoint assertEq( queue.queue(1).exitDate, epochStartTime + 8 weeks + clock.checkpointInterval(), - "Carlos should be able to exit at the next checkpoint" + "Alice should be able to exit at the next checkpoint" ); // second user point wrttien TokenPoint memory tp1_2 = curve.tokenPointHistory(1, 2); - assertEq(tp1_2.bias, 0, "Carlos point 1_2 should have the correct bias"); + assertEq(tp1_2.bias, 0, "Alice point 1_2 should have the correct bias"); assertEq( tp1_2.checkpointTs, epochStartTime + 8 weeks + clock.checkpointInterval(), - "Carlos point should have the correct checkpoint" + "Alice point should have the correct checkpoint" ); assertEq( tp1_2.writtenTs, epochStartTime + 8 weeks + 1 hours, - "Carlos point should have the correct written timestamp" + "Alice point should have the correct written timestamp" ); // he can't exit early - vm.startPrank(carlos); + vm.startPrank(alice); { vm.expectRevert(CannotExit.selector); escrow.withdraw(1); @@ -1069,15 +1057,15 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke // he should have his original amount back, minus any fees assertEq( - token.balanceOf(carlos), - depositCarlos0 - (queue.feePercent() * depositCarlos0) / 10_000, - "Carlos should have the correct balance after exiting" + token.balanceOf(alice), + depositAlice0 - (queue.feePercent() * depositAlice0) / 10_000, + "Alice should have the correct balance after exiting" ); // check the total locked assertEq( escrow.totalLocked(), - depositCarlos1 + depositCarlosJavi + balanceJordi, + depositAlice1 + depositAliceBob + balanceCarol, "Total locked should be the sum of the two deposits" ); } @@ -1103,31 +1091,31 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke assertEq(queue.cooldown(), 1 weeks, "Queue should have the correct cooldown period"); } - // carlos creates a new lock 12 h the window opens, he should be warm tomorrow + // alice creates a new lock 12 h the window opens, he should be warm tomorrow { goToEpochStartPlus(10 weeks - 12 hours); - vm.startPrank(carlos); + vm.startPrank(alice); { - token.approve(address(escrow), depositCarlos0); - escrow.createLock(depositCarlos0); + token.approve(address(escrow), depositAlice0); + escrow.createLock(depositAlice0); } vm.stopPrank(); // nope goToEpochStartPlus(10 weeks + 12 hours); - assertFalse(curve.isWarm(5), "Carlos should not be warm"); + assertFalse(curve.isWarm(5), "Alice should not be warm"); // +1s goToEpochStartPlus(10 weeks + 12 hours + 1); - assertTrue(curve.isWarm(5), "Carlos should be warm"); + assertTrue(curve.isWarm(5), "Alice should be warm"); } - // javi goes for an exit, he should be able to exit in a week + // bob goes for an exit, he should be able to exit in a week { goToEpochStartPlus(10 weeks + 3 days); - vm.startPrank(javi); + vm.startPrank(bob); { voter.reset(2); lock.approve(address(escrow), 2); @@ -1135,56 +1123,56 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke } vm.stopPrank(); - // javi doesnt have the nft - its in the queue but he has a ticket - assertEq(lock.ownerOf(2), address(escrow), "Javi should not own the nft"); - assertEq(queue.queue(2).holder, javi, "Javi should be in the queue"); + // bob doesnt have the nft - its in the queue but he has a ticket + assertEq(lock.ownerOf(2), address(escrow), "Bob should not own the nft"); + assertEq(queue.queue(2).holder, bob, "Bob should be in the queue"); // exit date should be 1 week from now assertEq( queue.queue(2).exitDate, epochStartTime + 11 weeks + 3 days, - "Javi should be able to exit in a week" + "Bob should be able to exit in a week" ); // go there + 1 and exit goToEpochStartPlus(11 weeks + 3 days + 1); - vm.startPrank(javi); + vm.startPrank(bob); { escrow.withdraw(2); } vm.stopPrank(); - // total locked should be carlos' 2 deposits + jordis + // total locked should be alice' 2 deposits + carols assertEq( escrow.totalLocked(), - depositCarlos0 + depositCarlos1 + balanceJordi, + depositAlice0 + depositAlice1 + balanceCarol, "Total locked should be the sum of the deposits" ); } - // jordi tries to send his nft to the lock and queue, and can't but he accidentally sends it to the escrow + // carol tries to send his nft to the lock and queue, and can't but he accidentally sends it to the escrow { - vm.startPrank(jordi); + vm.startPrank(carol); { vm.expectRevert(NotWhitelisted.selector); - lock.transferFrom(jordi, address(queue), 4); + lock.transferFrom(carol, address(queue), 4); vm.expectRevert(NotWhitelisted.selector); - lock.transferFrom(jordi, address(lock), 4); + lock.transferFrom(carol, address(lock), 4); // he sends it to the escrow - lock.transferFrom(address(jordisMultisig), address(escrow), 4); + lock.transferFrom(address(carolsMultisig), address(escrow), 4); } vm.stopPrank(); - assertEq(lock.ownerOf(4), address(escrow), "Jordi should not own the nft"); + assertEq(lock.ownerOf(4), address(escrow), "Carol should not own the nft"); // he can't get it back now :( - vm.startPrank(jordi); + vm.startPrank(carol); { vm.expectRevert("ERC721: caller is not token owner or approved"); - lock.transferFrom(address(escrow), jordi, 4); + lock.transferFrom(address(escrow), carol, 4); } vm.stopPrank(); } @@ -1195,39 +1183,39 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke actions[0] = IDAO.Action({ to: address(lock), value: 0, - data: abi.encodeWithSelector(lock.setWhitelisted.selector, address(jordi), true) + data: abi.encodeWithSelector(lock.setWhitelisted.selector, address(carol), true) }); actions[1] = IDAO.Action({ to: address(escrow), value: 0, - data: abi.encodeWithSelector(escrow.sweepNFT.selector, 4, jordi) + data: abi.encodeWithSelector(escrow.sweepNFT.selector, 4, carol) }); actions[2] = IDAO.Action({ to: address(lock), value: 0, - data: abi.encodeWithSelector(lock.setWhitelisted.selector, address(jordi), false) + data: abi.encodeWithSelector(lock.setWhitelisted.selector, address(carol), false) }); _buildSignProposal(actions); // check he has it - assertEq(lock.ownerOf(4), jordi, "Jordi should own the nft"); + assertEq(lock.ownerOf(4), carol, "Carol should own the nft"); } - // jordan convinces the dev team to give him sweeper access and tries to rug all the tokens, he cant + // david convinces the dev team to give him sweeper access and tries to rug all the tokens, he cant { IDAO.Action[] memory actions = new IDAO.Action[](1); actions[0] = IDAO.Action({ to: address(dao), value: 0, - data: abi.encodeCall(dao.grant, (address(escrow), jordan, escrow.SWEEPER_ROLE())) + data: abi.encodeCall(dao.grant, (address(escrow), david, escrow.SWEEPER_ROLE())) }); _buildSignProposal(actions); vm.prank(distributor); - token.transfer(jordan, 100 ether); + token.transfer(david, 100 ether); - vm.startPrank(jordan); + vm.startPrank(david); { vm.expectRevert(NothingToSweep.selector); escrow.sweep(); @@ -1235,11 +1223,11 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke // however he sends some of his own tokens and can get those out token.transfer(address(escrow), 100 ether); - assertEq(token.balanceOf(jordan), 0, "Jordan should have no tokens"); + assertEq(token.balanceOf(david), 0, "David should have no tokens"); escrow.sweep(); - assertEq(token.balanceOf(jordan), 100 ether, "Jordan should have his tokens"); + assertEq(token.balanceOf(david), 100 ether, "David should have his tokens"); } vm.stopPrank(); } @@ -1249,7 +1237,7 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke // warp to a voting window goToEpochStartPlus(12 weeks + 2 hours); - vm.startPrank(carlos); + vm.startPrank(alice); { lock.approve(address(escrow), 3); lock.approve(address(escrow), 5); @@ -1259,7 +1247,7 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke } vm.stopPrank(); - vm.startPrank(jordi); + vm.startPrank(carol); { lock.approve(address(escrow), 4); escrow.beginWithdrawal(4); @@ -1269,16 +1257,16 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke // fast forward like 5 weeks goToEpochStartPlus(16 weeks); - // carlos exits - vm.startPrank(carlos); + // alice exits + vm.startPrank(alice); { escrow.withdraw(3); escrow.withdraw(5); } vm.stopPrank(); - // jordi exits - vm.startPrank(jordi); + // carol exits + vm.startPrank(carol); { escrow.withdraw(4); } @@ -1310,17 +1298,18 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke //////////////////////////////////////////////////////////////*/ function _getTestMode() internal view returns (TestMode) { - string memory mode = vm.envString("FORK_TEST_MODE"); - if (keccak256(abi.encodePacked(mode)) == keccak256(abi.encodePacked("fork-deploy"))) { + // FORK_TEST_MODE is defined on the Makefile, depending on the target + string memory mode = vm.envOr("FORK_TEST_MODE", string("new-factory")); + if (keccak256(abi.encodePacked(mode)) == keccak256(abi.encodePacked("new-factory"))) { return TestMode.ForkDeploy; } else if ( - keccak256(abi.encodePacked(mode)) == keccak256(abi.encodePacked("fork-existing")) + keccak256(abi.encodePacked(mode)) == keccak256(abi.encodePacked("existing-factory")) ) { return TestMode.ForkExisting; } else if (keccak256(abi.encodePacked(mode)) == keccak256(abi.encodePacked("local"))) { - revert("Local mode not yet supported"); + return TestMode.Local; } else { - revert("Invalid test mode - valid options are fork-deploy, fork-existing, local"); + revert("Invalid test mode - valid options are new-factory, existing-factory, local"); } } @@ -1395,7 +1384,7 @@ contract TestE2EV2 is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveToke } catch {} // next we just try a good old fashioned find a whale and rug them in the test - address whale = vm.envAddress("TOKEN_TEST_WHALE"); + address whale = vm.envOr("TEST_TOKEN_WHALE", address(0)); if (whale == address(0)) { return false; }