From 6526a75cb6d62a7f459fc1b362fca276720fb4f4 Mon Sep 17 00:00:00 2001 From: Adam Cuculich <46691282+cucupac@users.noreply.github.com> Date: Mon, 23 Oct 2023 23:15:46 -0500 Subject: [PATCH] test: fix existing unit tests and fix coverage report (#5) * test: fix existing unit tests and fix coverage report * chore: add requested changes * test: add cannot deploy tests, add remappings, fix linter * test: add proxy tests, refactor existing tests to use proxy * test: add admin unit tests (extract funds from contract) * test: add receive unit test * test: add requested changes --- .env.example | 1 + .github/workflows/ci.yml | 99 +++--- .solhint.json | 1 + package-lock.json | 339 ++++++++++++++++++++ package.json | 7 +- remappings.txt | 3 + src/PredictiveDeployer.sol | 5 +- src/interfaces/IPredictiveDeployer.sol | 9 + src/interfaces/IPredictiveDeployerAdmin.sol | 12 + test/Child.t.sol | 2 +- test/PredictiveDeployer.admin.t.sol | 105 ++++++ test/PredictiveDeployer.t.sol | 167 +++++++--- test/common/constants.t.sol | 6 + test/test_proxy/ERC1967Proxy.t.sol | 52 +++ 14 files changed, 702 insertions(+), 106 deletions(-) create mode 100644 .env.example create mode 100644 remappings.txt create mode 100644 src/interfaces/IPredictiveDeployer.sol create mode 100644 src/interfaces/IPredictiveDeployerAdmin.sol create mode 100644 test/PredictiveDeployer.admin.t.sol create mode 100644 test/common/constants.t.sol create mode 100644 test/test_proxy/ERC1967Proxy.t.sol diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0e21983 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +ETHEREUM_RPC=your_rpc_url \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a77ada..723ac08 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,69 +1,70 @@ name: ci on: - push: - branches: - - main - pull_request: + push: + branches: + - main + pull_request: env: - ETHEREUM_RPC: ${{ secrets.ETHEREUM_RPC }} + ETHEREUM_RPC: ${{ secrets.ETHEREUM_RPC }} jobs: - run-ci: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive + run-ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v3 - - name: Install node dependencies - run: npm install + - name: Install node dependencies + run: npm install - - name: Set up python - id: setup-python - uses: actions/setup-python@v3 - with: - python-version: 3.9 + - name: Set up python + id: setup-python + uses: actions/setup-python@v3 + with: + python-version: 3.9 - - name: Install Poetry - uses: snok/install-poetry@v1 + - name: Install Poetry + uses: snok/install-poetry@v1 - - name: Load cached venv - id: cached-poetry-dependencies - uses: actions/cache@v3 - with: - path: .venv - key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v3 + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} - - name: Install python dependencies - run: poetry install --no-interaction + - name: Install python dependencies + run: poetry install --no-interaction - - name: Install Foundry - uses: onbjerg/foundry-toolchain@v1 - with: - version: nightly + - name: Install Foundry + uses: onbjerg/foundry-toolchain@v1 + with: + version: nightly - - name: Pull Submodules - run: forge update + - name: Pull Submodules + run: forge update - - name: Check formatting - run: npm run format:check + - name: Check formatting + run: npm run format:check - - name: Check linting - run: npm run lint:check + - name: Check linting + run: npm run lint:check - - name: Run tests - run: forge test --optimize --fork-url ${{ env.ETHEREUM_RPC }} + - name: Run tests + run: forge test --optimize --fork-url ${{ env.ETHEREUM_RPC }} - - name: Coverage - run: | - forge coverage --report lcov - id: coverage + - name: Coverage + run: | + sudo apt update && sudo apt install -y lcov + npm run coverage + id: coverage - - uses: codecov/codecov-action@v2 + - uses: codecov/codecov-action@v2 - # Too slow to run regularly - #- name: Run audit - # run: poetry run slither --solc-remaps @openzeppelin=lib/openzeppelin-contracts --solc-args optimize src/ + # Too slow to run regularly + #- name: Run audit + # run: poetry run slither --solc-remaps @openzeppelin=lib/openzeppelin-contracts --solc-args optimize src/ diff --git a/.solhint.json b/.solhint.json index d2f5bf8..3480695 100644 --- a/.solhint.json +++ b/.solhint.json @@ -1,6 +1,7 @@ { "extends": "solhint:recommended", "rules": { + "compiler-version": ["error",">=0.8.0"], "no-inline-assembly": ["off", ">=0.8.0"] } } diff --git a/package-lock.json b/package-lock.json index 1077805..9f2e699 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "commitizen": "^4.3.0", "cz-conventional-changelog": "^3.3.0", "husky": "^8.0.0", + "lcov-summary": "^1.0.1", "solhint": "^3.6.2" } }, @@ -794,6 +795,12 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, "node_modules/cachedir": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", @@ -803,6 +810,20 @@ "node": ">=6" } }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1055,6 +1076,51 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/conventional-changelog-angular": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-6.0.0.tgz", @@ -1103,6 +1169,12 @@ "node": ">=14" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -1307,6 +1379,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", @@ -1540,6 +1643,15 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1549,6 +1661,21 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -1653,6 +1780,18 @@ "which": "bin/which" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1677,6 +1816,27 @@ "node": ">= 0.4.0" } }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1686,6 +1846,42 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -1983,6 +2179,12 @@ "node": ">=0.10.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2065,6 +2267,83 @@ "node": ">=0.10.0" } }, + "node_modules/lcov-parse": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", + "integrity": "sha512-YsL0D4QF/vNlNcHPXM832si9d2ROryFQ4r4JvcfMIiUYr1f6WULuO75YCtxNu4P+XMRHz0SfUc524+c+U3G5kg==", + "dev": true + }, + "node_modules/lcov-summary": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lcov-summary/-/lcov-summary-1.0.1.tgz", + "integrity": "sha512-nuLd3rHSRP5W/pzoqpvAVTz3UfUr9aMQsN12Ddw0HBppG8tbecr4Aj9ZE3d85wRRrrycjdKvQqwDPncYFrHAOA==", + "dev": true, + "dependencies": { + "chalk": "^1.1.1", + "concat-stream": "^1.5.1", + "lcov-parse": "0.0.10", + "object.assign": "^4.0.3", + "sprintf-js": "^1.0.3" + }, + "bin": { + "lcov-summary": "bin/cmd.js" + } + }, + "node_modules/lcov-summary/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lcov-summary/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lcov-summary/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lcov-summary/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lcov-summary/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -2355,6 +2634,33 @@ "node": ">=8" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2568,6 +2874,12 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -2883,6 +3195,21 @@ "node": ">=10" } }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2999,6 +3326,12 @@ "readable-stream": "^3.0.0" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -3262,6 +3595,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true + }, "node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", diff --git a/package.json b/package.json index e9615d4..8e522f9 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,13 @@ }, "scripts": { "commit": "cz", - "coverage": "forge coverage --report lcov", + "coverage": "forge coverage --report lcov && npm run coverage:filter && npx lcov-summary lcov.info", + "coverage:filter": "lcov -r lcov.info 'test/*' 'script/*' 'src/dependencies/*' -o lcov.info", "format": "forge fmt", "format:check": "forge fmt --check", "lint:check": "solhint --config ./.solhint.json 'src/**/*.sol'", "prepare": "husky install", - "snapshot": "forge snapshot", - "test": "forge test --optimize --fork-url https://rpc.ankr.com/eth && forge coverage && forge snapshot" + "test": "forge test --optimize && npm run coverage" }, "commitlint": { "extends": [ @@ -33,6 +33,7 @@ "commitizen": "^4.3.0", "cz-conventional-changelog": "^3.3.0", "husky": "^8.0.0", + "lcov-summary": "^1.0.1", "solhint": "^3.6.2" } } diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..2ff87bc --- /dev/null +++ b/remappings.txt @@ -0,0 +1,3 @@ +ds-test/=lib/forge-std/lib/ds-test/src/ +forge-std/=lib/forge-std/src/ +solmate/=lib/solmate/src/ diff --git a/src/PredictiveDeployer.sol b/src/PredictiveDeployer.sol index 227a654..a753437 100644 --- a/src/PredictiveDeployer.sol +++ b/src/PredictiveDeployer.sol @@ -32,8 +32,7 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable { } /// @dev Required by the UUPS module. - // solhint-disable-next-line - function _authorizeUpgrade(address) internal override onlyOwner { } + function _authorizeUpgrade(address) internal override onlyOwner { } // solhint-disable-line no-empty-blocks function _computeDomainSeparator() internal view returns (bytes32) { return keccak256( @@ -136,4 +135,6 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable { SafeTransferLib.safeTransfer(ERC20(_token), msg.sender, balance); } + + receive() external payable { } // solhint-disable-line no-empty-blocks } diff --git a/src/interfaces/IPredictiveDeployer.sol b/src/interfaces/IPredictiveDeployer.sol new file mode 100644 index 0000000..25e7353 --- /dev/null +++ b/src/interfaces/IPredictiveDeployer.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +interface IPredictiveDeployer { + function userNonces(address _principal) external returns (uint256); + function getTransactionHash(address _principal, uint256 _nonce) external returns (bytes32); + function getAddress(address _principal, bytes memory _bytecode) external returns (address); + function deploy(address _principal, bytes memory _signature, bytes memory _bytecode) external returns (address); +} diff --git a/src/interfaces/IPredictiveDeployerAdmin.sol b/src/interfaces/IPredictiveDeployerAdmin.sol new file mode 100644 index 0000000..acc498d --- /dev/null +++ b/src/interfaces/IPredictiveDeployerAdmin.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import { IPredictiveDeployer } from "./IPredictiveDeployer.sol"; + +interface IPredictiveDeployerAdmin is IPredictiveDeployer { + error UUPSUnauthorizedCallContext(); + + function upgradeToAndCall(address newImplementation, bytes memory data) external; + function extractNative() external; + function extractERC20(address _token) external; +} diff --git a/test/Child.t.sol b/test/Child.t.sol index 8b5888c..d38e6f5 100644 --- a/test/Child.t.sol +++ b/test/Child.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.21; contract Child { - uint256 number; + uint256 public number; address public admin; constructor(address _admin) { diff --git a/test/PredictiveDeployer.admin.t.sol b/test/PredictiveDeployer.admin.t.sol new file mode 100644 index 0000000..f889598 --- /dev/null +++ b/test/PredictiveDeployer.admin.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +import { Test } from "forge-std/Test.sol"; +import { PredictiveDeployer } from "../src/PredictiveDeployer.sol"; +import { ERC1967Proxy } from "../src/dependencies/proxy/ERC1967Proxy.sol"; +import { IERC20 } from "../src/dependencies/token/interfaces/IERC20.sol"; +import { IPredictiveDeployerAdmin } from "../src/interfaces/IPredictiveDeployerAdmin.sol"; +import { CONTRACT_DEPLOYER, TEST_ERC20_TOKEN } from "./common/constants.t.sol"; + +contract PredictiveDeployerTest is Test { + /* solhint-disable func-name-mixedcase */ + + PredictiveDeployer public implementation; + ERC1967Proxy public proxy; + + function setUp() public { + // Instantiate contracts + implementation = new PredictiveDeployer(); + vm.prank(CONTRACT_DEPLOYER); + proxy = new ERC1967Proxy(address(implementation), abi.encodeWithSignature("initialize()")); + } + + function test_ExtractNative(uint256 amount) public { + // Setup + vm.assume(amount > 0 && amount < 1e22); + vm.deal(address(proxy), amount); + + assertEq(address(proxy).balance, amount); + + uint256 preLocalBalance = CONTRACT_DEPLOYER.balance; + + // Act + vm.prank(CONTRACT_DEPLOYER); + IPredictiveDeployerAdmin(address(proxy)).extractNative(); + + assertEq(CONTRACT_DEPLOYER.balance, preLocalBalance + amount); + } + + function testFail_ExtractNativeUnauthorized(uint256 amount, address invalidExtractor) public { + // Setup + vm.assume(amount > 0 && amount < 1e22); + vm.assume(invalidExtractor != CONTRACT_DEPLOYER); + vm.deal(address(proxy), amount); + + // Act + vm.prank(invalidExtractor); + IPredictiveDeployerAdmin(address(proxy)).extractNative(); + } +} + +contract PredictiveDeployerForkTest is Test { + /* solhint-disable func-name-mixedcase */ + + PredictiveDeployer public implementation; + ERC1967Proxy public proxy; + uint256 public mainnetFork; + + function setUp() public { + // Setup: use mainnet fork + mainnetFork = vm.createFork(vm.envString("ETHEREUM_RPC")); + vm.selectFork(mainnetFork); + + // Instantiate contracts + implementation = new PredictiveDeployer(); + vm.prank(CONTRACT_DEPLOYER); + proxy = new ERC1967Proxy(address(implementation), abi.encodeWithSignature("initialize()")); + } + + function test_ActiveFork() public { + assertEq(vm.activeFork(), mainnetFork); + } + + function test_ExtractERC20(uint256 amount) public { + // Assumptions + vm.assume(amount > 0 && amount <= 1e6 * 1e5); + + // Setup: give the contract ERC20 tokens + deal(TEST_ERC20_TOKEN, address(proxy), amount); + + // Pre-action assertions + assertEq(IERC20(TEST_ERC20_TOKEN).balanceOf(address(proxy)), amount); + + // Act + vm.prank(CONTRACT_DEPLOYER); + IPredictiveDeployerAdmin(address(proxy)).extractERC20(TEST_ERC20_TOKEN); + + // Post-action assertions + assertEq(IERC20(TEST_ERC20_TOKEN).balanceOf(address(proxy)), 0); + assertEq(IERC20(TEST_ERC20_TOKEN).balanceOf(CONTRACT_DEPLOYER), amount); + } + + function testFail_ExtractERC20Unauthorized(uint256 amount, address invalidExtractor) public { + // Assumptions + vm.assume(amount > 0 && amount <= 1e6 * 1e5); + vm.assume(invalidExtractor != CONTRACT_DEPLOYER); + + // Setup: give the contract ERC20 tokens + deal(TEST_ERC20_TOKEN, address(proxy), amount); + + // Act + vm.prank(invalidExtractor); + IPredictiveDeployerAdmin(address(proxy)).extractERC20(TEST_ERC20_TOKEN); + } +} diff --git a/test/PredictiveDeployer.t.sol b/test/PredictiveDeployer.t.sol index 0f1df7e..df0de82 100644 --- a/test/PredictiveDeployer.t.sol +++ b/test/PredictiveDeployer.t.sol @@ -2,18 +2,18 @@ pragma solidity ^0.8.21; import { Test } from "forge-std/Test.sol"; -import { ECDSA } from "../src/dependencies/cryptography/ECDSA.sol"; -import { PredictiveDeployer } from "../src/PredictiveDeployer.sol"; - -import "forge-std/console.sol"; - -import { Test } from "forge-std/Test.sol"; +import { VmSafe } from "forge-std/Vm.sol"; import { PredictiveDeployer } from "../src/PredictiveDeployer.sol"; +import { ERC1967Proxy } from "../src/dependencies/proxy/ERC1967Proxy.sol"; import { Child } from "./Child.t.sol"; -import { VmSafe } from "forge-std/Vm.sol"; +import { IPredictiveDeployer } from "../src/interfaces/IPredictiveDeployer.sol"; +import { CONTRACT_DEPLOYER } from "./common/constants.t.sol"; -contract Create2FactoryTest is Test { - PredictiveDeployer public predictive_deployer; +contract PredictiveDeployerTest is Test { + /* solhint-disable func-name-mixedcase */ + + PredictiveDeployer public implementation; + ERC1967Proxy public proxy; Child public child; bytes public childBytecode; @@ -21,7 +21,9 @@ contract Create2FactoryTest is Test { event Deploy(address indexed sender, address indexed child, bytes32 hashedBytecode, uint256 nonce); function setUp() public { - predictive_deployer = new PredictiveDeployer(); + implementation = new PredictiveDeployer(); + vm.prank(CONTRACT_DEPLOYER); + proxy = new ERC1967Proxy(address(implementation), abi.encodeWithSignature("initialize()")); child = new Child(address(this)); // Get Bytecode @@ -29,62 +31,125 @@ contract Create2FactoryTest is Test { childBytecode = abi.encodePacked(bytecode, abi.encode(address(this))); } - // function test_getAddress_fuzz(uint256 pk_num, address sender) public { - // // Setup - // VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pk_num))))); + function testFuzz_GetAddress(uint256 pkNum, address sender) public { + // Setup + VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); + + uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr); - // uint256 currentNonce = predictive_deployer.userNonces(wallet.addr); + // Get signature information + bytes32 txHash = IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, currentNonce); - // // Get signature information - // bytes32 txHash = predictive_deployer.getTransactionHash(currentNonce); + bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash)); - // bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, messageHash); - // (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, messageHash); + bytes memory signature = abi.encodePacked(r, s, v); - // bytes memory signature = abi.encodePacked(r, s, v); + // Expectation + vm.startPrank(sender); + uint256 snapShot = vm.snapshot(); + address expectedChild = IPredictiveDeployer(address(proxy)).deploy(wallet.addr, signature, childBytecode); - // // Expectation - // vm.startPrank(sender); - // uint256 snapShot = vm.snapshot(); - // address expectedChild = predictive_deployer.deploy(messageHash, signature, childBytecode); + // Set chain state to what it was before the deployment + vm.revertTo(snapShot); - // // Set chain state to what it was before the deployment - // vm.revertTo(snapShot); + // Act + address actualChild = IPredictiveDeployer(address(proxy)).getAddress(wallet.addr, childBytecode); + vm.stopPrank(); - // // Act - // address actualChild = predictive_deployer.getAddress(messageHash, signature, childBytecode); - // vm.stopPrank(); + // Assertions + assertEq(actualChild, expectedChild); + } + + function testFuzz_Deploy(uint256 pkNum, address sender) public { + // Setup + VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); + + uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr); + + // Get signature information + bytes32 txHash = IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, currentNonce); - // // Assertions - // assertEq(actualChild, expectedChild); - // } + bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash)); - // function test_deploy_fuzz(uint256 pk_num, address sender) public { - // // Setup - // VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pk_num))))); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, messageHash); + bytes memory signature = abi.encodePacked(r, s, v); + + // Expectations + vm.startPrank(sender); + address expectedChild = IPredictiveDeployer(address(proxy)).getAddress(wallet.addr, childBytecode); + vm.expectEmit(true, true, true, true, address(proxy)); + emit Deploy(wallet.addr, expectedChild, keccak256(childBytecode), currentNonce); + + // Act + address actualChild = IPredictiveDeployer(address(proxy)).deploy(wallet.addr, signature, childBytecode); + vm.stopPrank(); + + // Assertions + assertEq(actualChild, expectedChild); + } - // uint256 currentNonce = predictive_deployer.userNonces(wallet.addr); + function test_CannotDeployReplay(uint256 pkNum) public { + // Setup + VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); - // // Get signature information - // bytes32 txHash = predictive_deployer.getTransactionHash(currentNonce); + uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr); - // bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash)); + // Get signature information + bytes32 txHash = IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, currentNonce); - // (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, messageHash); - // bytes memory signature = abi.encodePacked(r, s, v); + bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash)); - // // Expectations - // vm.startPrank(sender); - // address expectedChild = predictive_deployer.getAddress(messageHash, signature, childBytecode); - // vm.expectEmit(true, true, true, true, address(predictive_deployer)); - // emit Deploy(sender, expectedChild, keccak256(childBytecode), currentNonce); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, messageHash); + bytes memory signature = abi.encodePacked(r, s, v); - // // Act - // address actualChild = predictive_deployer.deploy(messageHash, signature, childBytecode); - // vm.stopPrank(); + // Deploy once + IPredictiveDeployer(address(proxy)).deploy(wallet.addr, signature, childBytecode); - // // Assertions - // assertEq(actualChild, expectedChild); - // } + // Act: attempt replay + vm.expectRevert(PredictiveDeployer.Unauthorized.selector); + IPredictiveDeployer(address(proxy)).deploy(wallet.addr, signature, childBytecode); + } + + function test_CannotDeployWithoutApproval(uint256 pkNum, address invalidPrincipal) public { + // Setup + VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); + + // Failing condition: principal is not the signer + vm.assume(invalidPrincipal != wallet.addr); + + uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr); + + // Get signature information + bytes32 txHash = IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, currentNonce); + + bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, messageHash); + bytes memory signature = abi.encodePacked(r, s, v); + + // Act: attempt with invalid principal + vm.expectRevert(PredictiveDeployer.Unauthorized.selector); + IPredictiveDeployer(address(proxy)).deploy(invalidPrincipal, signature, childBytecode); + } + + function test_Receive(uint256 transferAmount) public { + // Assumptions + vm.assume(transferAmount != 0 && transferAmount <= 1000 ether); + + // Setup: deal transferAmount plus enough for gas + uint256 gasMoney = 1 ether; + vm.deal(address(this), transferAmount + gasMoney); + + // Expectations + assertEq(address(proxy).balance, 0); + + // Act + (bool sent,) = payable(address(proxy)).call{ value: transferAmount }(""); + require(sent, "Failed to send Ether"); + + // Assumptions + assertEq(address(proxy).balance, transferAmount); + } } diff --git a/test/common/constants.t.sol b/test/common/constants.t.sol new file mode 100644 index 0000000..e508ede --- /dev/null +++ b/test/common/constants.t.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +address constant CONTRACT_DEPLOYER = 0x76bd253e7a0FB5896b4ACA4b9ef06E9ee2b74e8E; + +address constant TEST_ERC20_TOKEN = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; // USDC on Ethereum diff --git a/test/test_proxy/ERC1967Proxy.t.sol b/test/test_proxy/ERC1967Proxy.t.sol new file mode 100644 index 0000000..e7bae1d --- /dev/null +++ b/test/test_proxy/ERC1967Proxy.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +import { Test } from "forge-std/Test.sol"; +import { PredictiveDeployer } from "../../src/PredictiveDeployer.sol"; +import { ERC1967Proxy } from "../../src/dependencies/proxy/ERC1967Proxy.sol"; +import { IPredictiveDeployerAdmin } from "../../src/interfaces/IPredictiveDeployerAdmin.sol"; +import { CONTRACT_DEPLOYER } from "../common/constants.t.sol"; + +contract ERC1967ProxyTest is Test { + /* solhint-disable func-name-mixedcase */ + + // Test Contracts + PredictiveDeployer public implementation; + ERC1967Proxy public proxy; + + function setUp() public { + implementation = new PredictiveDeployer(); + vm.prank(CONTRACT_DEPLOYER); + proxy = new ERC1967Proxy(address(implementation), abi.encodeWithSignature("initialize()")); + } + + function test_upgradeToAndCall() public { + // Setup + bytes32 implentationStorageSlot = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + address implementationAddressV1 = address(uint160(uint256(vm.load(address(proxy), implentationStorageSlot)))); + + // Pre-action assertions + assertEq(implementationAddressV1, address(implementation)); + + // Act + PredictiveDeployer implementationV2 = new PredictiveDeployer(); + vm.prank(CONTRACT_DEPLOYER); + IPredictiveDeployerAdmin(address(proxy)).upgradeToAndCall(address(implementationV2), ""); + + // Post-action assertions + address implementationAddressV2 = address(uint160(uint256(vm.load(address(proxy), implentationStorageSlot)))); + assertEq(implementationAddressV2, address(implementationV2)); + } + + function testFail_UpgradeToAndCallUnauthorized(address invalidDeployer) public { + // Assumptions + vm.assume(invalidDeployer != CONTRACT_DEPLOYER); + + // Setup + PredictiveDeployer implementationV2 = new PredictiveDeployer(); + + // Act + vm.prank(invalidDeployer); + IPredictiveDeployerAdmin(address(proxy)).upgradeToAndCall(address(implementationV2), ""); + } +}