diff --git a/Makefile b/Makefile index 212c43a..5cecee4 100644 --- a/Makefile +++ b/Makefile @@ -1,189 +1,74 @@ -.DEFAULT_TARGET: help - -# Import the .env files and export their values (ignore any error if missing) +# include .env file and export its env vars +# (-include to ignore error if it does not exist) -include .env --include .env.test - -# RULE SPECIFIC ENV VARS [optional] - -# 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\?" - -# 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 ./$@ $^ +# linux: allow shell scripts to be executed +allow-scripts:; chmod +x ./coverage.sh -lcov.info: $(TEST_COVERAGE_SRC_FILES) - forge coverage --no-match-path $(FORK_TEST_WILDCARD) --report lcov +# init the repo +install :; make allow-scripts && forge build -: ## +# 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" #### 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-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) +# 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 + +ft-mode-migration :; forge test --match-contract TestMigrate \ + --rpc-url https://mainnet.mode.network/ \ + --fork-block-number 17215462 \ + -vv + + + + +#### Deployments #### + +deploy-preview-mode-sepolia :; forge script DeployGauges \ + --rpc-url https://sepolia.mode.network \ + --private-key $(DEPLOYMENT_PRIVATE_KEY) \ + -vvvvv + +deploy-mode-sepolia :; forge script DeployGauges \ + --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 diff --git a/Makefile-pro b/Makefile-pro new file mode 100644 index 0000000..95f7b35 --- /dev/null +++ b/Makefile-pro @@ -0,0 +1,176 @@ +.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-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\?" + +# 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/sh + +# 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: chmod +x ./coverage.sh && ./coverage.sh + +#### 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-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/audits/AUDIT_2.md b/audits/AUDIT_2.md index 8767eb8..4e975e8 100644 --- a/audits/AUDIT_2.md +++ b/audits/AUDIT_2.md @@ -1,4 +1,9 @@ -# Notes for the auditors for the second audit +Aragon's first version of ve governance underwent 2 audits by Halborn and Blocksec, as well as an internal review by senior engineering in Aragon. The findings, changelog and associated PRs are included in the document below. + +1. [Summary of Changes](#summary-of-changes) +2. [Audit Notes](#notes-from-the-halborn-audit) + +# Summary of changes The changes in the second audit are a relatively small set of contract changes. Overall there are 2 things we are looking to address: @@ -16,3 +21,36 @@ For DAOs wishing to start new governance 'seasons', there is a requirement for v - a: lock, add NFT metadata URI - b: voting contract: permit resets outside of voting windows so users can unstake more easily - c: move from a quadratic -> linear voting curve + +## Additional changes surfaced during the audit + +1. Add versioning to the Smart contracts with changed behaviours, to improve traceability + +- `VotingEscrowIncreasing` -> `VotingEscrowIncreasing1_1_0` +- `SimpleGaugeVoter` -> `SimpleGaugeVoter1_1_0` + +2. Add a snapshot of the voting power inside the `Migrate` event + +3. Add `nonReentrant` to the `migrateFrom` function + +# Notes from the Halborn Audit + +https://www.halborn.com/portal/reports/ve-governance-updates + +## Addressed Issues + +Below issues were addressed in [PR 43](https://github.com/aragon/ve-governance/pull/43) + +| Severity | Issue | Comment | +| -------- | ------------------------------------------- | ---------------------------------- | +| L | (HAL-01) Deposits allowed during migration | Disabled deposits during migration | +| I | (HAL-04) Ordering for nonReentrant modifier | Changed order in Lock.sol | +| I | (HAL-05) Typos | | + +## Acknowledged Issues + +| Severity | Issue | Comment | +| -------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| L | (HAL-03) Potential Lockup after `sweepNFT` | As the NFT is non transferrable, by default the DAO needs to also authorise a transfer before the NFT can be sent back to the address via sweep. We assume the DAO will check the address is capable of interacting with the NFT during this authorisation. | +| I | (HAL-06) Floating pragma | Fixed pragma can cause difficulties integrating with external codebases. We prefer to use floating. | +| I | (HAL-02) POSSIBLE STORAGE COLLISIONS IN CONTRACT UPGRADES | Discussed the storage collisions with Halborn team and, on review, confirmed the storage layout is consistent with the v1 of the contracts. New changes don't implement any unexpected increases. | diff --git a/coverage.sh b/coverage.sh new file mode 100755 index 0000000..34ba4be --- /dev/null +++ b/coverage.sh @@ -0,0 +1,9 @@ +#!/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/foundry.toml b/foundry.toml index 45dd58f..ee3e61a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,7 +6,7 @@ bytecode_hash = "none" cbor_metadata = false fuzz = { runs = 256 } gas_reports = ["*"] -fs_permissions = [{ access = "read", path = "./script" }] +fs_permissions = [{ access = "read", path = "./" }] libs = ["lib"] optimizer = true optimizer_runs = 10_000 diff --git a/script/Migrate.s.sol b/script/Migrate.s.sol new file mode 100644 index 0000000..275e1ad --- /dev/null +++ b/script/Migrate.s.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {Script, console} from "forge-std/Script.sol"; +import {DAO, IDAO} from "@aragon/osx/core/dao/DAO.sol"; +import {GaugesDaoFactory, DeploymentParameters, Deployment, TokenParameters} from "../src/factory/GaugesDaoFactory.sol"; +import {Multisig, MultisigSetup as MultisigPluginSetup} from "@aragon/osx/plugins/governance/multisig/MultisigSetup.sol"; +import {VotingEscrow, Clock, Lock, QuadraticIncreasingEscrow, ExitQueue, SimpleGaugeVoter, SimpleGaugeVoterSetup, ISimpleGaugeVoterSetupParams} from "src/voting/SimpleGaugeVoterSetup.sol"; +import {PluginRepo} from "@aragon/osx/framework/plugin/repo/PluginRepo.sol"; +import {PluginRepoFactory} from "@aragon/osx/framework/plugin/repo/PluginRepoFactory.sol"; +import {PluginSetupProcessor} from "@aragon/osx/framework/plugin/setup/PluginSetupProcessor.sol"; +import {MockERC20} from "@mocks/MockERC20.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import {GaugesDaoFactory, GaugePluginSet, Deployment} from "src/factory/GaugesDaoFactory.sol"; +import {DeployGauges, DeploymentParameters} from "script/DeployGauges.s.sol"; + +contract Migrate is Script { + GaugesDaoFactory srcFactory; + GaugePluginSet srcMode; + GaugePluginSet srcBPT; + + address srcFactoryAddress = address(0x123); + address dstFactoryAddress = address(0x456); + + Multisig srcMultisig; + DAO srcDAO; + + GaugesDaoFactory dstFactory; + GaugePluginSet dstMode; + GaugePluginSet dstBPT; + + Multisig dstMultisig; + DAO dstDAO; + + 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 { + // 1. get the old and the new factory + srcFactory = GaugesDaoFactory(srcFactoryAddress); + Deployment memory srcDeployment = srcFactory.getDeployment(); + srcMode = srcDeployment.gaugeVoterPluginSets[0]; + srcBPT = srcDeployment.gaugeVoterPluginSets[1]; + srcMultisig = srcDeployment.multisigPlugin; + srcDAO = srcDeployment.dao; + + dstFactory = GaugesDaoFactory(dstFactoryAddress); + Deployment memory dstDeployment = dstFactory.getDeployment(); + dstMode = dstDeployment.gaugeVoterPluginSets[0]; + dstBPT = dstDeployment.gaugeVoterPluginSets[1]; + dstMultisig = dstDeployment.multisigPlugin; + dstDAO = dstDeployment.dao; + + GaugePluginSet[] memory srcGaugePluginSets = new GaugePluginSet[](2); + srcGaugePluginSets[0] = srcMode; + srcGaugePluginSets[1] = srcBPT; + + GaugePluginSet[] memory dstGaugePluginSets = new GaugePluginSet[](2); + dstGaugePluginSets[0] = dstMode; + dstGaugePluginSets[1] = dstBPT; + + for (uint i = 0; i < srcGaugePluginSets.length; i++) { + GaugePluginSet memory srcGaugePluginSet = srcGaugePluginSets[i]; + GaugePluginSet memory dstGaugePluginSet = dstGaugePluginSets[i]; + _upgradeSrcContracts(srcGaugePluginSet, dstGaugePluginSet); + _enableMigrationDst(srcGaugePluginSet, dstGaugePluginSet); + _enableMigrationSrc(srcGaugePluginSet, dstGaugePluginSet); + } + } + + // upgrade src + function _upgradeSrcContracts(GaugePluginSet memory src, GaugePluginSet memory dst) public { + // fetch the implementation contracts for the voter and escrow in the new deploy + address voterImpl = SimpleGaugeVoter(address(dst.plugin)).implementation(); + address escrowImpl = VotingEscrow(address(dst.votingEscrow)).implementation(); + + // first we need to upgrade both contracts + IDAO.Action[] memory actions = new IDAO.Action[](2); + actions[0] = IDAO.Action({ + to: address(src.plugin), + value: 0, + data: abi.encodeCall(src.plugin.upgradeTo, (voterImpl)) + }); + + actions[1] = IDAO.Action({ + to: address(src.votingEscrow), + value: 0, + data: abi.encodeCall(src.votingEscrow.upgradeTo, (escrowImpl)) + }); + + _buildSignProposal(actions, srcMultisig); + } + + function _enableMigrationDst(GaugePluginSet memory src, GaugePluginSet memory dst) public { + // on the destination: + IDAO.Action[] memory actions = new IDAO.Action[](2); + // pause the dst staking contract + actions[0] = IDAO.Action({ + to: address(dst.votingEscrow), + value: 0, + data: abi.encodeCall(dst.votingEscrow.pause, ()) + }); + + // grant the migrator role on the prev staking contract + actions[1] = IDAO.Action({ + to: address(dstDAO), + value: 0, + data: abi.encodeCall( + dstDAO.grant, + ( + address(dst.votingEscrow), + address(src.votingEscrow), + dst.votingEscrow.MIGRATOR_ROLE() + ) + ) + }); + + _buildSignProposal(actions, dstMultisig); + } + + function _enableMigrationSrc(GaugePluginSet memory src, GaugePluginSet memory dst) public { + IDAO.Action[] memory actions = new IDAO.Action[](1); + // pause the dst staking contract + actions[0] = IDAO.Action({ + to: address(src.votingEscrow), + value: 0, + data: abi.encodeCall(src.votingEscrow.enableMigration, (address(dst.votingEscrow))) + }); + + _buildSignProposal(actions, srcMultisig); + } + + function _buildSignProposal( + IDAO.Action[] memory actions, + Multisig multisig + ) internal returns (uint256 proposalId) { + // prank the first signer who will create stuff + proposalId = multisig.createProposal({ + _metadata: "", + _actions: actions, + _allowFailureMap: 0, + _approveProposal: true, + _tryExecution: true, + _startDate: 0, + _endDate: uint64(block.timestamp) + 3 days + }); + + return proposalId; + } + // enable src +} diff --git a/script/multisig-members.json b/script/multisig-members.json index eae9e52..0cffe45 100644 --- a/script/multisig-members.json +++ b/script/multisig-members.json @@ -1,3 +1,6 @@ { - "members": ["0x7771c1510509C0dA515BDD12a57dbDd8C58E5363"] + "members": [ + "0x7771c1510509C0dA515BDD12a57dbDd8C58E5363", + "0x1A1087Bf077f74fb21fD838a8a25Cf9Fe0818450" + ] } diff --git a/src/escrow/increasing/Lock.sol b/src/escrow/increasing/Lock.sol index b88ad73..1a0c007 100644 --- a/src/escrow/increasing/Lock.sol +++ b/src/escrow/increasing/Lock.sol @@ -122,12 +122,12 @@ contract Lock is /// @notice Minting and burning functions that can only be called by the escrow contract /// @dev Safe mint ensures contract addresses are ERC721 Receiver contracts - function mint(address _to, uint256 _tokenId) external onlyEscrow nonReentrant { + function mint(address _to, uint256 _tokenId) external nonReentrant onlyEscrow { _safeMint(_to, _tokenId); } /// @notice Minting and burning functions that can only be called by the escrow contract - function burn(uint256 _tokenId) external onlyEscrow nonReentrant { + function burn(uint256 _tokenId) external nonReentrant onlyEscrow { _burn(_tokenId); } diff --git a/src/escrow/increasing/VotingEscrowIncreasing.sol b/src/escrow/increasing/VotingEscrowIncreasing.sol index e6a9af5..5da0f60 100644 --- a/src/escrow/increasing/VotingEscrowIncreasing.sol +++ b/src/escrow/increasing/VotingEscrowIncreasing.sol @@ -13,7 +13,6 @@ import {IClock} from "@clock/IClock.sol"; import {IEscrowCurveIncreasing as IEscrowCurve} from "./interfaces/IEscrowCurveIncreasing.sol"; import {IExitQueue} from "./interfaces/IExitQueue.sol"; import {IVotingEscrowIncreasing as IVotingEscrow} from "./interfaces/IVotingEscrowIncreasing.sol"; -import {IMigrateable} from "./interfaces/IMigrateable.sol"; // libraries import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; @@ -30,8 +29,7 @@ contract VotingEscrow is ReentrancyGuard, Pausable, DaoAuthorizable, - UUPSUpgradeable, - IMigrateable + UUPSUpgradeable { using SafeERC20 for IERC20; using SafeCast for uint256; @@ -90,76 +88,6 @@ contract VotingEscrow is bool private _lockNFTSet; - /*////////////////////////////////////////////////////////////// - Added: V2 - //////////////////////////////////////////////////////////////*/ - - /// @notice The destination staking contract can add this to allow another address to call - /// the migrate function on it - bytes32 public constant MIGRATOR_ROLE = keccak256("MIGRATOR"); - - /// @notice destination migration contract - address public migrator; - - /// @notice The Escrow Admin can enable migrations by setting a destination migration contract. - /// @dev This function also approves all tokens in this contract to be transferred to the new migrator. - /// @param _migrator The address of the destination migration contract - function enableMigration(address _migrator) external auth(ESCROW_ADMIN_ROLE) { - if (migrator != address(0)) revert MigrationAlreadySet(); - migrator = _migrator; - // we approve max in the event that new deposits happen - IERC20(token).approve(migrator, type(uint256).max); - emit MigrationEnabled(_migrator); - } - - /// @notice Defined on the staking contract being exited from - burn the tokenId and mint a new one. - /// @dev Skips withdrawal queue logic and vote resets - /// @param _tokenId veNFT to migrate from - /// @return newTokenId veNFT created during the migrationg - function migrateFrom(uint256 _tokenId) external returns (uint256 newTokenId) { - // check the migration contract is set and the tokenid is active - if (migrator == address(0)) revert MigrationNotActive(); - if (!IERC721EMB(lockNFT).isApprovedOrOwner(_msgSender(), _tokenId)) revert NotOwner(); - if (votingPower(_tokenId) == 0) revert CannotExit(); - - // the user should be approved - address owner = IERC721EMB(lockNFT).ownerOf(_tokenId); - - // reset votes from voting contract - if (isVoting(_tokenId)) { - ISimpleGaugeVoter(voter).reset(_tokenId); - } - - LockedBalance memory oldLocked = _locked[_tokenId]; - uint256 value = oldLocked.amount; - - // burn the current veNFT and write a zero checkpoint. - _locked[_tokenId] = LockedBalance(0, 0); - totalLocked -= value; - _checkpointClear(_tokenId); - IERC721EMB(lockNFT).burn(_tokenId); - - // createLockFor on the new contract for the owner of the veNFT - newTokenId = VotingEscrow(migrator).migrateTo(value, owner); - - // emit the migrated event - emit Migrated(owner, _tokenId, newTokenId, value); - - return newTokenId; - } - - /// @notice Defined on the destination staking contract. Creates a new veNFT with the old params - /// @dev Skips validations like pause, allowing migration ahead of general release. - /// @param _value The amount of underlying token to be migrated. - /// @param _for The original owner of the lock - /// @return newTokenId the veNFT on the destination staking contract - function migrateTo( - uint256 _value, - address _for - ) external nonReentrant auth(MIGRATOR_ROLE) returns (uint256 newTokenId) { - return _createLockFor(_value, _for); - } - /*////////////////////////////////////////////////////////////// Initialization //////////////////////////////////////////////////////////////*/ @@ -480,6 +408,5 @@ contract VotingEscrow is function _authorizeUpgrade(address) internal virtual override auth(ESCROW_ADMIN_ROLE) {} /// @dev Reserved storage space to allow for layout changes in the future. - /// @dev V2: -1 slot for migrator contract - uint256[38] private __gap; + uint256[39] private __gap; } diff --git a/src/escrow/increasing/VotingEscrowIncreasing_v1_1_0.sol b/src/escrow/increasing/VotingEscrowIncreasing_v1_1_0.sol new file mode 100644 index 0000000..8378589 --- /dev/null +++ b/src/escrow/increasing/VotingEscrowIncreasing_v1_1_0.sol @@ -0,0 +1,488 @@ +/// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +// token interfaces +import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import {IERC20MetadataUpgradeable as IERC20Metadata} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; +import {IERC721EnumerableMintableBurnable as IERC721EMB} from "./interfaces/IERC721EMB.sol"; + +// veGovernance +import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; +import {ISimpleGaugeVoter} from "@voting/ISimpleGaugeVoter.sol"; +import {IClock} from "@clock/IClock.sol"; +import {IEscrowCurveIncreasing as IEscrowCurve} from "./interfaces/IEscrowCurveIncreasing.sol"; +import {IExitQueue} from "./interfaces/IExitQueue.sol"; +import {IVotingEscrowIncreasing as IVotingEscrow} from "./interfaces/IVotingEscrowIncreasing.sol"; +import {IMigrateable} from "./interfaces/IMigrateable.sol"; + +// libraries +import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {SafeCastUpgradeable as SafeCast} from "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; + +// parents +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {ReentrancyGuardUpgradeable as ReentrancyGuard} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import {PausableUpgradeable as Pausable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import {DaoAuthorizableUpgradeable as DaoAuthorizable} from "@aragon/osx/core/plugin/dao-authorizable/DaoAuthorizableUpgradeable.sol"; + +contract VotingEscrowV1_1_0 is + IVotingEscrow, + ReentrancyGuard, + Pausable, + DaoAuthorizable, + UUPSUpgradeable, + IMigrateable +{ + using SafeERC20 for IERC20; + using SafeCast for uint256; + + /// @notice Role required to manage the Escrow curve, this typically will be the DAO + bytes32 public constant ESCROW_ADMIN_ROLE = keccak256("ESCROW_ADMIN"); + + /// @notice Role required to pause the contract - can be given to emergency contracts + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER"); + + /// @notice Role required to withdraw underlying tokens from the contract + bytes32 public constant SWEEPER_ROLE = keccak256("SWEEPER"); + + /*////////////////////////////////////////////////////////////// + NFT Data + //////////////////////////////////////////////////////////////*/ + + /// @notice Decimals of the voting power + uint8 public constant decimals = 18; + + /// @notice Minimum deposit amount + uint256 public minDeposit; + + /// @notice Auto-incrementing ID for the most recently created lock, does not decrease on withdrawal + uint256 public lastLockId; + + /// @notice Total supply of underlying tokens deposited in the contract + uint256 public totalLocked; + + /// @dev tracks the locked balance of each NFT + mapping(uint256 => LockedBalance) private _locked; + + /*////////////////////////////////////////////////////////////// + Helper Contracts + //////////////////////////////////////////////////////////////*/ + + /// @notice Address of the underlying ERC20 token. + /// @dev Only tokens with 18 decimals and no transfer fees are supported + address public token; + + /// @notice Address of the gauge voting contract. + /// @dev We need to ensure votes are not left in this contract before allowing positing changes + address public voter; + + /// @notice Address of the voting Escrow Curve contract that will calculate the voting power + address public curve; + + /// @notice Address of the contract that manages exit queue logic for withdrawals + address public queue; + + /// @notice Address of the clock contract that manages epoch and voting periods + address public clock; + + /// @notice Address of the NFT contract that is the lock + address public lockNFT; + + bool private _lockNFTSet; + + /*////////////////////////////////////////////////////////////// + Added: V2 + //////////////////////////////////////////////////////////////*/ + + /// @notice The destination staking contract can add this to allow another address to call + /// the migrate function on it + bytes32 public constant MIGRATOR_ROLE = keccak256("MIGRATOR"); + + /// @notice destination migration contract + address public migrator; + + /// @notice The Escrow Admin can enable migrations by setting a destination migration contract. + /// @dev This function also approves all tokens in this contract to be transferred to the new migrator. + /// @param _migrator The address of the destination migration contract + function enableMigration(address _migrator) external auth(ESCROW_ADMIN_ROLE) { + if (migrator != address(0)) revert MigrationAlreadySet(); + migrator = _migrator; + // we approve max in the event that new deposits happen + IERC20(token).approve(migrator, type(uint256).max); + emit MigrationEnabled(_migrator); + } + + /// @notice Defined on the staking contract being exited from - burn the tokenId and mint a new one. + /// @dev Skips withdrawal queue logic and vote resets + /// @param _tokenId veNFT to migrate from + /// @return newTokenId veNFT created during the migration + function migrateFrom(uint256 _tokenId) external nonReentrant returns (uint256 newTokenId) { + // check the migration contract is set and the tokenid is active + if (migrator == address(0)) revert MigrationNotActive(); + if (!IERC721EMB(lockNFT).isApprovedOrOwner(_msgSender(), _tokenId)) revert NotOwner(); + + uint256 votingPowerSnapshot = votingPower(_tokenId); + if (votingPowerSnapshot == 0) revert CannotExit(); + + // the user should be approved + address owner = IERC721EMB(lockNFT).ownerOf(_tokenId); + + // reset votes from voting contract + if (isVoting(_tokenId)) { + ISimpleGaugeVoter(voter).reset(_tokenId); + } + + LockedBalance memory oldLocked = _locked[_tokenId]; + uint256 value = oldLocked.amount; + + // burn the current veNFT and write a zero checkpoint. + _locked[_tokenId] = LockedBalance(0, 0); + totalLocked -= value; + _checkpointClear(_tokenId); + IERC721EMB(lockNFT).burn(_tokenId); + + // createLockFor on the new contract for the owner of the veNFT + newTokenId = VotingEscrowV1_1_0(migrator).migrateTo(value, owner); + + // emit the migrated event + emit Migrated(owner, _tokenId, newTokenId, value, votingPowerSnapshot); + + return newTokenId; + } + + /// @notice Defined on the destination staking contract. Creates a new veNFT with the old params + /// @dev Skips validations like pause, allowing migration ahead of general release. + /// @param _value The amount of underlying token to be migrated. + /// @param _for The original owner of the lock + /// @return newTokenId the veNFT on the destination staking contract + function migrateTo( + uint256 _value, + address _for + ) external nonReentrant auth(MIGRATOR_ROLE) returns (uint256 newTokenId) { + return _createLockFor(_value, _for); + } + + /*////////////////////////////////////////////////////////////// + Initialization + //////////////////////////////////////////////////////////////*/ + + constructor() { + _disableInitializers(); + } + + function initialize( + address _token, + address _dao, + address _clock, + uint256 _initialMinDeposit + ) external initializer { + __DaoAuthorizableUpgradeable_init(IDAO(_dao)); + __ReentrancyGuard_init(); + __Pausable_init(); + + if (IERC20Metadata(_token).decimals() != 18) revert MustBe18Decimals(); + token = _token; + clock = _clock; + minDeposit = _initialMinDeposit; + emit MinDepositSet(_initialMinDeposit); + } + + /*////////////////////////////////////////////////////////////// + Admin Setters + //////////////////////////////////////////////////////////////*/ + + /// @notice Sets the curve contract that calculates the voting power + function setCurve(address _curve) external auth(ESCROW_ADMIN_ROLE) { + curve = _curve; + } + + /// @notice Sets the voter contract that tracks votes + function setVoter(address _voter) external auth(ESCROW_ADMIN_ROLE) { + voter = _voter; + } + + /// @notice Sets the exit queue contract that manages withdrawal eligibility + function setQueue(address _queue) external auth(ESCROW_ADMIN_ROLE) { + queue = _queue; + } + + /// @notice Sets the clock contract that manages epoch and voting periods + function setClock(address _clock) external auth(ESCROW_ADMIN_ROLE) { + clock = _clock; + } + + /// @notice Sets the NFT contract that is the lock + /// @dev By default this can only be set once due to the high risk of changing the lock + /// and having the ability to steal user funds. + function setLockNFT(address _nft) external auth(ESCROW_ADMIN_ROLE) { + if (_lockNFTSet) revert LockNFTAlreadySet(); + lockNFT = _nft; + _lockNFTSet = true; + } + + function pause() external auth(PAUSER_ROLE) { + _pause(); + } + + function unpause() external auth(PAUSER_ROLE) { + _unpause(); + } + + function setMinDeposit(uint256 _minDeposit) external auth(ESCROW_ADMIN_ROLE) { + minDeposit = _minDeposit; + emit MinDepositSet(_minDeposit); + } + + /*////////////////////////////////////////////////////////////// + Getters: ERC721 Functions + //////////////////////////////////////////////////////////////*/ + + function isApprovedOrOwner(address _spender, uint256 _tokenId) external view returns (bool) { + return IERC721EMB(lockNFT).isApprovedOrOwner(_spender, _tokenId); + } + + /// @notice Fetch all NFTs owned by an address by leveraging the ERC721Enumerable interface + /// @param _owner Address to query + /// @return tokenIds Array of token IDs owned by the address + function ownedTokens(address _owner) public view returns (uint256[] memory tokenIds) { + IERC721EMB enumerable = IERC721EMB(lockNFT); + uint256 balance = enumerable.balanceOf(_owner); + uint256[] memory tokens = new uint256[](balance); + for (uint256 i = 0; i < balance; i++) { + tokens[i] = enumerable.tokenOfOwnerByIndex(_owner, i); + } + return tokens; + } + + /*/////////////////////////////////////////////////////////////// + Getters: Voting + //////////////////////////////////////////////////////////////*/ + + /// @return The voting power of the NFT at the current block + function votingPower(uint256 _tokenId) public view returns (uint256) { + return votingPowerAt(_tokenId, block.timestamp); + } + + /// @return The voting power of the NFT at a specific timestamp + function votingPowerAt(uint256 _tokenId, uint256 _t) public view returns (uint256) { + return IEscrowCurve(curve).votingPowerAt(_tokenId, _t); + } + + /// @return The total voting power at the current block + /// @dev Currently unsupported + function totalVotingPower() external view returns (uint256) { + return totalVotingPowerAt(block.timestamp); + } + + /// @return The total voting power at a specific timestamp + /// @dev Currently unsupported + function totalVotingPowerAt(uint256 _timestamp) public view returns (uint256) { + return IEscrowCurve(curve).supplyAt(_timestamp); + } + + /// @return The details of the underlying lock for a given veNFT + function locked(uint256 _tokenId) external view returns (LockedBalance memory) { + return _locked[_tokenId]; + } + + /// @return accountVotingPower The voting power of an account at the current block + /// @dev We cannot do historic voting power at this time because we don't current track + /// histories of token transfers. + function votingPowerForAccount( + address _account + ) external view returns (uint256 accountVotingPower) { + uint256[] memory tokens = ownedTokens(_account); + + for (uint256 i = 0; i < tokens.length; i++) { + accountVotingPower += votingPowerAt(tokens[i], block.timestamp); + } + } + + /// @notice Checks if the NFT is currently voting. We require the user to reset their votes if so. + function isVoting(uint256 _tokenId) public view returns (bool) { + return ISimpleGaugeVoter(voter).isVoting(_tokenId); + } + + /*////////////////////////////////////////////////////////////// + ESCROW LOGIC + //////////////////////////////////////////////////////////////*/ + + function createLock(uint256 _value) external nonReentrant whenNotPaused returns (uint256) { + return _createLockFor(_value, _msgSender()); + } + + /// @notice Creates a lock on behalf of someone else. Restricted by default. + function createLockFor( + uint256 _value, + address _to + ) external nonReentrant whenNotPaused returns (uint256) { + return _createLockFor(_value, _to); + } + + /// @dev Deposit `_value` tokens for `_to` starting at next deposit interval + /// @param _value Amount to deposit + /// @param _to Address to deposit + function _createLockFor(uint256 _value, address _to) internal returns (uint256) { + if (_value == 0) revert ZeroAmount(); + if (_value < minDeposit) revert AmountTooSmall(); + if (migrator != address(0)) revert MigrationActive(); + + // query the duration lib to get the next time we can deposit + uint256 startTime = IClock(clock).epochNextCheckpointTs(); + + // increment the total locked supply and get the new tokenId + totalLocked += _value; + uint256 newTokenId = ++lastLockId; + + // write the lock and checkpoint the voting power + LockedBalance memory lock = LockedBalance(_value.toUint208(), startTime.toUint48()); + _locked[newTokenId] = lock; + + // we don't allow edits in this implementation, so only the new lock is used + _checkpoint(newTokenId, lock); + + uint256 balanceBefore = IERC20(token).balanceOf(address(this)); + + // transfer the tokens into the contract + IERC20(token).safeTransferFrom(_msgSender(), address(this), _value); + + // we currently don't support tokens that adjust balances on transfer + if (IERC20(token).balanceOf(address(this)) != balanceBefore + _value) + revert TransferBalanceIncorrect(); + + // mint the NFT before and emit the event to complete the lock + IERC721EMB(lockNFT).mint(_to, newTokenId); + emit Deposit(_to, newTokenId, startTime, _value, totalLocked); + + return newTokenId; + } + + /// @notice Record per-user data to checkpoints. Used by VotingEscrow system. + /// @param _tokenId NFT token ID + /// @dev Old locked balance is unused in the increasing case, at least in this implementation + /// @param _newLocked New locked amount / start lock time for the user + function _checkpoint(uint256 _tokenId, LockedBalance memory _newLocked) private { + IEscrowCurve(curve).checkpoint(_tokenId, LockedBalance(0, 0), _newLocked); + } + + /// @dev resets the voting power for a given tokenId. Checkpoint is written to the end of the epoch. + /// @param _tokenId The tokenId to reset the voting power for + /// @dev We don't need to fetch the old locked balance as it's not used in this implementation + function _checkpointClear(uint256 _tokenId) private { + uint256 checkpointClearTime = IClock(clock).epochNextCheckpointTs(); + IEscrowCurve(curve).checkpoint( + _tokenId, + LockedBalance(0, 0), + LockedBalance(0, checkpointClearTime.toUint48()) + ); + } + + /*////////////////////////////////////////////////////////////// + Exit and Withdraw Logic + //////////////////////////////////////////////////////////////*/ + + /// @notice Resets the votes and begins the withdrawal process for a given tokenId + /// @dev Convenience function, the user must have authorized this contract to act on their behalf. + function resetVotesAndBeginWithdrawal(uint256 _tokenId) external whenNotPaused { + ISimpleGaugeVoter(voter).reset(_tokenId); + beginWithdrawal(_tokenId); + } + + /// @notice Enters a tokenId into the withdrawal queue by transferring to this contract and creating a ticket. + /// @param _tokenId The tokenId to begin withdrawal for. Will be transferred to this contract before burning. + /// @dev The user must not have active votes in the voter contract. + function beginWithdrawal(uint256 _tokenId) public nonReentrant whenNotPaused { + // can't exit if you have votes pending + if (isVoting(_tokenId)) revert CannotExit(); + + // in the event of an increasing curve, 0 voting power means voting isn't active + if (votingPower(_tokenId) == 0) revert CannotExit(); + + address owner = IERC721EMB(lockNFT).ownerOf(_tokenId); + + // we can remove the user's voting power as it's no longer locked + _checkpointClear(_tokenId); + + // transfer NFT to this and queue the exit + IERC721EMB(lockNFT).transferFrom(_msgSender(), address(this), _tokenId); + IExitQueue(queue).queueExit(_tokenId, owner); + } + + /// @notice Withdraws tokens from the contract + function withdraw(uint256 _tokenId) external nonReentrant whenNotPaused { + address sender = _msgSender(); + + // we force the sender to be the ticket holder + if (!(IExitQueue(queue).ticketHolder(_tokenId) == sender)) revert NotTicketHolder(); + + // check that this ticket can exit + if (!(IExitQueue(queue).canExit(_tokenId))) revert CannotExit(); + + LockedBalance memory oldLocked = _locked[_tokenId]; + uint256 value = oldLocked.amount; + + // check for fees to be transferred + // do this before clearing the lock or it will be incorrect + uint256 fee = IExitQueue(queue).exit(_tokenId); + if (fee > 0) { + IERC20(token).safeTransfer(address(queue), fee); + } + + // clear out the token data + _locked[_tokenId] = LockedBalance(0, 0); + totalLocked -= value; + + // Burn the NFT and transfer the tokens to the user + IERC721EMB(lockNFT).burn(_tokenId); + IERC20(token).safeTransfer(sender, value - fee); + + emit Withdraw(sender, _tokenId, value - fee, block.timestamp, totalLocked); + } + + /// @notice withdraw excess tokens from the contract - possibly by accident + function sweep() external nonReentrant auth(SWEEPER_ROLE) { + // if there are extra tokens in the contract + // balance will be greater than the total locked + uint balance = IERC20(token).balanceOf(address(this)); + uint excess = balance - totalLocked; + + // if there isn't revert the tx + if (excess == 0) revert NothingToSweep(); + + // if there is, send them to the caller + IERC20(token).safeTransfer(_msgSender(), excess); + emit Sweep(_msgSender(), excess); + } + + /// @notice the sweeper can send NFTs mistakenly sent to the contract to a designated address + /// @param _tokenId the tokenId to sweep - must be currently in this contract + /// @param _to the address to send the NFT to - must be a whitelisted address for transfers + /// @dev Cannot sweep NFTs that are in the exit queue for obvious reasons + function sweepNFT(uint256 _tokenId, address _to) external nonReentrant auth(SWEEPER_ROLE) { + // if the token id is not in the contract, revert + if (IERC721EMB(lockNFT).ownerOf(_tokenId) != address(this)) revert NothingToSweep(); + + // if the token id is in the queue, we cannot sweep it + if (IExitQueue(queue).ticketHolder(_tokenId) != address(0)) revert CannotExit(); + + IERC721EMB(lockNFT).transferFrom(address(this), _to, _tokenId); + emit SweepNFT(_to, _tokenId); + } + + /*/////////////////////////////////////////////////////////////// + UUPS Upgrade + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns the address of the implementation contract in the [proxy storage slot](https://eips.ethereum.org/EIPS/eip-1967) slot the [UUPS proxy](https://eips.ethereum.org/EIPS/eip-1822) is pointing to. + /// @return The address of the implementation contract. + function implementation() public view returns (address) { + return _getImplementation(); + } + + /// @notice Internal method authorizing the upgrade of the contract via the [upgradeability mechanism for UUPS proxies](https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable) (see [ERC-1822](https://eips.ethereum.org/EIPS/eip-1822)). + function _authorizeUpgrade(address) internal virtual override auth(ESCROW_ADMIN_ROLE) {} + + /// @dev Reserved storage space to allow for layout changes in the future. + /// @dev V1.1.0: -1 slot for migrator contract + uint256[38] private __gap; +} diff --git a/src/escrow/increasing/interfaces/IMigrateable.sol b/src/escrow/increasing/interfaces/IMigrateable.sol index a3c9f91..f696edf 100644 --- a/src/escrow/increasing/interfaces/IMigrateable.sol +++ b/src/escrow/increasing/interfaces/IMigrateable.sol @@ -21,14 +21,17 @@ interface IMigrateableEventsAndErrors { /// @param oldTokenId TokenId burned in the old staking contract /// @param newTokenId TokenId minted in the new staking contract /// @param amount The locked amount migrated between contracts + /// @param votingPower The voting power at the time of migration event Migrated( address indexed owner, uint256 indexed oldTokenId, uint256 indexed newTokenId, - uint256 amount + uint256 amount, + uint256 votingPower ); error MigrationAlreadySet(); error MigrationNotActive(); + error MigrationActive(); } interface IMigrateable is IMigrateableFrom, IMigrateableTo, IMigrateableEventsAndErrors {} diff --git a/src/libs/CurveConstantLib.sol b/src/libs/CurveConstantLib.sol index fedafb5..de503af 100644 --- a/src/libs/CurveConstantLib.sol +++ b/src/libs/CurveConstantLib.sol @@ -12,6 +12,6 @@ library CurveConstantLib { /// @dev linear curve with zero quadratic term int256 internal constant SHARED_QUADRATIC_COEFFICIENT = 0; - /// @dev the maxiumum number of epochs the cure can keep increasing + /// @dev the maximum number of epochs the cure can keep increasing uint256 internal constant MAX_EPOCHS = 5; } diff --git a/src/voting/SimpleGaugeVoterSetup.sol b/src/voting/SimpleGaugeVoterSetup.sol index fdf5db2..66dac2e 100644 --- a/src/voting/SimpleGaugeVoterSetup.sol +++ b/src/voting/SimpleGaugeVoterSetup.sol @@ -14,8 +14,8 @@ import {PermissionLib} from "@aragon/osx/core/permission/PermissionLib.sol"; import {PluginSetup} from "@aragon/osx/framework/plugin/setup/PluginSetup.sol"; // these should be interfaces -import {SimpleGaugeVoter} from "@voting/SimpleGaugeVoter.sol"; -import {VotingEscrow} from "@escrow/VotingEscrowIncreasing.sol"; +import {SimpleGaugeVoterV1_1_0 as SimpleGaugeVoter} from "@voting/SimpleGaugeVoter_v1_1_0.sol"; +import {VotingEscrowV1_1_0 as VotingEscrow} from "@escrow/VotingEscrowIncreasing_v1_1_0.sol"; import {ExitQueue} from "@escrow/ExitQueue.sol"; import {QuadraticIncreasingEscrow} from "@escrow/QuadraticIncreasingEscrow.sol"; import {Clock} from "@clock/Clock.sol"; diff --git a/src/voting/SimpleGaugeVoter_v1_1_0.sol b/src/voting/SimpleGaugeVoter_v1_1_0.sol new file mode 100644 index 0000000..fcdec44 --- /dev/null +++ b/src/voting/SimpleGaugeVoter_v1_1_0.sol @@ -0,0 +1,339 @@ +/// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; +import {IVotingEscrowIncreasing as IVotingEscrow} from "@escrow-interfaces/IVotingEscrowIncreasing.sol"; +import {IClockUser, IClock} from "@clock/IClock.sol"; +import {ISimpleGaugeVoter} from "./ISimpleGaugeVoter.sol"; + +import {ReentrancyGuardUpgradeable as ReentrancyGuard} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import {PausableUpgradeable as Pausable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import {PluginUUPSUpgradeable} from "@aragon/osx/core/plugin/PluginUUPSUpgradeable.sol"; + +contract SimpleGaugeVoterV1_1_0 is + ISimpleGaugeVoter, + IClockUser, + ReentrancyGuard, + Pausable, + PluginUUPSUpgradeable +{ + /// @notice The Gauge admin can can create and manage voting gauges for token holders + bytes32 public constant GAUGE_ADMIN_ROLE = keccak256("GAUGE_ADMIN"); + + /// @notice Address of the voting escrow contract that will track voting power + address public escrow; + + /// @notice Clock contract for epoch duration + address public clock; + + /// @notice The total votes that have accumulated in this contract + uint256 public totalVotingPowerCast; + + /// @notice enumerable list of all gauges that can be voted on + address[] public gaugeList; + + /// @notice address => gauge data + mapping(address => Gauge) public gauges; + + /// @notice gauge => total votes (global) + mapping(address => uint256) public gaugeVotes; + + /// @dev tokenId => tokenVoteData + mapping(uint256 => TokenVoteData) internal tokenVoteData; + + /*/////////////////////////////////////////////////////////////// + Initialization + //////////////////////////////////////////////////////////////*/ + + constructor() { + _disableInitializers(); + } + + function initialize( + address _dao, + address _escrow, + bool _startPaused, + address _clock + ) external initializer { + __PluginUUPSUpgradeable_init(IDAO(_dao)); + __ReentrancyGuard_init(); + __Pausable_init(); + escrow = _escrow; + clock = _clock; + if (_startPaused) _pause(); + } + + /*/////////////////////////////////////////////////////////////// + Modifiers + //////////////////////////////////////////////////////////////*/ + + function pause() external auth(GAUGE_ADMIN_ROLE) { + _pause(); + } + + function unpause() external auth(GAUGE_ADMIN_ROLE) { + _unpause(); + } + + modifier whenVotingActive() { + if (!votingActive()) revert VotingInactive(); + _; + } + + /*/////////////////////////////////////////////////////////////// + Voting + //////////////////////////////////////////////////////////////*/ + + /// @notice extrememly simple for loop. We don't need reentrancy checks in this implementation + /// because the plugin doesn't do anything other than signal. + function voteMultiple( + uint256[] calldata _tokenIds, + GaugeVote[] calldata _votes + ) external nonReentrant whenNotPaused whenVotingActive { + for (uint256 i = 0; i < _tokenIds.length; i++) { + _vote(_tokenIds[i], _votes); + } + } + + function vote( + uint256 _tokenId, + GaugeVote[] calldata _votes + ) public nonReentrant whenNotPaused whenVotingActive { + _vote(_tokenId, _votes); + } + + function _vote(uint256 _tokenId, GaugeVote[] calldata _votes) internal { + // ensure the user is allowed to vote on this + if (!IVotingEscrow(escrow).isApprovedOrOwner(_msgSender(), _tokenId)) { + revert NotApprovedOrOwner(); + } + + uint256 votingPower = IVotingEscrow(escrow).votingPower(_tokenId); + if (votingPower == 0) revert NoVotingPower(); + + uint256 numVotes = _votes.length; + if (numVotes == 0) revert NoVotes(); + + // clear any existing votes + if (isVoting(_tokenId)) _reset(_tokenId); + + // voting power continues to increase over the voting epoch. + // this means you can revote later in the epoch to increase votes. + // while not a huge problem, it's worth noting that when rewards are fully + // on chain, this could be a vector for gaming. + TokenVoteData storage voteData = tokenVoteData[_tokenId]; + uint256 votingPowerUsed = 0; + uint256 sumOfWeights = 0; + + for (uint256 i = 0; i < numVotes; i++) { + sumOfWeights += _votes[i].weight; + } + + // this is technically redundant as checks below will revert div by zero + // but it's clearer to the caller if we revert here + if (sumOfWeights == 0) revert NoVotes(); + + // iterate over votes and distribute weight + for (uint256 i = 0; i < numVotes; i++) { + // the gauge must exist and be active, + // it also can't have any votes or we haven't reset properly + address gauge = _votes[i].gauge; + + if (!gaugeExists(gauge)) revert GaugeDoesNotExist(gauge); + if (!isActive(gauge)) revert GaugeInactive(gauge); + + // prevent double voting + if (voteData.votes[gauge] != 0) revert DoubleVote(); + + // calculate the weight for this gauge + uint256 votesForGauge = (_votes[i].weight * votingPower) / sumOfWeights; + if (votesForGauge == 0) revert NoVotes(); + + // record the vote for the token + voteData.gaugesVotedFor.push(gauge); + voteData.votes[gauge] += votesForGauge; + + // update the total weights accruing to this gauge + gaugeVotes[gauge] += votesForGauge; + + // track the running changes to the total + // this might differ from the total voting power + // due to rounding, so aggregating like this ensures consistency + votingPowerUsed += votesForGauge; + + emit Voted({ + voter: _msgSender(), + gauge: gauge, + epoch: epochId(), + tokenId: _tokenId, + votingPowerCastForGauge: votesForGauge, + totalVotingPowerInGauge: gaugeVotes[gauge], + totalVotingPowerInContract: totalVotingPowerCast + votingPowerUsed, + timestamp: block.timestamp + }); + } + + // record the total weight used for this vote + totalVotingPowerCast += votingPowerUsed; + voteData.usedVotingPower = votingPowerUsed; + + // setting the last voted also has the second-order effect of indicating the user has voted + voteData.lastVoted = block.timestamp; + } + + function reset(uint256 _tokenId) external nonReentrant whenNotPaused { + if (!IVotingEscrow(escrow).isApprovedOrOwner(msg.sender, _tokenId)) + revert NotApprovedOrOwner(); + if (!isVoting(_tokenId)) revert NotCurrentlyVoting(); + _reset(_tokenId); + } + + function _reset(uint256 _tokenId) internal { + // get what we need + TokenVoteData storage voteData = tokenVoteData[_tokenId]; + address[] storage pastVotes = voteData.gaugesVotedFor; + + // reset the global state variables we don't need + voteData.usedVotingPower = 0; + voteData.lastVoted = 0; + + // iterate over all the gauges voted for and reset the votes + uint256 votingPowerToRemove = 0; + for (uint256 i = 0; i < pastVotes.length; i++) { + address gauge = pastVotes[i]; + uint256 _votes = voteData.votes[gauge]; + + // remove from the total weight and globals + gaugeVotes[gauge] -= _votes; + votingPowerToRemove += _votes; + + delete voteData.votes[gauge]; + + emit Reset({ + voter: _msgSender(), + gauge: gauge, + epoch: epochId(), + tokenId: _tokenId, + votingPowerRemovedFromGauge: _votes, + totalVotingPowerInGauge: gaugeVotes[gauge], + totalVotingPowerInContract: totalVotingPowerCast - votingPowerToRemove, + timestamp: block.timestamp + }); + } + + // clear the remaining state + voteData.gaugesVotedFor = new address[](0); + totalVotingPowerCast -= votingPowerToRemove; + } + + /*/////////////////////////////////////////////////////////////// + Gauge Management + //////////////////////////////////////////////////////////////*/ + + function gaugeExists(address _gauge) public view returns (bool) { + // this doesn't revert if you create multiple gauges at genesis + // but that's not a practical concern + return gauges[_gauge].created > 0; + } + + function isActive(address _gauge) public view returns (bool) { + return gauges[_gauge].active; + } + + function createGauge( + address _gauge, + string calldata _metadataURI + ) external auth(GAUGE_ADMIN_ROLE) nonReentrant returns (address gauge) { + if (_gauge == address(0)) revert ZeroGauge(); + if (gaugeExists(_gauge)) revert GaugeExists(); + + gauges[_gauge] = Gauge(true, block.timestamp, _metadataURI); + gaugeList.push(_gauge); + + emit GaugeCreated(_gauge, _msgSender(), _metadataURI); + return _gauge; + } + + function deactivateGauge(address _gauge) external auth(GAUGE_ADMIN_ROLE) { + if (!gaugeExists(_gauge)) revert GaugeDoesNotExist(_gauge); + if (!isActive(_gauge)) revert GaugeActivationUnchanged(); + gauges[_gauge].active = false; + emit GaugeDeactivated(_gauge); + } + + function activateGauge(address _gauge) external auth(GAUGE_ADMIN_ROLE) { + if (!gaugeExists(_gauge)) revert GaugeDoesNotExist(_gauge); + if (isActive(_gauge)) revert GaugeActivationUnchanged(); + gauges[_gauge].active = true; + emit GaugeActivated(_gauge); + } + + function updateGaugeMetadata( + address _gauge, + string calldata _metadataURI + ) external auth(GAUGE_ADMIN_ROLE) { + if (!gaugeExists(_gauge)) revert GaugeDoesNotExist(_gauge); + gauges[_gauge].metadataURI = _metadataURI; + emit GaugeMetadataUpdated(_gauge, _metadataURI); + } + + /*/////////////////////////////////////////////////////////////// + Getters: Epochs & Time + //////////////////////////////////////////////////////////////*/ + + /// @notice autogenerated epoch id based on elapsed time + function epochId() public view returns (uint256) { + return IClock(clock).currentEpoch(); + } + + /// @notice whether voting is active in the current epoch + function votingActive() public view returns (bool) { + return IClock(clock).votingActive(); + } + + /// @notice timestamp of the start of the next epoch + function epochStart() external view returns (uint256) { + return IClock(clock).epochStartTs(); + } + + /// @notice timestamp of the start of the next voting period + function epochVoteStart() external view returns (uint256) { + return IClock(clock).epochVoteStartTs(); + } + + /// @notice timestamp of the end of the current voting period + function epochVoteEnd() external view returns (uint256) { + return IClock(clock).epochVoteEndTs(); + } + + /*/////////////////////////////////////////////////////////////// + Getters: Mappings + //////////////////////////////////////////////////////////////*/ + + function getGauge(address _gauge) external view returns (Gauge memory) { + return gauges[_gauge]; + } + + function getAllGauges() external view returns (address[] memory) { + return gaugeList; + } + + function isVoting(uint256 _tokenId) public view returns (bool) { + return tokenVoteData[_tokenId].lastVoted > 0; + } + + function votes(uint256 _tokenId, address _gauge) external view returns (uint256) { + return tokenVoteData[_tokenId].votes[_gauge]; + } + + function gaugesVotedFor(uint256 _tokenId) external view returns (address[] memory) { + return tokenVoteData[_tokenId].gaugesVotedFor; + } + + function usedVotingPower(uint256 _tokenId) external view returns (uint256) { + return tokenVoteData[_tokenId].usedVotingPower; + } + + /// Rest of UUPS logic is handled by OSx plugin + uint256[43] private __gap; +} diff --git a/test/escrow/migration/MigrationBase.sol b/test/escrow/migration/MigrationBase.sol index ee1732c..ffddb93 100644 --- a/test/escrow/migration/MigrationBase.sol +++ b/test/escrow/migration/MigrationBase.sol @@ -21,7 +21,7 @@ import {IVotingEscrowEventsStorageErrorsEvents} from "@escrow-interfaces/IVoting import {IMigrateableEventsAndErrors} from "@escrow-interfaces/IMigrateable.sol"; import {IWhitelistErrors, IWhitelistEvents} from "@escrow-interfaces/ILock.sol"; import {Lock} from "@escrow/Lock.sol"; -import {VotingEscrow} from "@escrow/VotingEscrowIncreasing.sol"; +import {VotingEscrowV1_1_0 as VotingEscrow} from "@escrow/VotingEscrowIncreasing_v1_1_0.sol"; import {QuadraticIncreasingEscrow} from "@escrow/QuadraticIncreasingEscrow.sol"; import {ExitQueue} from "@escrow/ExitQueue.sol"; import {SimpleGaugeVoter, SimpleGaugeVoterSetup} from "src/voting/SimpleGaugeVoterSetup.sol"; diff --git a/test/escrow/migration/MigrationStateful.t.sol b/test/escrow/migration/MigrationStateful.t.sol new file mode 100644 index 0000000..dc0a602 --- /dev/null +++ b/test/escrow/migration/MigrationStateful.t.sol @@ -0,0 +1,175 @@ +/// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {Test} from "forge-std/Test.sol"; + +// aragon contracts +import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; +import {DAO} from "@aragon/osx/core/dao/DAO.sol"; +import {DaoUnauthorized} from "@aragon/osx/core/utils/auth.sol"; +import {Multisig, MultisigSetup} from "@aragon/multisig/MultisigSetup.sol"; + +import {MockPluginSetupProcessor} from "@mocks/osx/MockPSP.sol"; +import {MockDAOFactory} from "@mocks/osx/MockDAOFactory.sol"; +import {MockERC20} from "@mocks/MockERC20.sol"; +import {createTestDAO} from "@mocks/MockDAO.sol"; + +import "@helpers/OSxHelpers.sol"; +import {ProxyLib} from "@libs/ProxyLib.sol"; + +import {IGaugeVote} from "src/voting/ISimpleGaugeVoter.sol"; +import {IVotingEscrowEventsStorageErrorsEvents} from "@escrow-interfaces/IVotingEscrowIncreasing.sol"; +import {IWhitelistErrors, IWhitelistEvents} from "@escrow-interfaces/ILock.sol"; +import {Lock} from "@escrow/Lock.sol"; +import {VotingEscrow} from "@escrow/VotingEscrowIncreasing.sol"; +import {QuadraticIncreasingEscrow} from "@escrow/QuadraticIncreasingEscrow.sol"; +import {ExitQueue} from "@escrow/ExitQueue.sol"; +import {SimpleGaugeVoter, SimpleGaugeVoterSetup} from "src/voting/SimpleGaugeVoterSetup.sol"; +import {Clock} from "@clock/Clock.sol"; + +import {MigrationBase} from "./MigrationBase.sol"; +import {MockMigrator} from "@mocks/MockMigrator.sol"; + +/** + * @dev Tests various real world scenarios where we have migrations from contracts + */ +contract TestMigrationStateful is MigrationBase, IGaugeVote { + MockMigrator migrator; + GaugeVote[] votes; + address gauge = address(0x777); + function setUp() public override { + super.setUp(); + migrator = new MockMigrator(); + } + + // tests votes reset if the user is voting + function testVotesResetIfVoting(uint32 _warp) public { + // setup a gauge vote + src.voter.createGauge(gauge, "metadata"); + + address depositor = address(420); + src.token.mint(depositor, 100 ether); + uint tokenId; + + vm.startPrank(depositor); + { + src.token.approve(address(src.escrow), 100 ether); + tokenId = src.escrow.createLock(100 ether); + + // arbitrary jump for voting power + vm.warp(1 weeks); + // next voting window + vm.warp(src.clock.resolveEpochVoteStartTs(block.timestamp) + 1); + + // vote + votes.push(GaugeVote(100 ether, gauge)); + src.voter.vote(tokenId, votes); + + // check votes are set + assertGt(src.voter.totalVotingPowerCast(), 0, "votes cast"); + } + vm.stopPrank(); + + // random warp: means we can be in either voting or non-voting period + vm.warp(block.timestamp + uint(_warp)); + src.escrow.enableMigration(address(migrator)); + + vm.startPrank(depositor); + { + // migrate - requires approving the escrow + src.nftLock.approve(address(src.escrow), tokenId); + src.escrow.migrateFrom(tokenId); + + // check votes are reset + assertEq(src.voter.totalVotingPowerCast(), 0, "votes cast after migration"); + } + vm.stopPrank(); + } + + function testExitingUserIsUnaffected() public { + dst.dao.grant({ + _who: address(src.escrow), + _where: address(dst.escrow), + _permissionId: dst.escrow.MIGRATOR_ROLE() + }); + + address alice = address(0xa11ce); + address bob = address(0xb0b); + + src.token.mint(alice, 100 ether); + src.token.mint(bob, 100 ether); + + uint aliceTokenId; + uint bobTokenId; + // user 1 and 2 create lock + vm.startPrank(alice); + { + src.token.approve(address(src.escrow), 100 ether); + aliceTokenId = src.escrow.createLock(100 ether); + } + vm.stopPrank(); + + vm.startPrank(bob); + { + src.token.approve(address(src.escrow), 100 ether); + bobTokenId = src.escrow.createLock(100 ether); + } + vm.stopPrank(); + + src.escrow.enableMigration(address(dst.escrow)); + + // u1 goes through the exit queue + vm.warp(10 weeks); // arbitrary time + + vm.startPrank(alice); + { + src.nftLock.approve(address(src.escrow), aliceTokenId); + src.escrow.beginWithdrawal(aliceTokenId); + + // check #1: can't now migrate + vm.expectRevert(NotOwner.selector); + src.escrow.migrateFrom(aliceTokenId); + } + vm.stopPrank(); + + // u2 migrates + vm.startPrank(bob); + { + src.nftLock.approve(address(src.escrow), bobTokenId); + src.escrow.migrateFrom(bobTokenId); + + // cant withdraw as doesn't exist + vm.expectRevert("ERC721: invalid token ID"); + src.escrow.beginWithdrawal(bobTokenId); + } + vm.stopPrank(); + + // check #2: u1 can still exit + vm.startPrank(alice); + { + vm.warp(block.timestamp + 52 weeks); // arbitrary time + src.escrow.withdraw(aliceTokenId); + } + vm.stopPrank(); + + // expect empty state on src + assertEq(src.nftLock.totalSupply(), 0, "src total supply"); + assertEq(src.escrow.totalLocked(), 0, "src total locked"); + assertEq(src.escrow.locked(aliceTokenId).amount, 0, "src alice locked amount"); + assertEq(src.escrow.locked(bobTokenId).amount, 0, "src bob locked amount"); + assertEq(src.nftLock.balanceOf(alice), 0, "src alice balance"); + assertEq(src.nftLock.balanceOf(bob), 0, "src bob balance"); + assertEq(src.token.balanceOf(address(src.escrow)), 0, "src escrow balance"); + assertEq(src.token.balanceOf(alice), 100 ether, "src alice balance"); + + // expect state on dst + assertEq(dst.nftLock.totalSupply(), 1, "dst total supply"); + assertEq(dst.escrow.totalLocked(), 100 ether, "dst total locked"); + // remember bob is now id 1 on dst + assertEq(dst.escrow.locked(1).amount, 100 ether, "dst bob locked amount"); + assertEq(dst.nftLock.balanceOf(alice), 0, "dst alice balance"); + assertEq(dst.nftLock.balanceOf(bob), 1, "dst bob balance"); + assertEq(dst.nftLock.ownerOf(1), bob, "dst bob owner"); + assertEq(dst.token.balanceOf(address(dst.escrow)), 100 ether, "dst escrow balance"); + } +} diff --git a/test/escrow/migration/MigrationStateless.t.sol b/test/escrow/migration/MigrationStateless.t.sol index 363a853..285b454 100644 --- a/test/escrow/migration/MigrationStateless.t.sol +++ b/test/escrow/migration/MigrationStateless.t.sol @@ -87,8 +87,6 @@ contract TestMigrationStateless is MigrationBase { } function testCannotMigrateIfNotOwner() public { - src.escrow.enableMigration(address(migrator)); - address depositor = address(420); src.token.mint(depositor, 100 ether); @@ -101,13 +99,13 @@ contract TestMigrationStateless is MigrationBase { } vm.stopPrank(); + src.escrow.enableMigration(address(migrator)); + vm.expectRevert(NotOwner.selector); src.escrow.migrateFrom(tokenId); } function testCannotMigrateIfNoVotingPower() public { - src.escrow.enableMigration(address(migrator)); - address depositor = address(420); src.token.mint(depositor, 100 ether); @@ -117,15 +115,35 @@ contract TestMigrationStateless is MigrationBase { { src.token.approve(address(src.escrow), 100 ether); tokenId = src.escrow.createLock(100 ether); + } + vm.stopPrank(); + + src.escrow.enableMigration(address(migrator)); + + vm.startPrank(depositor); + { vm.expectRevert(CannotExit.selector); src.escrow.migrateFrom(tokenId); } vm.stopPrank(); } - - function testCannotMigrateIfMigratorRoleNotGivenToDestination() public { + function testCannotDepositIfMigrationEnabled() public { src.escrow.enableMigration(address(dst.escrow)); + address depositor = address(420); + + src.token.mint(depositor, 100 ether); + uint tokenId; + + vm.startPrank(depositor); + { + src.token.approve(address(src.escrow), 100 ether); + vm.expectRevert(MigrationActive.selector); + tokenId = src.escrow.createLock(100 ether); + } + vm.stopPrank(); + } + function testCannotMigrateIfMigratorRoleNotGivenToDestination() public { address depositor = address(420); src.token.mint(depositor, 100 ether); @@ -137,7 +155,13 @@ contract TestMigrationStateless is MigrationBase { tokenId = src.escrow.createLock(100 ether); vm.warp(src.clock.checkpointInterval() + 1); + } + vm.stopPrank(); + + src.escrow.enableMigration(address(dst.escrow)); + vm.startPrank(depositor); + { vm.expectRevert( _authErr( address(dst.dao), @@ -152,8 +176,6 @@ contract TestMigrationStateless is MigrationBase { } function testMigrateFromAndTo() public { - src.escrow.enableMigration(address(dst.escrow)); - dst.dao.grant({ _who: address(src.escrow), _where: address(dst.escrow), @@ -170,11 +192,17 @@ contract TestMigrationStateless is MigrationBase { { src.token.approve(address(src.escrow), 100 ether); tokenId = src.escrow.createLock(100 ether); + } + vm.stopPrank(); - vm.warp(src.clock.checkpointInterval() + 1); + vm.warp(src.clock.checkpointInterval() + 1); + src.escrow.enableMigration(address(dst.escrow)); + vm.startPrank(depositor); + { + uint votingPower = src.escrow.votingPower(tokenId); vm.expectEmit(true, true, true, true); - emit Migrated(depositor, tokenId, 1, 100 ether); + emit Migrated(depositor, tokenId, 1, 100 ether, votingPower); newTokenId = src.escrow.migrateFrom(tokenId); } vm.stopPrank(); diff --git a/test/fork/Migration.t.sol b/test/fork/Migration.t.sol new file mode 100644 index 0000000..8f979da --- /dev/null +++ b/test/fork/Migration.t.sol @@ -0,0 +1,316 @@ +pragma solidity ^0.8.17; + +import {AragonTest} from "../base/AragonTest.sol"; +import {console2 as console} from "forge-std/console2.sol"; + +import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {DAO} from "@aragon/osx/core/dao/DAO.sol"; +import {Multisig, MultisigSetup} from "@aragon/multisig/MultisigSetup.sol"; +import {UUPSUpgradeable as UUPS} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +import "../helpers/OSxHelpers.sol"; + +import {Clock} from "@clock/Clock.sol"; +import {IEscrowCurveTokenStorage} from "@escrow-interfaces/IEscrowCurveIncreasing.sol"; +import {IWithdrawalQueueErrors} from "src/escrow/increasing/interfaces/IVotingEscrowIncreasing.sol"; +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 {DeployGauges, DeploymentParameters} from "script/DeployGauges.s.sol"; + +/** + * Test the upgrade of the migration contracts and the move to the new contracts + * We do the following steps: + 1. Deploy the new contracts + 2. Deploy upgraded implementations to the voting and staking contracts + 3. Pause the new staking contract + 4. upgrade the old contracts + 4. Add the old staking contract as the migration target + 5. Enable the migration + + 6. Grab all the mode stakers and run a test to see that they can migrate. + 7. for those in the exit queue, ensure they can still exit + + + */ +contract TestMigrate is AragonTest { + GaugesDaoFactory srcFactory; + GaugePluginSet src; + + Multisig srcMultisig; + DAO srcDAO; + + GaugesDaoFactory dstFactory; + GaugePluginSet dst; + + Multisig dstMultisig; + DAO dstDAO; + + DeployGauges deploy; + address[] signers; + + address[] stakers; + + enum ModeOrBPT { + MODE, + BPT + } + + uint8 constant modeOrBPT = uint8(ModeOrBPT.BPT); + + function setUp() public { + deploy = new DeployGauges(); + + signers = deploy.readMultisigMembers(); + + _fetchOldDeploy(); + _deployNew(); + vm.roll(block.number + 1); + _upgradeSrcContracts(); + _enableMigrationDst(); + _enableMigrationSrc(); + _loadStakers(); + } + + /// Tests + + function testMigrate() public { + // warp a few days to allow warmups + vm.warp(block.timestamp + 1 weeks); + { + uint totalLocked = src.votingEscrow.totalLocked(); + uint balance = IERC20(src.votingEscrow.token()).balanceOf(address(src.votingEscrow)); + console.log("[PRE] Total Locked: %s, Balance: %s", totalLocked, balance); + } + + for (uint256 i = 0; i < stakers.length; i++) { + uint totalLockedPre = src.votingEscrow.totalLocked(); + address staker = stakers[i]; + // get the tokens of the staker + uint[] memory veNFTs = src.votingEscrow.ownedTokens(staker); + + // get the balance locked in the contract + uint totalLockedStakerPre; + uint totalLockedStakerPost; + uint totalNotMigrated; + for (uint j = 0; j < veNFTs.length; j++) { + uint tokenId = veNFTs[j]; + totalLockedStakerPre += src.votingEscrow.locked(tokenId).amount; + // migrate the token + vm.startPrank(staker); + { + src.nftLock.approve(address(src.votingEscrow), tokenId); + try src.votingEscrow.migrateFrom(tokenId) returns (uint newTokenId) { + totalLockedStakerPost += dst.votingEscrow.locked(newTokenId).amount; + } catch { + console.log( + "Migration failed for staker: %s with tokenId %s", + _hToS(staker), + tokenId + ); + totalNotMigrated += src.votingEscrow.locked(tokenId).amount; + } + } + vm.stopPrank(); + } + + if (totalLockedStakerPre == 0) { + console.log("Staker: %s, no tokens to migrate", _hToS(staker)); + } else + console.log( + "Staker: %s, Total Locked Before: %s, Total locked After: %s", + staker, + totalLockedStakerPre, + totalLockedStakerPost + ); + + assertEq(totalLockedStakerPre, totalLockedStakerPost + totalNotMigrated, _hToS(staker)); + if (totalNotMigrated > 0) console.log("Total Not Migrated: %s", totalNotMigrated); + } + { + uint totalLocked = src.votingEscrow.totalLocked(); + uint balance = IERC20(src.votingEscrow.token()).balanceOf(address(src.votingEscrow)); + uint totalLockeddst = dst.votingEscrow.totalLocked(); + uint balancedst = IERC20(src.votingEscrow.token()).balanceOf(address(dst.votingEscrow)); + console.log("[POST:src] Total Locked: %s, Balance: %s", totalLocked, balance); + console.log("[POST:dst] Total Locked: %s, Balance: %s", totalLockeddst, balancedst); + } + } + + // fetch the most recent deploy + function _fetchOldDeploy() internal { + DeploymentParameters memory deploymentParameters = deploy.getDeploymentParameters(false); + + address factoryAddress = vm.envOr("FACTORY_ADDRESS", address(0)); + if (factoryAddress == address(0)) { + revert("Factory address not set"); + } + srcFactory = GaugesDaoFactory(factoryAddress); + + Deployment memory deployment = srcFactory.getDeployment(); + src = deployment.gaugeVoterPluginSets[modeOrBPT]; + srcMultisig = deployment.multisigPlugin; + srcDAO = deployment.dao; + } + + function _deployNew() internal { + DeploymentParameters memory deploymentParameters = deploy.getDeploymentParameters(false); + + deploymentParameters.voterEnsSubdomain = _hToS( + keccak256(abi.encodePacked("gauges", block.timestamp)) + ); + + // deploy the factory + dstFactory = new GaugesDaoFactory(deploymentParameters); + dstFactory.deployOnce(); + + Deployment memory deployment = dstFactory.getDeployment(); + dst = deployment.gaugeVoterPluginSets[modeOrBPT]; + + dstMultisig = deployment.multisigPlugin; + dstDAO = deployment.dao; + } + + // create the multisig transaction to upgrade the voter and escrow + function _upgradeSrcContracts() public { + // fetch the implementation contracts for the voter and escrow in the new deploy + address voterImpl = SimpleGaugeVoter(address(dst.plugin)).implementation(); + address escrowImpl = VotingEscrow(address(dst.votingEscrow)).implementation(); + + // first we need to upgrade both contracts + IDAO.Action[] memory actions = new IDAO.Action[](2); + actions[0] = IDAO.Action({ + to: address(src.plugin), + value: 0, + data: abi.encodeCall(src.plugin.upgradeTo, (voterImpl)) + }); + + actions[1] = IDAO.Action({ + to: address(src.votingEscrow), + value: 0, + data: abi.encodeCall(src.votingEscrow.upgradeTo, (escrowImpl)) + }); + + _buildSignProposal(actions, srcMultisig); + } + + function _enableMigrationDst() public { + // on the destination: + IDAO.Action[] memory actions = new IDAO.Action[](2); + // pause the dst staking contract + actions[0] = IDAO.Action({ + to: address(dst.votingEscrow), + value: 0, + data: abi.encodeCall(dst.votingEscrow.pause, ()) + }); + + // grant the migrator role on the prev staking contract + actions[1] = IDAO.Action({ + to: address(dstDAO), + value: 0, + data: abi.encodeCall( + dstDAO.grant, + ( + address(dst.votingEscrow), + address(src.votingEscrow), + dst.votingEscrow.MIGRATOR_ROLE() + ) + ) + }); + + _buildSignProposal(actions, dstMultisig); + } + + function _enableMigrationSrc() public { + IDAO.Action[] memory actions = new IDAO.Action[](1); + // pause the dst staking contract + actions[0] = IDAO.Action({ + to: address(src.votingEscrow), + value: 0, + data: abi.encodeCall(src.votingEscrow.enableMigration, (address(dst.votingEscrow))) + }); + + _buildSignProposal(actions, srcMultisig); + } + + function _loadStakers() internal { + // Load the JSON file as a string + string memory json = vm.readFile("./test/fork/stakers.json"); + + // Parse the JSON into an array of Staker structs + bytes memory data = vm.parseJson(json, ""); // Assuming root of JSON + stakers = abi.decode(data, (address[])); + } + + /// Utils + function _hToS(address _addr) internal pure returns (string memory) { + return _hToS(bytes32(uint256(uint160(_addr)))); + } + + function _hToS(bytes32 _hash) internal pure returns (string memory) { + bytes memory hexString = new bytes(64); + bytes memory alphabet = "0123456789abcdef"; + + for (uint256 i = 0; i < 32; i++) { + hexString[i * 2] = alphabet[uint8(_hash[i] >> 4)]; + hexString[1 + i * 2] = alphabet[uint8(_hash[i] & 0x0f)]; + } + + return string(hexString); + } + + function _buildMsigProposal( + IDAO.Action[] memory actions, + Multisig multisig + ) internal returns (uint256 proposalId) { + // prank the first signer who will create stuff + vm.startPrank(signers[0]); + { + proposalId = multisig.createProposal({ + _metadata: "", + _actions: actions, + _allowFailureMap: 0, + _approveProposal: true, + _tryExecution: false, + _startDate: 0, + _endDate: uint64(block.timestamp) + 3 days + }); + } + vm.stopPrank(); + + return proposalId; + } + + function _signExecuteMultisigProposal(uint256 _proposalId, Multisig multisig) internal { + // load all the proposers into memory other than the first + + if (signers.length > 1) { + // have them sign + for (uint256 i = 1; i < signers.length; i++) { + vm.startPrank(signers[i]); + { + multisig.approve(_proposalId, false); + } + vm.stopPrank(); + } + } + + // prank the first signer who will create stuff + vm.startPrank(signers[0]); + { + multisig.execute(_proposalId); + } + vm.stopPrank(); + } + + function _buildSignProposal( + IDAO.Action[] memory actions, + Multisig multisig + ) internal returns (uint256 proposalId) { + proposalId = _buildMsigProposal(actions, multisig); + _signExecuteMultisigProposal(proposalId, multisig); + return proposalId; + } +} diff --git a/test/fork/bpt-leaderboard.json b/test/fork/bpt-leaderboard.json new file mode 100644 index 0000000..7fa69ca --- /dev/null +++ b/test/fork/bpt-leaderboard.json @@ -0,0 +1,1132 @@ +{ + "bpt": [ + { + "address": "0x9ff471F9f98F42E5151C7855fD1b5aa906b1AF7e", + "totalStaked": "1881458172034086676302656", + "veNFTs": ["98"], + "rank": 1 + }, + { + "address": "0x126FF8faBeC84EF8a96F25dD0C2668fB9b7E7088", + "totalStaked": "105101073805781590000000", + "veNFTs": ["103"], + "rank": 2 + }, + { + "address": "0x7B59793aD075e4ce1e35181054759C080B8D965D", + "totalStaked": "97760073155744519865039", + "veNFTs": ["140"], + "rank": 3 + }, + { + "address": "0x8bF1e340055c7dE62F11229A149d3A1918de3d74", + "totalStaked": "79652719568288960000000", + "veNFTs": ["1", "3", "8"], + "rank": 4 + }, + { + "address": "0x2662467718Bd14c8407a5E0A6657C9009FEb2D44", + "totalStaked": "73440206167228732164943", + "veNFTs": ["171"], + "rank": 5 + }, + { + "address": "0xa0Eb44b173AF64f2FdB47C7d76FF3A33e04103b6", + "totalStaked": "63011892308555388816284", + "veNFTs": ["64"], + "rank": 6 + }, + { + "address": "0xECEe8B6F8f2B4900f631EbC91021BAf95Bb7E99F", + "totalStaked": "61486042816901769839395", + "veNFTs": ["9", "176"], + "rank": 7 + }, + { + "address": "0xAdffC760EDF4f6146dce89022B5AE7EbB7edD2B9", + "totalStaked": "41877933363400023195923", + "veNFTs": ["119", "196"], + "rank": 8 + }, + { + "address": "0xa6bBAd15B9cc15cd1f81874D98dBb78F522161dC", + "totalStaked": "37948732870370868308555", + "veNFTs": ["62"], + "rank": 9 + }, + { + "address": "0x99AfD53f807766A8B98400B0C785E500c041F32B", + "totalStaked": "28258710936138522794941", + "veNFTs": ["94"], + "rank": 10 + }, + { + "address": "0x53D8EDF6a54239eB785eC72213919Fb6b6B73598", + "totalStaked": "24803424421682982429169", + "veNFTs": ["63", "80", "228"], + "rank": 11 + }, + { + "address": "0x9DC3E0fC228c2455773E70ee9E987aE2a6FBc07D", + "totalStaked": "21998689113958348374511", + "veNFTs": ["167"], + "rank": 12 + }, + { + "address": "0x652AB9e3577eD85d7fC9A41fAFf68E9DA4Db00Ff", + "totalStaked": "21183406923796799496463", + "veNFTs": ["4"], + "rank": 13 + }, + { + "address": "0x02E7b714fae84e4BA80f3CDa5508553e7CF5042A", + "totalStaked": "17988561788902629830118", + "veNFTs": ["148", "206", "230", "250"], + "rank": 14 + }, + { + "address": "0xB4B1b6928337974D98843f45aBbb8B38DFB1B72C", + "totalStaked": "17501084610575315000000", + "veNFTs": ["52"], + "rank": 15 + }, + { + "address": "0x6f9BB7e454f5B3eb2310343f0E99269dC2BB8A1d", + "totalStaked": "11339078289323297611023", + "veNFTs": ["33", "135"], + "rank": 16 + }, + { + "address": "0xC02D68B1287534d8D12a9bE01A5B4ef5A1771bA3", + "totalStaked": "11031855803855859000000", + "veNFTs": ["36"], + "rank": 17 + }, + { + "address": "0xb740FF2bDD86330FF51dD3bB7eF933566f364793", + "totalStaked": "6681227919113860304381", + "veNFTs": ["91"], + "rank": 18 + }, + { + "address": "0x16B00dB74167b7469dDa63b674Ca9A3B1b70C439", + "totalStaked": "6111161903754785000000", + "veNFTs": ["88"], + "rank": 19 + }, + { + "address": "0x71535AAe1B6C0c51Db317B54d5eEe72d1ab843c1", + "totalStaked": "5913876443294608640299", + "veNFTs": ["210"], + "rank": 20 + }, + { + "address": "0xaf1C56ABb73ce0C13cCeE81b59479A5D0177eaB9", + "totalStaked": "5167889595247183000000", + "veNFTs": ["66"], + "rank": 21 + }, + { + "address": "0xe16609a91291Dd45aBFCd8555b6cb3800A86a4A8", + "totalStaked": "5132761738809127111620", + "veNFTs": ["53"], + "rank": 22 + }, + { + "address": "0xc7E82FA77f32BFD6AeeD59924a1a564a924b2EA8", + "totalStaked": "5005090942643535989452", + "veNFTs": ["268"], + "rank": 23 + }, + { + "address": "0x059F4c04295bde54893C24025d8229E35318d3Cc", + "totalStaked": "4876628552790918540000", + "veNFTs": ["153", "239"], + "rank": 24 + }, + { + "address": "0x77b90c77c5e647b6ad208DFF364cDD42D44De124", + "totalStaked": "4868000000000000000000", + "veNFTs": ["101"], + "rank": 25 + }, + { + "address": "0xD83D994B102A5E6B452469D26Fe5acd1608954f9", + "totalStaked": "4494256520311017865258", + "veNFTs": ["58", "96", "158"], + "rank": 26 + }, + { + "address": "0x3fE2e08a1F7766606574a91a5CfAf3342d023947", + "totalStaked": "4221793140088443000000", + "veNFTs": ["172"], + "rank": 27 + }, + { + "address": "0xd5dBb9e213545a4d8248d671BfCf6E5fa6E72fAF", + "totalStaked": "4153811898073334000000", + "veNFTs": ["44"], + "rank": 28 + }, + { + "address": "0xF4b40B20572D117EAD7b5ca3C7eFf7d3CBdC4067", + "totalStaked": "4052704882819250803104", + "veNFTs": ["82"], + "rank": 29 + }, + { + "address": "0xEB4576fE753DAB07635c0Bb6c8f0A355e1Db5d31", + "totalStaked": "4032372002892932937662", + "veNFTs": ["127"], + "rank": 30 + }, + { + "address": "0xa9D403b3905b7b5c9c345f711A8B9E858aaA4856", + "totalStaked": "3717718724944238000000", + "veNFTs": ["204"], + "rank": 31 + }, + { + "address": "0xA5274842aa457d8A1a2AAdB9dFB37B266C8d30e5", + "totalStaked": "3201569556387656000000", + "veNFTs": ["47"], + "rank": 32 + }, + { + "address": "0x8DcAC18996dC58b5eebfDC5D0A7b22E0B951A4b7", + "totalStaked": "3113172380675641623192", + "veNFTs": ["61"], + "rank": 33 + }, + { + "address": "0x4B02af3D86A380A90F0b413A0D6414FaCf2cde4a", + "totalStaked": "3046792506636914972946", + "veNFTs": ["28"], + "rank": 34 + }, + { + "address": "0x4C38705A7B26f1D90dEbca268D99c79520d793cD", + "totalStaked": "2583732838945636300000", + "veNFTs": ["48", "189"], + "rank": 35 + }, + { + "address": "0x2bcd8898A678050EFA5F41F4c6012045dee1B5c3", + "totalStaked": "2253517055866339664271", + "veNFTs": ["78"], + "rank": 36 + }, + { + "address": "0xE3053cDe2124eAA6103DddE8E5c6BD36c66F716E", + "totalStaked": "2240046414493693600000", + "veNFTs": ["193"], + "rank": 37 + }, + { + "address": "0x20151c34D01D6785493F3416b3f82812a3dbB46F", + "totalStaked": "1808219035027588545889", + "veNFTs": ["105", "112"], + "rank": 38 + }, + { + "address": "0xcc5B087283D69dB4237Da489d487140f2745fD28", + "totalStaked": "1778962281950008200000", + "veNFTs": ["106"], + "rank": 39 + }, + { + "address": "0x672A7B6431B8C77BEE625963F14fD8C388b682D2", + "totalStaked": "1757973779776763162864", + "veNFTs": ["50"], + "rank": 40 + }, + { + "address": "0x5f1Da18007f212A0281AC98438fEbcc44F0f3709", + "totalStaked": "1603027849990656220218", + "veNFTs": ["170"], + "rank": 41 + }, + { + "address": "0xB4173fEe41920a8e7D3451863f9779468EbAb98E", + "totalStaked": "1480097841222978750841", + "veNFTs": ["10", "118"], + "rank": 42 + }, + { + "address": "0xcF8373446363216f0daC6ace39cd3dB12cF8689b", + "totalStaked": "1450980865801439158339", + "veNFTs": ["16"], + "rank": 43 + }, + { + "address": "0x36A5639479f44F6540623046BE1b8bDd04b42Cc3", + "totalStaked": "1416000000000000000000", + "veNFTs": ["226"], + "rank": 44 + }, + { + "address": "0x2D6b9A471DB03Ecf92927e6921DdB17932Fffa64", + "totalStaked": "1352993904750918143721", + "veNFTs": ["199"], + "rank": 45 + }, + { + "address": "0x70b88351Cd1fF4C637556462734c61785AF4fCa5", + "totalStaked": "1255267346796166300000", + "veNFTs": ["56"], + "rank": 46 + }, + { + "address": "0xEB2D541a7599eeCc86049e366AA82A914A7cbc74", + "totalStaked": "1169540883170777800000", + "veNFTs": ["57"], + "rank": 47 + }, + { + "address": "0xb1AC9db0d6a1eC291F427ad03fc3B632E1E93a56", + "totalStaked": "1146280332885100500000", + "veNFTs": ["17"], + "rank": 48 + }, + { + "address": "0x040007A949fa726f50810f6C98b1B620b50793a2", + "totalStaked": "1140782770394130500000", + "veNFTs": ["32"], + "rank": 49 + }, + { + "address": "0x9a10586aA206a401Ba50A168a847686d1E500dC6", + "totalStaked": "1127600681681497500000", + "veNFTs": ["69"], + "rank": 50 + }, + { + "address": "0x28288639D997B8aF1F4cb25190818C0001372fC8", + "totalStaked": "1114242223017863790424", + "veNFTs": ["237"], + "rank": 51 + }, + { + "address": "0xEF629261993A1eE4f6F64f651820CB3f44A58ba9", + "totalStaked": "1092356673502094700000", + "veNFTs": ["65"], + "rank": 52 + }, + { + "address": "0x32a59b87352e980dD6aB1bAF462696D28e63525D", + "totalStaked": "1074314119417372381976", + "veNFTs": ["72"], + "rank": 53 + }, + { + "address": "0x398851f0412decb07d5A1Ba900c352C1aAd7b432", + "totalStaked": "1070025680149805600000", + "veNFTs": ["249"], + "rank": 54 + }, + { + "address": "0x48555a6c6c88cbe69D36A44CA6095b731F453597", + "totalStaked": "1015541554220262542447", + "veNFTs": ["79"], + "rank": 55 + }, + { + "address": "0xc77a40a9a04Ef9e8DB7fC4F046ECe7c3C4F6867C", + "totalStaked": "1009750943784186784349", + "veNFTs": ["241"], + "rank": 56 + }, + { + "address": "0xAFbFE5B459e0985B6bF9F2c1Da086f182b930Cc1", + "totalStaked": "946379865718882079744", + "veNFTs": ["190"], + "rank": 57 + }, + { + "address": "0xBDac6532be38Db1D48640d46C3011dF6D4C099Ec", + "totalStaked": "850770619560439600000", + "veNFTs": ["54"], + "rank": 58 + }, + { + "address": "0xB8cd02828c214e8549B9e1951fbF049A457b2117", + "totalStaked": "794078709465817169385", + "veNFTs": ["90"], + "rank": 59 + }, + { + "address": "0x37749f66F360D9DE7B8B1F7928d839aa52aB2f91", + "totalStaked": "746484563352050100000", + "veNFTs": ["89"], + "rank": 60 + }, + { + "address": "0x896C20Da40c2A4df9B7C98B16a8D5A95129161a5", + "totalStaked": "736258000000000000000", + "veNFTs": ["132", "133", "256"], + "rank": 61 + }, + { + "address": "0x3906DDBaE2D8aF0581a38a09c90E14b48f3D2F94", + "totalStaked": "730561004110398423814", + "veNFTs": ["6"], + "rank": 62 + }, + { + "address": "0x0b1d114d9249ad870F11543215722D7804CC3dbA", + "totalStaked": "704458007445718683748", + "veNFTs": ["19", "231"], + "rank": 63 + }, + { + "address": "0x4770F67Db9D09Ca7347C1fCcBf3795A464065eCB", + "totalStaked": "682023421343406400000", + "veNFTs": ["110"], + "rank": 64 + }, + { + "address": "0xd48f1A44e56526e96A983FF750559aa365810D5A", + "totalStaked": "663271423689199900000", + "veNFTs": ["83"], + "rank": 65 + }, + { + "address": "0x29F38498071b772a99bAb18C7ea4125387Ed2332", + "totalStaked": "628374344323057315980", + "veNFTs": ["29"], + "rank": 66 + }, + { + "address": "0xEe6066b7655657e34f6190629a64F0a8014d3a96", + "totalStaked": "621917143278037400000", + "veNFTs": ["39"], + "rank": 67 + }, + { + "address": "0xF05454C3D16090b652cf5389C9eE520ceC477E5e", + "totalStaked": "612330410597468500000", + "veNFTs": ["77"], + "rank": 68 + }, + { + "address": "0x3844597c2b75E8510d8895173adDC28dd995fA45", + "totalStaked": "586996947594153200000", + "veNFTs": ["212"], + "rank": 69 + }, + { + "address": "0xee206E05D50783b2d5c553A56917e990a6BB6750", + "totalStaked": "506348949363020385428", + "veNFTs": ["108"], + "rank": 70 + }, + { + "address": "0x3021A59F56cAe360183C40965D7059a4a73A7Ba7", + "totalStaked": "505753421493514600000", + "veNFTs": ["31"], + "rank": 71 + }, + { + "address": "0x40677e9D3CBAfD1522a1baEdECebA68Aca35dB4D", + "totalStaked": "472193185938875375125", + "veNFTs": ["115"], + "rank": 72 + }, + { + "address": "0x530BcD1CE67288cC0EEb67C1D337De2Bf849CfB3", + "totalStaked": "465318471552065383356", + "veNFTs": ["60"], + "rank": 73 + }, + { + "address": "0xeE9A0bbD2cE67fc008fE3f7f6a86F2c6dA5F7a65", + "totalStaked": "420190719330748940000", + "veNFTs": ["163", "233"], + "rank": 74 + }, + { + "address": "0xc5115D9D85256cC93836189D21d23729014B9Afe", + "totalStaked": "417463654108070360104", + "veNFTs": ["197"], + "rank": 75 + }, + { + "address": "0x13e0d0A9e4024F1804FA2a0dde4F7c38abCc63F7", + "totalStaked": "413499366304488300000", + "veNFTs": ["11"], + "rank": 76 + }, + { + "address": "0x30a2837F3D319A3e1984DE5a32C0DF310e2496B7", + "totalStaked": "393599765245629544212", + "veNFTs": ["70"], + "rank": 77 + }, + { + "address": "0xADB40fbC28BC71856BFc46CF3e7Ff36cB44ba37D", + "totalStaked": "378349911260664196564", + "veNFTs": ["229"], + "rank": 78 + }, + { + "address": "0x9AeE301D7B11a6dc7293B7b6ba5f11cd048590dF", + "totalStaked": "371894627994395140000", + "veNFTs": ["200"], + "rank": 79 + }, + { + "address": "0xD66fa7a53803E34fAd9A0eCbd52111E93d0B3542", + "totalStaked": "366856439648724533574", + "veNFTs": ["67", "232"], + "rank": 80 + }, + { + "address": "0x4D5FeEdA8AacE603b44FeFCBFc82DE5336e17aCd", + "totalStaked": "352049989581424787111", + "veNFTs": ["246", "263"], + "rank": 81 + }, + { + "address": "0x959eBe8861481e0BC21B37444bEf96167BAd5834", + "totalStaked": "328609435872539568785", + "veNFTs": ["260"], + "rank": 82 + }, + { + "address": "0xF87C56EBE182Ac6866C5168fb24e1BEEa1Ffaa63", + "totalStaked": "316538043973851530000", + "veNFTs": ["40"], + "rank": 83 + }, + { + "address": "0x002748dB885D4CCfe3C7C92143f0805F4eBEEC01", + "totalStaked": "308515849129646000000", + "veNFTs": ["131"], + "rank": 84 + }, + { + "address": "0x74b0D3CA2709cb51738C179d66c4b70f3bB18778", + "totalStaked": "308008789367855850000", + "veNFTs": ["18"], + "rank": 85 + }, + { + "address": "0x7FBA15285f03FCb89419229bed2f14C03A1E81Da", + "totalStaked": "299725074521320300000", + "veNFTs": ["203"], + "rank": 86 + }, + { + "address": "0x4D2e59FB5d619A8b67Acd7E642CfCF3E8D8B0c20", + "totalStaked": "293845390895186879099", + "veNFTs": ["242"], + "rank": 87 + }, + { + "address": "0x9dd711B0cB4430F429231E5Cb9940DBD1952a36f", + "totalStaked": "268915992050197175442", + "veNFTs": ["55"], + "rank": 88 + }, + { + "address": "0x6d174ed3c6B7B557B25E8103dCdbB4d6aFe1C616", + "totalStaked": "257913453511212940000", + "veNFTs": ["146"], + "rank": 89 + }, + { + "address": "0xa485Cf32D76AA53B97a57150133c0C4882174562", + "totalStaked": "257907902207526200000", + "veNFTs": ["46"], + "rank": 90 + }, + { + "address": "0x673A02395d103E0cAa008378d9c5C6902d1A4F96", + "totalStaked": "241506177133707750000", + "veNFTs": ["99"], + "rank": 91 + }, + { + "address": "0x3b6239114d84A25D4CE995B344d399DD0a0ed4f6", + "totalStaked": "239981088585077940000", + "veNFTs": ["75"], + "rank": 92 + }, + { + "address": "0xF68d4b506ED84e4c9F30652dA9c511A32Bd1A192", + "totalStaked": "237620345698993152996", + "veNFTs": ["218", "227"], + "rank": 93 + }, + { + "address": "0xF17b03b741bB7162bD5236203B59197d216b7F3D", + "totalStaked": "233125919274892831244", + "veNFTs": ["34", "76"], + "rank": 94 + }, + { + "address": "0xd113a6a043c7c284de9A570171859D12C43A4F83", + "totalStaked": "233091198439186120000", + "veNFTs": ["186"], + "rank": 95 + }, + { + "address": "0x27796303C9a13Ad82F9608De6D038bbeED23A3C5", + "totalStaked": "233026544577208248804", + "veNFTs": ["35"], + "rank": 96 + }, + { + "address": "0x95b04b76f9a907c70b88748B985B64f46b310961", + "totalStaked": "229461961256163329672", + "veNFTs": ["12"], + "rank": 97 + }, + { + "address": "0x15F408322A4f8f0cA4b079EaF3DCDDBc2E06a158", + "totalStaked": "227726882160587420000", + "veNFTs": ["5"], + "rank": 98 + }, + { + "address": "0x923B26470a3164FEc6911D9cbF66B0426C72d2E9", + "totalStaked": "220000000000000000000", + "veNFTs": ["143"], + "rank": 99 + }, + { + "address": "0x7d12e2B0B687B5a13fC3D69FBbDec2c26BF7B1a6", + "totalStaked": "219116912191802980000", + "veNFTs": ["123"], + "rank": 100 + }, + { + "address": "0xa8a71D5fb2d5Fbc8f31799c5Cdd8AE67722885a5", + "totalStaked": "219012505873455669118", + "veNFTs": ["129"], + "rank": 101 + }, + { + "address": "0x9C572aD1BdcD12Fe8C277b04BFb655d98764126b", + "totalStaked": "208088723000000000000", + "veNFTs": ["262"], + "rank": 102 + }, + { + "address": "0xEdfd92393F7dFF3947DD841345569200c57c9775", + "totalStaked": "206268280742015567819", + "veNFTs": ["188"], + "rank": 103 + }, + { + "address": "0x732BE24f7ec08D12b7CeA42a8036841634ffa382", + "totalStaked": "200369764778320402230", + "veNFTs": ["73"], + "rank": 104 + }, + { + "address": "0x61dCD77F2175a68B72223E51bC573b91Dc9555bc", + "totalStaked": "200000000000000000000", + "veNFTs": ["2"], + "rank": 105 + }, + { + "address": "0x6744246EE02134fF28CB28E9eeBf55527C0CcFeE", + "totalStaked": "199141319980939980000", + "veNFTs": ["236"], + "rank": 106 + }, + { + "address": "0xF4a963BB46e1E235928D876a006D317D98fa45E8", + "totalStaked": "197786220515524720000", + "veNFTs": ["21"], + "rank": 107 + }, + { + "address": "0xA76b21A7D550F535A1b7836A9b7E454f91eC6B38", + "totalStaked": "195381266351225370227", + "veNFTs": ["23"], + "rank": 108 + }, + { + "address": "0x849dc276E7880096d9c56e5FC909CD7bbf751554", + "totalStaked": "185871288138271799238", + "veNFTs": ["259"], + "rank": 109 + }, + { + "address": "0xe6f11B9d80183141E0ae7076aED1d95B0C23d6D5", + "totalStaked": "184589341620419635636", + "veNFTs": ["51"], + "rank": 110 + }, + { + "address": "0xc1eA3db638495d90bB778863ff5C2AaCCfBbF421", + "totalStaked": "183556476224048893824", + "veNFTs": ["234"], + "rank": 111 + }, + { + "address": "0xFf36b9cb75C9178841D8b75Baf9776bfA59FBE9d", + "totalStaked": "182302002977065900000", + "veNFTs": ["175"], + "rank": 112 + }, + { + "address": "0xA97Be4812b99f4Cf014d3bd1F2Bae18aFff771B8", + "totalStaked": "161343778984600870000", + "veNFTs": ["124"], + "rank": 113 + }, + { + "address": "0xD127b81Cb0BC79434D4Eff419e22e8C96a4266c5", + "totalStaked": "151922939064796620000", + "veNFTs": ["213"], + "rank": 114 + }, + { + "address": "0x645100f1eF0A4cE989eB1ae5648769f1e87bd945", + "totalStaked": "149251948198231156293", + "veNFTs": ["208", "209"], + "rank": 115 + }, + { + "address": "0x925030586AEE5426E5A828c5C1b595727353daDA", + "totalStaked": "146649017934074640000", + "veNFTs": ["100"], + "rank": 116 + }, + { + "address": "0x000006eee6e39015cB523AeBDD4d0B1855aBa682", + "totalStaked": "143744270806092600000", + "veNFTs": ["221"], + "rank": 117 + }, + { + "address": "0x3e838417c23aB3a32415F20F4056bec9CA204594", + "totalStaked": "129768404081931667445", + "veNFTs": ["267", "269"], + "rank": 118 + }, + { + "address": "0x8C9422d475955723F62a7Cc022dCfF3da45D7478", + "totalStaked": "128013300320802600000", + "veNFTs": ["247"], + "rank": 119 + }, + { + "address": "0x9Dd9790372028087211e726C2992a9916B1CB8D8", + "totalStaked": "127062652040025463184", + "veNFTs": ["27"], + "rank": 120 + }, + { + "address": "0x11fd8f7B4e8Acf54Be24EE85517b90F8755E7A96", + "totalStaked": "126663665106740350000", + "veNFTs": ["74"], + "rank": 121 + }, + { + "address": "0x59E2835670c6854d098b6586D8a5B0530acf0889", + "totalStaked": "125576724905897280000", + "veNFTs": ["43"], + "rank": 122 + }, + { + "address": "0xAAd510A93DB84537c9720f457cefde18bfc669E8", + "totalStaked": "125523048908724186989", + "veNFTs": ["86"], + "rank": 123 + }, + { + "address": "0xc3048C60AfA9f823c492DE784F72cfaB49B945f0", + "totalStaked": "125016139896438738200", + "veNFTs": ["107"], + "rank": 124 + }, + { + "address": "0xa4898858C129f8BAD8aD066dD7a4aB370f3B9408", + "totalStaked": "121968382226654117915", + "veNFTs": ["154", "155"], + "rank": 125 + }, + { + "address": "0x9f66BDE6Cb2889dBa0b16868061D4093D7D5d0A1", + "totalStaked": "119354509980309512919", + "veNFTs": ["84"], + "rank": 126 + }, + { + "address": "0x7BFEe91193d9Df2Ac0bFe90191D40F23c773C060", + "totalStaked": "113822981181632454119", + "veNFTs": ["7"], + "rank": 127 + }, + { + "address": "0xC603483c46980ae77AD662d3f0750E23444Ad223", + "totalStaked": "111352667312211408155", + "veNFTs": ["258"], + "rank": 128 + }, + { + "address": "0x0a2427Fa7F2a56caB87708f830f5801c1c09Ddc7", + "totalStaked": "111000000000000000000", + "veNFTs": ["183"], + "rank": 129 + }, + { + "address": "0xb3467880204D2A2d5dbfF08Ee67584cDE9035253", + "totalStaked": "107038440193406114752", + "veNFTs": ["14"], + "rank": 130 + }, + { + "address": "0x53a806789BBfd366d9dEB9Cbe5d622089e845fdb", + "totalStaked": "105047678549566510000", + "veNFTs": ["111"], + "rank": 131 + }, + { + "address": "0x35226E78809a5DaE4562B0B1fC4cf96cdb6A01A8", + "totalStaked": "102066477793748460406", + "veNFTs": ["45"], + "rank": 132 + }, + { + "address": "0x0235De9f7A9514F92ca5D96ECe7d07D99719751D", + "totalStaked": "101984667468462350000", + "veNFTs": ["71"], + "rank": 133 + }, + { + "address": "0xb7696dCa94A198b4E3A10Fb37C37101f02D42613", + "totalStaked": "100563932086555177721", + "veNFTs": ["25"], + "rank": 134 + }, + { + "address": "0xdca6143E849247DF9d1A32264FE5108FE31ae878", + "totalStaked": "90258383025407962037", + "veNFTs": ["257"], + "rank": 135 + }, + { + "address": "0x6f4dE9DB463F6Bcf0FBa0280304c1367835D42B3", + "totalStaked": "87965833379936197717", + "veNFTs": ["252"], + "rank": 136 + }, + { + "address": "0x13b3531328734e34B4FD446D2F82cEacfFEf3118", + "totalStaked": "80357690643102513921", + "veNFTs": ["261", "272"], + "rank": 137 + }, + { + "address": "0xB3117A19adf8966D38b0c66E117F37497B86f139", + "totalStaked": "75463963419146920000", + "veNFTs": ["147"], + "rank": 138 + }, + { + "address": "0x04340938ffA7809D7333785Af662C408949a9560", + "totalStaked": "75396862231600345433", + "veNFTs": ["205", "238", "254"], + "rank": 139 + }, + { + "address": "0x1Aa8c34B91Cf89DDD5A959dA5099be91cebEeaD0", + "totalStaked": "66101694098197043926", + "veNFTs": ["251"], + "rank": 140 + }, + { + "address": "0xb56Ee863f444B1D58bd7eFEC48ee87A8aA3610Bc", + "totalStaked": "61453469061537309955", + "veNFTs": ["266"], + "rank": 141 + }, + { + "address": "0x87001fE0924B20441d3131631Cf0278d35862906", + "totalStaked": "61091930312742731655", + "veNFTs": ["264"], + "rank": 142 + }, + { + "address": "0xdD80549AC8378a43DD010E8375F3Ad5b63a5C228", + "totalStaked": "60904647877858841348", + "veNFTs": ["253"], + "rank": 143 + }, + { + "address": "0x3BfF8Bc467e45c07c6a7Ef75F76Ee7e559B02a7D", + "totalStaked": "60841706308154265001", + "veNFTs": ["194", "198"], + "rank": 144 + }, + { + "address": "0x565A6Ae3c8cAE20ac43e122FF814380E47599294", + "totalStaked": "55430241033377730000", + "veNFTs": ["159"], + "rank": 145 + }, + { + "address": "0xb8D175F16742395F530e0b3bC1d30BD06B78CdA9", + "totalStaked": "48442701947053967746", + "veNFTs": ["139", "173"], + "rank": 146 + }, + { + "address": "0x7C020587596Ee0772cD970B1Fa6a5ab4FEa10949", + "totalStaked": "42783448382652830000", + "veNFTs": ["214"], + "rank": 147 + }, + { + "address": "0x379b04Ce062eB3BB3D03E364358940f11e5bBD77", + "totalStaked": "42718517318866249562", + "veNFTs": ["255"], + "rank": 148 + }, + { + "address": "0xF42B1a25d903489c46F74c79A4Cd1c22330D231d", + "totalStaked": "30352858500666890000", + "veNFTs": ["201"], + "rank": 149 + }, + { + "address": "0x19c2A0E1398Ba78C00AF784fDEc4cD0bBE70A392", + "totalStaked": "30300000000000000000", + "veNFTs": ["191"], + "rank": 150 + }, + { + "address": "0x9883A6520c38F8763d3333B579d4fb21FD6Bf252", + "totalStaked": "27474389706727594733", + "veNFTs": ["152"], + "rank": 151 + }, + { + "address": "0x0E9B063789909565CEdA1Fba162474405A151E66", + "totalStaked": "27291825452918430000", + "veNFTs": ["137"], + "rank": 152 + }, + { + "address": "0x01975013eD48Ccbd745270cFb841FFFEdC07abBf", + "totalStaked": "26467403145242535000", + "veNFTs": ["126"], + "rank": 153 + }, + { + "address": "0xfEEBdE0b1f602a0149E0E78d183b3e9715f98075", + "totalStaked": "24586505711336358000", + "veNFTs": ["138"], + "rank": 154 + }, + { + "address": "0xA3a211bDFba8d614c3d030da6CDbC98940db3Ba8", + "totalStaked": "24574643413031550635", + "veNFTs": ["113"], + "rank": 155 + }, + { + "address": "0x4Cf2D94d0e4D6F03F402c0a56BDb58B656C49fA9", + "totalStaked": "23407904560323521860", + "veNFTs": ["150"], + "rank": 156 + }, + { + "address": "0x60aa4D0745DcA8261F3085ab395417E0Fb15817e", + "totalStaked": "22878614005462504516", + "veNFTs": ["122"], + "rank": 157 + }, + { + "address": "0x0494F503912C101Bfd76b88e4F5D8A33de284d1A", + "totalStaked": "22416787436687395087", + "veNFTs": ["271"], + "rank": 158 + }, + { + "address": "0x3a7dabF8Fc7ae93e256D92b12A15bBEFf75F68E3", + "totalStaked": "22172894905860815000", + "veNFTs": ["121"], + "rank": 159 + }, + { + "address": "0xe394C410aCAdb7e0d352E467Fe91262F1C3Be1D1", + "totalStaked": "22040588491020080000", + "veNFTs": ["130"], + "rank": 160 + }, + { + "address": "0xe5988E0A077491660bADdb23d2444c5519195596", + "totalStaked": "22019732469211998655", + "veNFTs": ["149"], + "rank": 161 + }, + { + "address": "0x4a03731625b66E2Aa6bBB0681d3764794f6b0389", + "totalStaked": "21975308599814500000", + "veNFTs": ["142"], + "rank": 162 + }, + { + "address": "0x576AadA631E77700AD16A4cB4cD7209129f26907", + "totalStaked": "21745573298225313000", + "veNFTs": ["128"], + "rank": 163 + }, + { + "address": "0xE1a9736E18fFa353b67A8797bfD93852E28250f6", + "totalStaked": "21508314362610618000", + "veNFTs": ["177", "178"], + "rank": 164 + }, + { + "address": "0x1014A66402Ff5b51d86a527da1dbe96343Bd9D95", + "totalStaked": "21075528630937298359", + "veNFTs": ["161"], + "rank": 165 + }, + { + "address": "0x24FF4FaCDA99CE0E00f441b85A541dae8520F985", + "totalStaked": "20999457096053263000", + "veNFTs": ["225"], + "rank": 166 + }, + { + "address": "0xD8Ae876262b86C86D7fF6303775810F695Eea424", + "totalStaked": "20821282722625529280", + "veNFTs": ["192"], + "rank": 167 + }, + { + "address": "0x3e97b8dA1E88C8A318069A8b35DFf8a7b20df636", + "totalStaked": "20521359881036594135", + "veNFTs": ["195"], + "rank": 168 + }, + { + "address": "0x00cC1e6ed4d6AFEA76f7104F7f2f0d20f255a24e", + "totalStaked": "18675220746577175000", + "veNFTs": ["220"], + "rank": 169 + }, + { + "address": "0x77535aEF7F300d9ce6d9dE85A7E813F8DcF23Bc5", + "totalStaked": "18503267594385153242", + "veNFTs": ["109"], + "rank": 170 + }, + { + "address": "0xca99330589dAa7912d9F058259fc7057b866dDd2", + "totalStaked": "17697970098558805000", + "veNFTs": ["224"], + "rank": 171 + }, + { + "address": "0x1cc46367d9055Ce7d63f772368470E4e548b3B4b", + "totalStaked": "16388344680533561984", + "veNFTs": ["215"], + "rank": 172 + }, + { + "address": "0x57A07e048A4DC20De692dc6e176a8E03fa14200b", + "totalStaked": "16207316370884584000", + "veNFTs": ["151"], + "rank": 173 + }, + { + "address": "0xD2EE27c1f2Eb1175B45d24FB0F91304F017C49C3", + "totalStaked": "14878792834309719000", + "veNFTs": ["222"], + "rank": 174 + }, + { + "address": "0x9De710acBab5935149655DFc708ED3fD68D2250C", + "totalStaked": "14769160747114605551", + "veNFTs": ["270"], + "rank": 175 + }, + { + "address": "0x4Fa05a6D57f40Fd4BB660d687796cCAB5e8ab99f", + "totalStaked": "13613070960224288000", + "veNFTs": ["243"], + "rank": 176 + }, + { + "address": "0x92954C71e5713644768eC1fa50e7254600DCf504", + "totalStaked": "13230879676791652000", + "veNFTs": ["207"], + "rank": 177 + }, + { + "address": "0xa21b23e5f07E5E28cB42d09502227Eb75b0B64B5", + "totalStaked": "12576215838743071943", + "veNFTs": ["235"], + "rank": 178 + }, + { + "address": "0xbDec7287C08C0c153F9843C4796D59e3BF7178E4", + "totalStaked": "12275943962846343103", + "veNFTs": ["265"], + "rank": 179 + }, + { + "address": "0xfE904aE4a2dFcCa42C50607a254EC33D6926CF29", + "totalStaked": "11520770357793788000", + "veNFTs": ["184"], + "rank": 180 + }, + { + "address": "0x7E7fE107E85f626903497FAF0f4240b9E18a2672", + "totalStaked": "11000000000000000000", + "veNFTs": ["219"], + "rank": 181 + }, + { + "address": "0x4cc9b2e2879e7926B018D8d109692014295b51D1", + "totalStaked": "10944889087119022545", + "veNFTs": ["141"], + "rank": 182 + }, + { + "address": "0xDdaC95994D0C3B6773801D26210e2b6b2F8e8dab", + "totalStaked": "10925377276599898000", + "veNFTs": ["216"], + "rank": 183 + }, + { + "address": "0x7241C18533ACF5F786c5A0b5f9c2C370620c5c6f", + "totalStaked": "10918877837526835581", + "veNFTs": ["248"], + "rank": 184 + }, + { + "address": "0x7Bf43582130Cc3Da12E1B1b73A4Be45229920FEC", + "totalStaked": "10389829062199879443", + "veNFTs": ["156"], + "rank": 185 + }, + { + "address": "0xDeFE47617bF5E649915232fAF7953Bc6029C51b7", + "totalStaked": "10295221591219536999", + "veNFTs": ["134"], + "rank": 186 + }, + { + "address": "0x9a5f6821917B9D733892048EC62fED80e939d01a", + "totalStaked": "10182136326265828000", + "veNFTs": ["168"], + "rank": 187 + }, + { + "address": "0x1277255B8e274C8a753EeA277aA62115bAeD21Eb", + "totalStaked": "10100000000000000000", + "veNFTs": ["185"], + "rank": 188 + } + ] +} diff --git a/test/fork/leaderboard-to-stakers.ts b/test/fork/leaderboard-to-stakers.ts new file mode 100644 index 0000000..2b8371d --- /dev/null +++ b/test/fork/leaderboard-to-stakers.ts @@ -0,0 +1,11 @@ +// read from the file test/fork/bpt-leaderboard.json +import fs from "fs"; + +export const leaderboardToStakers = JSON.parse( + fs.readFileSync("./test/fork/bpt-leaderboard.json", "utf8"), +); +// extract the addresses only + +const data = leaderboardToStakers.bpt.map(({ address }) => address); +// write back to stakers.json in the same folder +fs.writeFileSync("./test/fork/stakers.json", JSON.stringify(data, null, 2)); diff --git a/test/fork/stakers.json b/test/fork/stakers.json new file mode 100644 index 0000000..1a4c342 --- /dev/null +++ b/test/fork/stakers.json @@ -0,0 +1,190 @@ +[ + "0x9ff471F9f98F42E5151C7855fD1b5aa906b1AF7e", + "0x126FF8faBeC84EF8a96F25dD0C2668fB9b7E7088", + "0x7B59793aD075e4ce1e35181054759C080B8D965D", + "0x8bF1e340055c7dE62F11229A149d3A1918de3d74", + "0x2662467718Bd14c8407a5E0A6657C9009FEb2D44", + "0xa0Eb44b173AF64f2FdB47C7d76FF3A33e04103b6", + "0xECEe8B6F8f2B4900f631EbC91021BAf95Bb7E99F", + "0xAdffC760EDF4f6146dce89022B5AE7EbB7edD2B9", + "0xa6bBAd15B9cc15cd1f81874D98dBb78F522161dC", + "0x99AfD53f807766A8B98400B0C785E500c041F32B", + "0x53D8EDF6a54239eB785eC72213919Fb6b6B73598", + "0x9DC3E0fC228c2455773E70ee9E987aE2a6FBc07D", + "0x652AB9e3577eD85d7fC9A41fAFf68E9DA4Db00Ff", + "0x02E7b714fae84e4BA80f3CDa5508553e7CF5042A", + "0xB4B1b6928337974D98843f45aBbb8B38DFB1B72C", + "0x6f9BB7e454f5B3eb2310343f0E99269dC2BB8A1d", + "0xC02D68B1287534d8D12a9bE01A5B4ef5A1771bA3", + "0xb740FF2bDD86330FF51dD3bB7eF933566f364793", + "0x16B00dB74167b7469dDa63b674Ca9A3B1b70C439", + "0x71535AAe1B6C0c51Db317B54d5eEe72d1ab843c1", + "0xaf1C56ABb73ce0C13cCeE81b59479A5D0177eaB9", + "0xe16609a91291Dd45aBFCd8555b6cb3800A86a4A8", + "0xc7E82FA77f32BFD6AeeD59924a1a564a924b2EA8", + "0x059F4c04295bde54893C24025d8229E35318d3Cc", + "0x77b90c77c5e647b6ad208DFF364cDD42D44De124", + "0xD83D994B102A5E6B452469D26Fe5acd1608954f9", + "0x3fE2e08a1F7766606574a91a5CfAf3342d023947", + "0xd5dBb9e213545a4d8248d671BfCf6E5fa6E72fAF", + "0xF4b40B20572D117EAD7b5ca3C7eFf7d3CBdC4067", + "0xEB4576fE753DAB07635c0Bb6c8f0A355e1Db5d31", + "0xa9D403b3905b7b5c9c345f711A8B9E858aaA4856", + "0xA5274842aa457d8A1a2AAdB9dFB37B266C8d30e5", + "0x8DcAC18996dC58b5eebfDC5D0A7b22E0B951A4b7", + "0x4B02af3D86A380A90F0b413A0D6414FaCf2cde4a", + "0x4C38705A7B26f1D90dEbca268D99c79520d793cD", + "0x2bcd8898A678050EFA5F41F4c6012045dee1B5c3", + "0xE3053cDe2124eAA6103DddE8E5c6BD36c66F716E", + "0x20151c34D01D6785493F3416b3f82812a3dbB46F", + "0xcc5B087283D69dB4237Da489d487140f2745fD28", + "0x672A7B6431B8C77BEE625963F14fD8C388b682D2", + "0x5f1Da18007f212A0281AC98438fEbcc44F0f3709", + "0xB4173fEe41920a8e7D3451863f9779468EbAb98E", + "0xcF8373446363216f0daC6ace39cd3dB12cF8689b", + "0x36A5639479f44F6540623046BE1b8bDd04b42Cc3", + "0x2D6b9A471DB03Ecf92927e6921DdB17932Fffa64", + "0x70b88351Cd1fF4C637556462734c61785AF4fCa5", + "0xEB2D541a7599eeCc86049e366AA82A914A7cbc74", + "0xb1AC9db0d6a1eC291F427ad03fc3B632E1E93a56", + "0x040007A949fa726f50810f6C98b1B620b50793a2", + "0x9a10586aA206a401Ba50A168a847686d1E500dC6", + "0x28288639D997B8aF1F4cb25190818C0001372fC8", + "0xEF629261993A1eE4f6F64f651820CB3f44A58ba9", + "0x32a59b87352e980dD6aB1bAF462696D28e63525D", + "0x398851f0412decb07d5A1Ba900c352C1aAd7b432", + "0x48555a6c6c88cbe69D36A44CA6095b731F453597", + "0xc77a40a9a04Ef9e8DB7fC4F046ECe7c3C4F6867C", + "0xAFbFE5B459e0985B6bF9F2c1Da086f182b930Cc1", + "0xBDac6532be38Db1D48640d46C3011dF6D4C099Ec", + "0xB8cd02828c214e8549B9e1951fbF049A457b2117", + "0x37749f66F360D9DE7B8B1F7928d839aa52aB2f91", + "0x896C20Da40c2A4df9B7C98B16a8D5A95129161a5", + "0x3906DDBaE2D8aF0581a38a09c90E14b48f3D2F94", + "0x0b1d114d9249ad870F11543215722D7804CC3dbA", + "0x4770F67Db9D09Ca7347C1fCcBf3795A464065eCB", + "0xd48f1A44e56526e96A983FF750559aa365810D5A", + "0x29F38498071b772a99bAb18C7ea4125387Ed2332", + "0xEe6066b7655657e34f6190629a64F0a8014d3a96", + "0xF05454C3D16090b652cf5389C9eE520ceC477E5e", + "0x3844597c2b75E8510d8895173adDC28dd995fA45", + "0xee206E05D50783b2d5c553A56917e990a6BB6750", + "0x3021A59F56cAe360183C40965D7059a4a73A7Ba7", + "0x40677e9D3CBAfD1522a1baEdECebA68Aca35dB4D", + "0x530BcD1CE67288cC0EEb67C1D337De2Bf849CfB3", + "0xeE9A0bbD2cE67fc008fE3f7f6a86F2c6dA5F7a65", + "0xc5115D9D85256cC93836189D21d23729014B9Afe", + "0x13e0d0A9e4024F1804FA2a0dde4F7c38abCc63F7", + "0x30a2837F3D319A3e1984DE5a32C0DF310e2496B7", + "0xADB40fbC28BC71856BFc46CF3e7Ff36cB44ba37D", + "0x9AeE301D7B11a6dc7293B7b6ba5f11cd048590dF", + "0xD66fa7a53803E34fAd9A0eCbd52111E93d0B3542", + "0x4D5FeEdA8AacE603b44FeFCBFc82DE5336e17aCd", + "0x959eBe8861481e0BC21B37444bEf96167BAd5834", + "0xF87C56EBE182Ac6866C5168fb24e1BEEa1Ffaa63", + "0x002748dB885D4CCfe3C7C92143f0805F4eBEEC01", + "0x74b0D3CA2709cb51738C179d66c4b70f3bB18778", + "0x7FBA15285f03FCb89419229bed2f14C03A1E81Da", + "0x4D2e59FB5d619A8b67Acd7E642CfCF3E8D8B0c20", + "0x9dd711B0cB4430F429231E5Cb9940DBD1952a36f", + "0x6d174ed3c6B7B557B25E8103dCdbB4d6aFe1C616", + "0xa485Cf32D76AA53B97a57150133c0C4882174562", + "0x673A02395d103E0cAa008378d9c5C6902d1A4F96", + "0x3b6239114d84A25D4CE995B344d399DD0a0ed4f6", + "0xF68d4b506ED84e4c9F30652dA9c511A32Bd1A192", + "0xF17b03b741bB7162bD5236203B59197d216b7F3D", + "0xd113a6a043c7c284de9A570171859D12C43A4F83", + "0x27796303C9a13Ad82F9608De6D038bbeED23A3C5", + "0x95b04b76f9a907c70b88748B985B64f46b310961", + "0x15F408322A4f8f0cA4b079EaF3DCDDBc2E06a158", + "0x923B26470a3164FEc6911D9cbF66B0426C72d2E9", + "0x7d12e2B0B687B5a13fC3D69FBbDec2c26BF7B1a6", + "0xa8a71D5fb2d5Fbc8f31799c5Cdd8AE67722885a5", + "0x9C572aD1BdcD12Fe8C277b04BFb655d98764126b", + "0xEdfd92393F7dFF3947DD841345569200c57c9775", + "0x732BE24f7ec08D12b7CeA42a8036841634ffa382", + "0x61dCD77F2175a68B72223E51bC573b91Dc9555bc", + "0x6744246EE02134fF28CB28E9eeBf55527C0CcFeE", + "0xF4a963BB46e1E235928D876a006D317D98fa45E8", + "0xA76b21A7D550F535A1b7836A9b7E454f91eC6B38", + "0x849dc276E7880096d9c56e5FC909CD7bbf751554", + "0xe6f11B9d80183141E0ae7076aED1d95B0C23d6D5", + "0xc1eA3db638495d90bB778863ff5C2AaCCfBbF421", + "0xFf36b9cb75C9178841D8b75Baf9776bfA59FBE9d", + "0xA97Be4812b99f4Cf014d3bd1F2Bae18aFff771B8", + "0xD127b81Cb0BC79434D4Eff419e22e8C96a4266c5", + "0x645100f1eF0A4cE989eB1ae5648769f1e87bd945", + "0x925030586AEE5426E5A828c5C1b595727353daDA", + "0x000006eee6e39015cB523AeBDD4d0B1855aBa682", + "0x3e838417c23aB3a32415F20F4056bec9CA204594", + "0x8C9422d475955723F62a7Cc022dCfF3da45D7478", + "0x9Dd9790372028087211e726C2992a9916B1CB8D8", + "0x11fd8f7B4e8Acf54Be24EE85517b90F8755E7A96", + "0x59E2835670c6854d098b6586D8a5B0530acf0889", + "0xAAd510A93DB84537c9720f457cefde18bfc669E8", + "0xc3048C60AfA9f823c492DE784F72cfaB49B945f0", + "0xa4898858C129f8BAD8aD066dD7a4aB370f3B9408", + "0x9f66BDE6Cb2889dBa0b16868061D4093D7D5d0A1", + "0x7BFEe91193d9Df2Ac0bFe90191D40F23c773C060", + "0xC603483c46980ae77AD662d3f0750E23444Ad223", + "0x0a2427Fa7F2a56caB87708f830f5801c1c09Ddc7", + "0xb3467880204D2A2d5dbfF08Ee67584cDE9035253", + "0x53a806789BBfd366d9dEB9Cbe5d622089e845fdb", + "0x35226E78809a5DaE4562B0B1fC4cf96cdb6A01A8", + "0x0235De9f7A9514F92ca5D96ECe7d07D99719751D", + "0xb7696dCa94A198b4E3A10Fb37C37101f02D42613", + "0xdca6143E849247DF9d1A32264FE5108FE31ae878", + "0x6f4dE9DB463F6Bcf0FBa0280304c1367835D42B3", + "0x13b3531328734e34B4FD446D2F82cEacfFEf3118", + "0xB3117A19adf8966D38b0c66E117F37497B86f139", + "0x04340938ffA7809D7333785Af662C408949a9560", + "0x1Aa8c34B91Cf89DDD5A959dA5099be91cebEeaD0", + "0xb56Ee863f444B1D58bd7eFEC48ee87A8aA3610Bc", + "0x87001fE0924B20441d3131631Cf0278d35862906", + "0xdD80549AC8378a43DD010E8375F3Ad5b63a5C228", + "0x3BfF8Bc467e45c07c6a7Ef75F76Ee7e559B02a7D", + "0x565A6Ae3c8cAE20ac43e122FF814380E47599294", + "0xb8D175F16742395F530e0b3bC1d30BD06B78CdA9", + "0x7C020587596Ee0772cD970B1Fa6a5ab4FEa10949", + "0x379b04Ce062eB3BB3D03E364358940f11e5bBD77", + "0xF42B1a25d903489c46F74c79A4Cd1c22330D231d", + "0x19c2A0E1398Ba78C00AF784fDEc4cD0bBE70A392", + "0x9883A6520c38F8763d3333B579d4fb21FD6Bf252", + "0x0E9B063789909565CEdA1Fba162474405A151E66", + "0x01975013eD48Ccbd745270cFb841FFFEdC07abBf", + "0xfEEBdE0b1f602a0149E0E78d183b3e9715f98075", + "0xA3a211bDFba8d614c3d030da6CDbC98940db3Ba8", + "0x4Cf2D94d0e4D6F03F402c0a56BDb58B656C49fA9", + "0x60aa4D0745DcA8261F3085ab395417E0Fb15817e", + "0x0494F503912C101Bfd76b88e4F5D8A33de284d1A", + "0x3a7dabF8Fc7ae93e256D92b12A15bBEFf75F68E3", + "0xe394C410aCAdb7e0d352E467Fe91262F1C3Be1D1", + "0xe5988E0A077491660bADdb23d2444c5519195596", + "0x4a03731625b66E2Aa6bBB0681d3764794f6b0389", + "0x576AadA631E77700AD16A4cB4cD7209129f26907", + "0xE1a9736E18fFa353b67A8797bfD93852E28250f6", + "0x1014A66402Ff5b51d86a527da1dbe96343Bd9D95", + "0x24FF4FaCDA99CE0E00f441b85A541dae8520F985", + "0xD8Ae876262b86C86D7fF6303775810F695Eea424", + "0x3e97b8dA1E88C8A318069A8b35DFf8a7b20df636", + "0x00cC1e6ed4d6AFEA76f7104F7f2f0d20f255a24e", + "0x77535aEF7F300d9ce6d9dE85A7E813F8DcF23Bc5", + "0xca99330589dAa7912d9F058259fc7057b866dDd2", + "0x1cc46367d9055Ce7d63f772368470E4e548b3B4b", + "0x57A07e048A4DC20De692dc6e176a8E03fa14200b", + "0xD2EE27c1f2Eb1175B45d24FB0F91304F017C49C3", + "0x9De710acBab5935149655DFc708ED3fD68D2250C", + "0x4Fa05a6D57f40Fd4BB660d687796cCAB5e8ab99f", + "0x92954C71e5713644768eC1fa50e7254600DCf504", + "0xa21b23e5f07E5E28cB42d09502227Eb75b0B64B5", + "0xbDec7287C08C0c153F9843C4796D59e3BF7178E4", + "0xfE904aE4a2dFcCa42C50607a254EC33D6926CF29", + "0x7E7fE107E85f626903497FAF0f4240b9E18a2672", + "0x4cc9b2e2879e7926B018D8d109692014295b51D1", + "0xDdaC95994D0C3B6773801D26210e2b6b2F8e8dab", + "0x7241C18533ACF5F786c5A0b5f9c2C370620c5c6f", + "0x7Bf43582130Cc3Da12E1B1b73A4Be45229920FEC", + "0xDeFE47617bF5E649915232fAF7953Bc6029C51b7", + "0x9a5f6821917B9D733892048EC62fED80e939d01a", + "0x1277255B8e274C8a753EeA277aA62115bAeD21Eb" +] \ No newline at end of file diff --git a/test/voting/GaugeVote.t.sol b/test/voting/GaugeVote.t.sol index ed3caf2..333a1f3 100644 --- a/test/voting/GaugeVote.t.sol +++ b/test/voting/GaugeVote.t.sol @@ -118,7 +118,7 @@ contract TestGaugeVote is GaugeVotingBase { assertEq(voter.gaugeVotes(gauge), 0); } - function testFuzz_canResetAnytime(uint _time) public { + function testFuzz_canResetAnytime(uint48 _time) public { // create the vote votes.push(GaugeVote(1, gauge));