├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── README.md ├── foundry.toml ├── script └── Counter.s.sol ├── src ├── ERC4626-DittoETH │ ├── DittoVulnerableVault.sol │ └── README.md ├── ERC4626-v1 │ ├── README.md │ └── VulnerableVault.sol └── ERC721-v1 │ ├── MysteryMasks.sol │ └── README.md └── test ├── ERC4626-DittoETH └── ExploitDittoVault.t.sol ├── ERC4626-v1 └── ExploitERC4626v1.t.sol └── ERC721-v1 └── ExploitMysteryMasks.t.sol /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Block logo](https://imgur.com/EvzXMnq.png) 2 | 3 | ## Rareskills Inspired CTF Challenges 4 | 5 | **Test your whitehat skills in our range of challenges inspired from Rareskills educational content** 6 | 7 | ## Documentation 8 | 9 | [Uniswap V2 Book](https://www.rareskills.io/uniswap-v2-book) 10 | 11 | [Discount Fees & Timelocked Withdrawals – ERC4626 Vault Vulnerability Analysis](https://x.com/DegenShaker/status/1833110466039849376) 12 | 13 | [ERC721 Callback Solution, Samczsun's Dangers of Suprising Code](https://samczsun.com/the-dangers-of-surprising-code/) 14 | 15 | ## Challenges 16 | 17 | [Uniswap V2 Book Chapter 1: ERC4626 - Vulnerable Vault](https://github.com/BlockChomper/ctf-challenges/tree/master/src/ERC4626-v1) 18 | 19 | [DittoETH Vulnerable Vault](https://github.com/BlockChomper/ctf-challenges/tree/master/src/ERC4626-DittoETH) 20 | 21 | [ERC721 Callback - MysteryMasks Challenge](https://github.com/BlockChomper/ctf-challenges/tree/master/src/ERC721-v1) 22 | 23 | ## Setting up Challenge Environment 24 | 25 | ```jsx 26 | git clone https://github.com/BlockChomper/ctf-challenges.git 27 | ``` 28 | 29 | Ensure you have Foundry installed with `foundryup` and then build the repo after cloning from Github 30 | 31 | ```jsx 32 | forge build 33 | ``` 34 | 35 | - Select challenge from sub folder within SRC. 36 | - View associated readme.md for scenario details. 37 | - Analyse vulnerable contract. 38 | - Write exploit code within matching test file for vulnerable contract. 39 | 40 | Run exploit test to confirm successful completion of challenge 41 | ```jsx 42 | forge test --match-test testExploit 43 | ``` 44 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | remappings = [ 6 | "@openzeppelin/=lib/openzeppelin-contracts/contracts/", 7 | ] 8 | 9 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 10 | -------------------------------------------------------------------------------- /script/Counter.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import {Script, console} from "forge-std/Script.sol"; 5 | 6 | contract CounterScript is Script { 7 | function setUp() public {} 8 | 9 | function run() public { 10 | vm.broadcast(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ERC4626-DittoETH/DittoVulnerableVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | contract DittoVulnerableVault { 5 | mapping(address => uint256) public balances; 6 | uint256 public totalSupply; 7 | uint256 public constant DISCOUNT_THRESHOLD = 100 ether; 8 | uint256 public constant DISCOUNT_FEE_PERCENTAGE = 10; // 10% fee 9 | uint256 public initialTotalSupply; 10 | 11 | // Deposit tokens into the vault 12 | function deposit(uint256 amount) external { 13 | if (initialTotalSupply == 0) { 14 | initialTotalSupply = totalSupply + amount; 15 | } 16 | balances[msg.sender] += amount; 17 | totalSupply += amount; 18 | } 19 | 20 | // Withdraw tokens from the vault 21 | function withdraw(uint256 amount) external { 22 | require(balances[msg.sender] >= amount, "Insufficient balance"); 23 | balances[msg.sender] -= amount; 24 | totalSupply -= amount; 25 | } 26 | 27 | // Simulate a discounted trade 28 | function executeTrade(uint256 tradeAmount) external { 29 | require(tradeAmount <= DISCOUNT_THRESHOLD, "Trade too large"); 30 | 31 | // Calculate fee based on total supply, not just the trade amount 32 | uint256 fee = (totalSupply * DISCOUNT_FEE_PERCENTAGE) / 100; 33 | 34 | // Mint new tokens as fee and add to total supply 35 | totalSupply += fee; 36 | 37 | // Distribute fee proportionally to all users 38 | for (uint256 i = 0; i < 5; i++) { 39 | address user = address(uint160(i + 1)); // Simplified user addresses 40 | uint256 userShare = (fee * balances[user]) / totalSupply; 41 | balances[user] += userShare; 42 | } 43 | } 44 | 45 | function checkVictory(address user, uint256 initialBalance, uint256 withdrawnAmount) external view returns (bool) { 46 | require(initialTotalSupply > 0, "Initial total supply not set"); 47 | uint256 currentBalance = balances[user]; 48 | uint256 totalProfit = (currentBalance + withdrawnAmount) - initialBalance; 49 | uint256 profitPercentage = (totalProfit * 100) / initialTotalSupply; 50 | return profitPercentage > 25; // Victory if profit > 25% of initial total supply 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ERC4626-DittoETH/README.md: -------------------------------------------------------------------------------- 1 | # The DittoVulnerableVault Challenge 2 | 3 | Welcome, aspiring blockchain security expert! You've been tasked with auditing a new DeFi protocol called DittoVault. The protocol claims to offer users a way to deposit their tokens and earn rewards through a unique trading mechanism. However, there are whispers in the crypto community that something might be off with its implementation. 4 | 5 | ## Background 6 | 7 | DittoVault allows users to: 8 | 1. Deposit tokens into the vault 9 | 2. Execute trades with a special "discount" feature 10 | 3. Withdraw their tokens (hopefully with profits) 11 | 12 | The vault has been live for a week, and five early adopters (addresses 0x1 to 0x5) have each deposited 100 ether worth of tokens. 13 | 14 | ## Your Mission 15 | 16 | As a white hat hacker, your goal is to investigate the DittoVault smart contract and determine if there are any vulnerabilities that could be exploited. If you find a vulnerability, you need to demonstrate how it could be used to drain a significant portion of the vault's assets. 17 | 18 | ## Success Criteria 19 | 20 | You'll be considered successful if you can: 21 | 1. Identify the vulnerability in the contract 22 | 2. Develop an exploit that allows you to profit more than 25% of the initial total supply 23 | 24 | ## Tools at Your Disposal 25 | 26 | - The full source code of the DittoVault contract 27 | - A test environment where you can interact with the contract 28 | - Your knowledge of Solidity and DeFi vulnerabilities 29 | 30 | ## Ethical Considerations 31 | 32 | Remember, this is a controlled environment for educational purposes. In the real world, always disclose vulnerabilities responsibly and never exploit them for personal gain. 33 | 34 | ## Hint 35 | 36 | Pay close attention to how fees are calculated and distributed in the `executeTrade` function. There might be a way to manipulate this mechanism to your advantage. 37 | 38 | Good luck, and happy hacking! -------------------------------------------------------------------------------- /src/ERC4626-v1/README.md: -------------------------------------------------------------------------------- 1 | # The Vulnerable Vault Challenge 2 | 3 | ## Background 4 | A new decentralized finance (DeFi) protocol has emerged, promising high yields and secure asset management. At the heart of this protocol is the VulnerableVault, an implementation of the ERC-4626 Tokenized Vault Standard. The vault has quickly gained popularity, with many users eagerly depositing their hard-earned tokens. 5 | 6 | ## Your Mission 7 | You are a skilled white hat hacker who has been secretly analyzing the VulnerableVault's smart contract. After days of scrutiny, you've discovered a critical vulnerability that could put all depositors' funds at risk. However, simply reporting the vulnerability might not be enough to convince the protocol's overconfident developers. 8 | 9 | Your mission, should you choose to accept it, is to ethically exploit the vulnerability to demonstrate its severity. Your goal is to drain a significant portion of the vault's funds, specifically targeting a high-profile victim who recently deposited 10,000 tokens. 10 | 11 | ## Objectives 12 | 1. Exploit the vulnerability in the VulnerableVault contract. 13 | 2. Player should have gained more than 100 tokens (the token has 18 decimals). 14 | 3. Vault should have lost more than 100 tokens. 15 | 4. Complete the exploit in a single transaction (within the `testExploit` function). 16 | 17 | ## Resources at Your Disposal 18 | - You start with 1,000 tokens in your wallet. 19 | - You have full access to the VulnerableVault's source code. 20 | - The vault currently holds 10,000 tokens deposited by the victim. 21 | 22 | ## Constraints 23 | - You must work within the confines of the Ethereum Virtual Machine (EVM). 24 | - You cannot modify the VulnerableVault contract itself. 25 | - Your exploit must be executed through interactions with the vault's public interface. 26 | - You may only use the `player` address in the test, you may not prank other addresses. 27 | 28 | ## Hints 29 | - Pay close attention to the implementation of the deposit and withdraw functions. 30 | - The relationship between assets and shares in ERC-4626 vaults is crucial. 31 | - Think about how you might manipulate the exchange rate between assets and shares. 32 | 33 | ## Ethical Considerations 34 | Remember, in a real-world scenario, exploiting vulnerabilities for personal gain is illegal and unethical. This challenge is a simulation designed to help you understand and prevent such vulnerabilities in the future. 35 | 36 | Good luck, hacker. users are counting on you to expose this vulnerability before real malicious actors can exploit it! 37 | -------------------------------------------------------------------------------- /src/ERC4626-v1/VulnerableVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7 | import "@openzeppelin/contracts/utils/math/Math.sol"; 8 | import "@openzeppelin/contracts/interfaces/IERC4626.sol"; 9 | 10 | contract VulnerableVault is ERC20, IERC4626 { 11 | using SafeERC20 for IERC20; 12 | using Math for uint256; 13 | 14 | IERC20 private immutable _asset; 15 | 16 | constructor(IERC20 assetToken) ERC20("Vulnerable Vault", "vVLT") { 17 | _asset = assetToken; 18 | } 19 | 20 | function asset() public view virtual override returns (address) { 21 | return address(_asset); 22 | } 23 | 24 | function deposit(uint256 assets, address receiver) public virtual override returns (uint256 shares) { 25 | uint256 supply = totalSupply(); 26 | shares = supply == 0 ? assets : (assets * supply) / totalAssets(); 27 | _mint(receiver, shares); 28 | _asset.safeTransferFrom(msg.sender, address(this), assets); 29 | emit Deposit(msg.sender, receiver, assets, shares); 30 | } 31 | 32 | function mint(uint256 shares, address receiver) public virtual override returns (uint256 assets) { 33 | if (msg.sender != owner) { 34 | uint256 maxAssets = convertToAssets(allowance(owner, msg.sender)); 35 | require(assets <= maxAssets, "Withdraw amount exceeds allowance"); 36 | _spendAllowance(owner, msg.sender, convertToShares(assets)); 37 | } 38 | 39 | assets = convertToAssets(shares); 40 | _mint(receiver, shares); 41 | _asset.safeTransferFrom(msg.sender, address(this), assets); 42 | emit Deposit(msg.sender, receiver, assets, shares); 43 | } 44 | 45 | function withdraw(uint256 assets, address receiver, address owner) public virtual override returns (uint256 shares) { 46 | if (msg.sender != owner) { 47 | uint256 maxAssets = convertToAssets(allowance(owner, msg.sender)); 48 | require(assets <= maxAssets, "Withdraw amount exceeds allowance"); 49 | _spendAllowance(owner, msg.sender, convertToShares(assets)); 50 | } 51 | 52 | shares = convertToShares(assets); 53 | require(shares <= balanceOf(owner), "Withdraw amount exceeds balance"); 54 | 55 | _burn(owner, shares); 56 | _asset.safeTransfer(receiver, assets); 57 | 58 | emit Withdraw(msg.sender, receiver, owner, assets, shares); 59 | return shares; 60 | } 61 | 62 | function redeem(uint256 shares, address receiver, address owner) public virtual override returns (uint256 assets) { 63 | if (msg.sender != owner) { 64 | _spendAllowance(owner, msg.sender, shares); 65 | } 66 | assets = convertToAssets(shares); 67 | _burn(owner, shares); 68 | _asset.safeTransfer(receiver, assets); 69 | emit Withdraw(msg.sender, receiver, owner, assets, shares); 70 | } 71 | 72 | function totalAssets() public view virtual override returns (uint256) { 73 | return _asset.balanceOf(address(this)); 74 | } 75 | 76 | function convertToShares(uint256 assets) public view virtual override returns (uint256) { 77 | uint256 supply = totalSupply(); 78 | return supply == 0 ? assets : assets.mulDiv(supply, totalAssets(), Math.Rounding.Floor); 79 | } 80 | 81 | function donateAssets(uint256 amount) public { 82 | _asset.safeTransferFrom(msg.sender, address(this), amount); 83 | } 84 | 85 | function convertToAssets(uint256 shares) public view virtual override returns (uint256) { 86 | uint256 supply = totalSupply(); 87 | if (supply == 0) { 88 | return shares; 89 | } 90 | return (shares * totalAssets() * 100) / supply; 91 | } 92 | 93 | function previewDeposit(uint256 assets) public view virtual override returns (uint256) { 94 | return convertToShares(assets); 95 | } 96 | 97 | function previewMint(uint256 shares) public view virtual override returns (uint256) { 98 | return convertToAssets(shares); 99 | } 100 | 101 | function previewWithdraw(uint256 assets) public view virtual override returns (uint256) { 102 | return convertToShares(assets); 103 | } 104 | 105 | function previewRedeem(uint256 shares) public view virtual override returns (uint256) { 106 | return convertToAssets(shares); 107 | } 108 | 109 | function maxDeposit(address) public view virtual override returns (uint256) { 110 | return type(uint256).max; 111 | } 112 | 113 | function maxMint(address) public view virtual override returns (uint256) { 114 | return type(uint256).max; 115 | } 116 | 117 | function maxWithdraw(address owner) public view virtual override returns (uint256) { 118 | return convertToAssets(balanceOf(owner)); 119 | } 120 | 121 | function maxRedeem(address owner) public view virtual override returns (uint256) { 122 | return balanceOf(owner); 123 | } 124 | } 125 | 126 | 127 | -------------------------------------------------------------------------------- /src/ERC721-v1/MysteryMasks.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | 7 | contract MysteryMasks is ERC721, Ownable { 8 | 9 | uint256 private _nextTokenId; 10 | 11 | uint256 public constant MINT_PRICE = 0.1 ether; 12 | uint256 public constant MAX_NFT_SUPPLY = 1000; 13 | uint256 public constant MAX_MINT_PER_TX = 20; 14 | 15 | bool public mintingActive = true; 16 | mapping(uint256 => bool) public hasSpecialPower; 17 | 18 | constructor() ERC721("MysteryMasks", "MASK") Ownable(msg.sender) {} 19 | 20 | function mintMask(uint256 numberOfMasks) external payable { 21 | require(mintingActive, "Minting is not active"); 22 | require(numberOfMasks > 0 && numberOfMasks <= MAX_MINT_PER_TX, "Invalid number of masks"); 23 | require(msg.value == MINT_PRICE * numberOfMasks, "Incorrect payment amount"); 24 | require(_nextTokenId + numberOfMasks <= MAX_NFT_SUPPLY, "Would exceed max supply"); 25 | 26 | for(uint256 i = 0; i < numberOfMasks; i++) { 27 | _nextTokenId++; // Increment first 28 | uint256 newTokenId = _nextTokenId; 29 | 30 | // Randomly assign special powers 31 | if(block.timestamp % 2 == 0) { 32 | hasSpecialPower[newTokenId] = true; 33 | } 34 | 35 | _safeMint(msg.sender, newTokenId); 36 | } 37 | } 38 | 39 | function withdrawFunds() external onlyOwner { 40 | payable(owner()).transfer(address(this).balance); 41 | } 42 | 43 | function toggleMinting() external onlyOwner { 44 | mintingActive = !mintingActive; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ERC721-v1/README.md: -------------------------------------------------------------------------------- 1 | # The MysteryMasks Challenge 2 | 3 | Welcome, blockchain security enthusiast! You've discovered a new NFT collection called MysteryMasks that's generating significant buzz in the crypto community. The project boasts "secure-by-design" mechanics using industry-standard OpenZeppelin contracts, but something about their minting implementation has caught your attention. 4 | 5 | ## Background 6 | 7 | MysteryMasks offers: 8 | 9 | 1. A collection of unique NFT masks with random special powers 10 | 2. A fair minting system with a max of 20 masks per transaction 11 | 3. Each mask costs 0.1 ETH to mint 12 | 4. The collection has a hard cap of 1000 masks, and early minters have reported that masks minted during even-numbered timestamps have special powers. 13 | 14 | ## Your Mission 15 | 16 | As a smart contract security researcher, your goal is to analyze the MysteryMasks contract for potential vulnerabilities. Initial code review suggests there might be a way to circumvent the transaction limits that are meant to ensure fair distribution. 17 | 18 | ## Success Criteria 19 | 20 | You'll be considered successful if you can: 21 | 1. Finding a way to mint more than the maximum allowed masks (20) in a single transaction 22 | 2. Creating a working exploit contract that demonstrates the vulnerability 23 | 3. Successfully executing your exploit against the test contract 24 | 25 | ## Ethical Considerations 26 | 27 | Remember, this is a controlled environment for educational purposes. In the real world, always disclose vulnerabilities responsibly and never exploit them for personal gain. 28 | 29 | ## Hint 30 | 31 | "Safe" functions often make external calls. What happens during these calls, and how might this affect the contract's state management? 32 | 33 | Good luck, and remember: understanding vulnerabilities is the first step to building more secure contracts! 34 | -------------------------------------------------------------------------------- /test/ERC4626-DittoETH/ExploitDittoVault.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../../src/ERC4626-DittoETH/DittoVulnerableVault.sol"; 6 | 7 | contract ExploitTest is Test { 8 | DittoVulnerableVault public vault; 9 | address public attacker = address(0x1337); 10 | address[4] public otherUsers; 11 | 12 | function setUp() public { 13 | // Deploy the VulnerableVault contract 14 | vault = new DittoVulnerableVault(); 15 | 16 | // Setup other users 17 | otherUsers[0] = address(0x1); 18 | otherUsers[1] = address(0x2); 19 | otherUsers[2] = address(0x3); 20 | otherUsers[3] = address(0x4); 21 | 22 | // Other users deposit funds 23 | for (uint256 i = 0; i < 4; i++) { 24 | vm.prank(otherUsers[i]); 25 | vault.deposit(100 ether); 26 | } 27 | } 28 | 29 | function testExploit() public { 30 | uint256 initialBalance = vault.balances(attacker); 31 | uint256 initialTotalSupply = vault.totalSupply(); 32 | 33 | vm.startPrank(attacker); 34 | 35 | // TODO: CODE YOUR EXPLOIT HERE 36 | // HINT: Pay close attention to how fees are calculated and distributed. 37 | // You may want to use a combination of deposits, trades, and withdrawals. 38 | 39 | // Example (this won't solve the challenge, you need to write the exploit): 40 | // vault.deposit(500 ether); 41 | // for (uint i = 0; i < 10; i++) { 42 | // vault.executeTrade(1 ether); 43 | // } 44 | // uint256 attackerBalance = vault.balances(attacker); 45 | // vault.withdraw(attackerBalance); 46 | 47 | vm.stopPrank(); 48 | 49 | // Retrieve the final balance after the exploit 50 | uint256 finalBalance = vault.balances(attacker); 51 | uint256 withdrawnAmount = finalBalance > initialBalance ? finalBalance - initialBalance : 0; 52 | 53 | // Victory check 54 | assertTrue(vault.checkVictory(attacker, initialBalance, withdrawnAmount)); 55 | 56 | // Print the results 57 | uint256 profit = withdrawnAmount > 500 ether ? withdrawnAmount - 500 ether : 0; 58 | console.log("Initial total supply:", initialTotalSupply); 59 | console.log("Attacker's withdrawn amount:", withdrawnAmount); 60 | console.log("Profit:", profit); 61 | console.log("Profit percentage of initial total supply:", (profit * 100) / initialTotalSupply, "%"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/ERC4626-v1/ExploitERC4626v1.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../../src/ERC4626-v1/VulnerableVault.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | 8 | contract MockERC20 is ERC20 { 9 | constructor() ERC20("Mock Token", "MCK") { 10 | _mint(msg.sender, 1000000 * 10**decimals()); 11 | } 12 | } 13 | 14 | contract VaultExploit is Test { 15 | VulnerableVault public vault; 16 | MockERC20 public token; 17 | address public player; 18 | address public victim; 19 | 20 | function setUp() public { 21 | token = new MockERC20(); 22 | vault = new VulnerableVault(IERC20(address(token))); 23 | 24 | player = makeAddr("player"); 25 | victim = makeAddr("victim"); 26 | 27 | // Give some tokens to the player and victim 28 | token.transfer(player, 1000 * 10**token.decimals()); 29 | token.transfer(victim, 10000 * 10**token.decimals()); 30 | 31 | // Victim deposits into the vault 32 | vm.startPrank(victim); 33 | token.approve(address(vault), type(uint256).max); 34 | vault.deposit(10000 * 10**token.decimals(), victim); 35 | vm.stopPrank(); 36 | } 37 | 38 | function testExploit() public { 39 | vm.startPrank(player); 40 | 41 | // TODO: Exploit the vault here 42 | // Hint: The vulnerability is in the deposit and withdraw functions 43 | // Your goal is to drain the victim's funds from the vault 44 | 45 | vm.stopPrank(); 46 | 47 | // Check if the exploit was successful 48 | assertGt(token.balanceOf(player), 10000 * 10**token.decimals(), "Player should have more than 10000 tokens"); 49 | assertLt(token.balanceOf(address(vault)), 1000 * 10**token.decimals(), "Vault should have less than 1000 tokens left"); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/ERC721-v1/ExploitMysteryMasks.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../../src/ERC721-v1/MysteryMasks.sol"; 6 | import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 7 | 8 | contract ExploitContract is IERC721Receiver { 9 | MysteryMasks public mysteryMasks; 10 | uint256 public attackCount; 11 | bool public isExploiting; 12 | 13 | constructor(address _mysteryMasks) { 14 | mysteryMasks = MysteryMasks(_mysteryMasks); 15 | } 16 | 17 | function exploit() external payable { 18 | // Your exploit code here 19 | } 20 | 21 | function onERC721Received( 22 | address, 23 | address, 24 | uint256, 25 | bytes calldata 26 | ) external override returns (bytes4) { 27 | // Your callback exploit code here 28 | return IERC721Receiver.onERC721Received.selector; 29 | } 30 | 31 | receive() external payable {} 32 | } 33 | 34 | contract MysteryMasksCTF is Test { 35 | MysteryMasks public mysteryMasks; 36 | ExploitContract public exploitContract; 37 | address public player = address(0x1337); 38 | 39 | function setUp() public { 40 | mysteryMasks = new MysteryMasks(); 41 | exploitContract = new ExploitContract(address(mysteryMasks)); 42 | vm.deal(player, 100 ether); 43 | } 44 | 45 | function testExploit() public { 46 | vm.startPrank(player); 47 | 48 | uint256 initialBalance = mysteryMasks.balanceOf(address(exploitContract)); 49 | 50 | uint256 mintCost = mysteryMasks.MINT_PRICE() * 40; 51 | exploitContract.exploit{value: mintCost}(); 52 | 53 | uint256 finalBalance = mysteryMasks.balanceOf(address(exploitContract)); 54 | uint256 tokensMinted = finalBalance - initialBalance; 55 | 56 | console.log("Tokens minted:", tokensMinted); 57 | assertGt(tokensMinted, mysteryMasks.MAX_MINT_PER_TX(), "Challenge not solved - didn't mint enough tokens"); 58 | 59 | vm.stopPrank(); 60 | } 61 | } --------------------------------------------------------------------------------