diff --git a/.gitmodules b/.gitmodules index 4c4ae08..8e8dfc1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +1,6 @@ -[submodule "lib/openzeppelin-contracts"] - path = lib/openzeppelin-contracts +[submodule "foundry/lib/openzeppelin-contracts-v4"] + path = foundry/lib/openzeppelin-contracts-v4 url = https://github.com/OpenZeppelin/openzeppelin-contracts -[submodule "lib/forge-std"] - path = lib/forge-std - url = https://github.com/foundry-rs/forge-std -[submodule "lib/prb-test"] - path = lib/prb-test - url = https://github.com/PaulRBerg/prb-test [submodule "foundry/lib/openzeppelin-contracts"] path = foundry/lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts @@ -40,3 +34,15 @@ [submodule "foundry/lib/solady"] path = foundry/lib/solady url = https://github.com/vectorized/solady +[submodule "foundry/lib/safe-contracts"] + path = foundry/lib/safe-contracts + url = https://github.com/safe-global/safe-contracts +[submodule "foundry/lib/openzeppelin-contracts-upgradeable"] + path = foundry/lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "foundry/lib/openzeppelin-contracts-upgradeable-v4.7.1"] + path = foundry/lib/openzeppelin-contracts-upgradeable-v4.7.1 + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "foundry/lib/openzeppelin-contracts-v4.7.1"] + path = foundry/lib/openzeppelin-contracts-v4.7.1 + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/README.md b/README.md index 4dedebf..49fb7d2 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ git submodule update --init --recursive # OR forge install foundry-rs/forge-std@v1.7.1 --no-commit -forge install OpenZeppelin/openzeppelin-contracts@v5.0.0 --no-commit + forge install transmissions11/solmate@0384dbaaa4fcb5715738a9254a7c0a4cb62cf458 --no-commit forge install vectorized/solady@v0.0.124 --no-commit @@ -27,6 +27,24 @@ forge install Uniswap/v3-core --no-commit forge install Uniswap/v4-periphery --no-commit forge install Uniswap/v4-core --no-commit +forge install OpenZeppelin/openzeppelin-contracts@v5.0.0 --no-commit +forge install OpenZeppelin/openzeppelin-contracts-upgradeable@v5.0.0 --no-commit + +# OpenZeppelin v4 +v4.7.1 + +git clone https://github.com/OpenZeppelin/openzeppelin-contracts foundry/lib/openzeppelin-contracts-v4.7.1 && cd foundry/lib/openzeppelin-contracts-v4.7.1 && git checkout tags/v4.7.1 + +git clone https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable foundry/lib/openzeppelin-contracts-upgradeable-v4.7.1 && cd foundry/lib/openzeppelin-contracts-upgradeable-v4.7.1 && git checkout tags/v4.7.1 + +git rm --cached foundry/lib/openzeppelin-contracts-v4.7.1 + +git submodule add https://github.com/OpenZeppelin/openzeppelin-contracts foundry/lib/openzeppelin-contracts-v4.7.1 + +git submodule add https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable foundry/lib/openzeppelin-contracts-upgradeable-v4.7.1 + +forge install safe-global/safe-contracts@v1.3.0 --no-commit + ``` ```bash diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableNFT.sol b/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableNFT.sol new file mode 100644 index 0000000..def9374 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableNFT.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-v4.7.1/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts-v4.7.1/token/ERC721/extensions/ERC721Burnable.sol"; +import "@solady/auth/OwnableRoles.sol"; + +/** + * @title DamnValuableNFT + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + * @notice Implementation of a mintable and burnable NFT with role-based access controls + */ +contract DamnValuableNFT is ERC721, ERC721Burnable, OwnableRoles { + uint256 public constant MINTER_ROLE = _ROLE_0; + uint256 public tokenIdCounter; + + constructor() ERC721("DamnValuableNFT", "DVNFT") { + _initializeOwner(msg.sender); + _grantRoles(msg.sender, MINTER_ROLE); + } + + function safeMint(address to) public onlyRoles(MINTER_ROLE) returns (uint256 tokenId) { + tokenId = tokenIdCounter; + _safeMint(to, tokenId); + ++tokenIdCounter; + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableTokenSnapshot.sol b/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableTokenSnapshot.sol new file mode 100644 index 0000000..5b1c91a --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableTokenSnapshot.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-v4.7.1/token/ERC20/extensions/ERC20Snapshot.sol"; + +/** + * @title DamnValuableTokenSnapshot + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract DamnValuableTokenSnapshot is ERC20Snapshot { + uint256 private _lastSnapshotId; + + constructor(uint256 initialSupply) ERC20("DamnValuableToken", "DVT") { + _mint(msg.sender, initialSupply); + } + + // @audit-issue no access control, anyone can take a snapshot + function snapshot() public returns (uint256 lastSnapshotId) { + lastSnapshotId = _snapshot(); + _lastSnapshotId = lastSnapshotId; + } + + function getBalanceAtLastSnapshot(address account) external view returns (uint256) { + return balanceOfAt(account, _lastSnapshotId); + } + + function getTotalSupplyAtLastSnapshot() external view returns (uint256) { + return totalSupplyAt(_lastSnapshotId); + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable/ReceiverUnstoppable.sol b/contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable/ReceiverUnstoppable.sol index 4327fdd..fd360d4 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable/ReceiverUnstoppable.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable/ReceiverUnstoppable.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; import "@solmate/auth/Owned.sol"; -import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol"; +import "@openzeppelin/contracts-v4.7.1/interfaces/IERC3156FlashBorrower.sol"; import { UnstoppableVault, ERC20 } from "./UnstoppableVault.sol"; /** diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable/UnstoppableVault.sol b/contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable/UnstoppableVault.sol index 59c7447..b594a14 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable/UnstoppableVault.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable/UnstoppableVault.sol @@ -5,7 +5,7 @@ import { Owned } from "@solmate/auth/Owned.sol"; import { FixedPointMathLib } from "@solmate/utils/FixedPointMathLib.sol"; import { ReentrancyGuard } from "@solmate/utils/ReentrancyGuard.sol"; import { SafeTransferLib, ERC4626, ERC20 } from "@solmate/mixins/ERC4626.sol"; -import { IERC3156FlashBorrower, IERC3156FlashLender } from "@openzeppelin/contracts/interfaces/IERC3156.sol"; +import { IERC3156FlashBorrower, IERC3156FlashLender } from "@openzeppelin/contracts-v4.7.1/interfaces/IERC3156.sol"; /** * @title UnstoppableVault diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/FlashLoanReceiver.sol b/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/FlashLoanReceiver.sol index 54420a1..ce3f212 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/FlashLoanReceiver.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/FlashLoanReceiver.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.0; -import { SafeTransferLib } from "solady/src/utils/SafeTransferLib.sol"; -import { IERC3156FlashBorrower } from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol"; +import { SafeTransferLib } from "@solady/utils/SafeTransferLib.sol"; +import { IERC3156FlashBorrower } from "@openzeppelin/contracts-v4.7.1/interfaces/IERC3156FlashBorrower.sol"; import { NaiveReceiverLenderPool } from "./NaiveReceiverLenderPool.sol"; /** diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/NaiveReceiverLenderPool.sol b/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/NaiveReceiverLenderPool.sol index 7c1ec66..10b42b1 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/NaiveReceiverLenderPool.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/NaiveReceiverLenderPool.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; -import { IERC3156FlashBorrower, IERC3156FlashLender } from "@openzeppelin/contracts/interfaces/IERC3156.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; +import { IERC3156FlashBorrower, IERC3156FlashLender } from "@openzeppelin/contracts-v4.7.1/interfaces/IERC3156.sol"; import { SafeTransferLib } from "@solady/utils/SafeTransferLib.sol"; import { FlashLoanReceiver } from "./FlashLoanReceiver.sol"; diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/03.Truster/TrusterLenderPool.sol b/contracts/CTF/Damn-Vulnerable-DeFi/03.Truster/TrusterLenderPool.sol new file mode 100644 index 0000000..a41a262 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/03.Truster/TrusterLenderPool.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-v4.7.1/utils/Address.sol"; +import "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; + +/** + * @title TrusterLenderPool + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract TrusterLenderPool is ReentrancyGuard { + using Address for address; + + DamnValuableToken public immutable token; + + error RepayFailed(); + + constructor(DamnValuableToken _token) { + token = _token; + } + + function flashLoan( + uint256 amount, + address borrower, + address target, + bytes calldata data + ) + external + nonReentrant + returns (bool) + { + uint256 balanceBefore = token.balanceOf(address(this)); + + token.transfer(borrower, amount); + + // @audit-issue Execute abitraty call to any contract on behald of the pool + target.functionCall(data); + + if (token.balanceOf(address(this)) < balanceBefore) { + revert RepayFailed(); + } + + return true; + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance/SideEntranceLenderPool.sol b/contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance/SideEntranceLenderPool.sol new file mode 100644 index 0000000..58d911a --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance/SideEntranceLenderPool.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@solady/utils/SafeTransferLib.sol"; + +interface IFlashLoanEtherReceiver { + function execute() external payable; +} + +/** + * @title SideEntranceLenderPool + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract SideEntranceLenderPool { + mapping(address => uint256) private balances; + + error RepayFailed(); + + event Deposit(address indexed who, uint256 amount); + event Withdraw(address indexed who, uint256 amount); + + function deposit() external payable { + unchecked { + balances[msg.sender] += msg.value; + } + emit Deposit(msg.sender, msg.value); + } + + function withdraw() external { + uint256 amount = balances[msg.sender]; + + delete balances[msg.sender]; + emit Withdraw(msg.sender, amount); + + SafeTransferLib.safeTransferETH(msg.sender, amount); + } + + function flashLoan(uint256 amount) external { + uint256 balanceBefore = address(this).balance; + + IFlashLoanEtherReceiver(msg.sender).execute{ value: amount }(); + + if (address(this).balance < balanceBefore) { + revert RepayFailed(); + } + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/AccountingToken.sol b/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/AccountingToken.sol new file mode 100644 index 0000000..c2b4972 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/AccountingToken.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-v4.7.1/token/ERC20/extensions/ERC20Snapshot.sol"; +import "@solady/auth/OwnableRoles.sol"; + +/** + * @title AccountingToken + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + * @notice A limited pseudo-ERC20 token to keep track of deposits and withdrawals + * with snapshotting capabilities. + */ +contract AccountingToken is ERC20Snapshot, OwnableRoles { + uint256 public constant MINTER_ROLE = _ROLE_0; + uint256 public constant SNAPSHOT_ROLE = _ROLE_1; + uint256 public constant BURNER_ROLE = _ROLE_2; + + error NotImplemented(); + + constructor() ERC20("rToken", "rTKN") { + _initializeOwner(msg.sender); + _grantRoles(msg.sender, MINTER_ROLE | SNAPSHOT_ROLE | BURNER_ROLE); + } + + function mint(address to, uint256 amount) external onlyRoles(MINTER_ROLE) { + _mint(to, amount); + } + + function burn(address from, uint256 amount) external onlyRoles(BURNER_ROLE) { + _burn(from, amount); + } + + function snapshot() external onlyRoles(SNAPSHOT_ROLE) returns (uint256) { + return _snapshot(); + } + + function _transfer(address, address, uint256) internal pure override { + revert NotImplemented(); + } + + function _approve(address, address, uint256) internal pure override { + revert NotImplemented(); + } +} + +/** + * @title RewardToken + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract RewardToken is ERC20, OwnableRoles { + uint256 public constant MINTER_ROLE = _ROLE_0; + + constructor() ERC20("Reward Token", "RWT") { + _initializeOwner(msg.sender); + _grantRoles(msg.sender, MINTER_ROLE); + } + + function mint(address to, uint256 amount) external onlyRoles(MINTER_ROLE) { + _mint(to, amount); + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/FlashLoanerPool.sol b/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/FlashLoanerPool.sol new file mode 100644 index 0000000..ec436d6 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/FlashLoanerPool.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts-v4.7.1/utils/Address.sol"; +import "../00.Base/DamnValuableToken.sol"; + +/** + * @title FlashLoanerPool + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + * @dev A simple pool to get flashloans of DVT + */ +contract FlashLoanerPool is ReentrancyGuard { + using Address for address; + + DamnValuableToken public immutable liquidityToken; + + error NotEnoughTokenBalance(); + error CallerIsNotContract(); + error FlashLoanNotPaidBack(); + + constructor(address liquidityTokenAddress) { + liquidityToken = DamnValuableToken(liquidityTokenAddress); + } + + function flashLoan(uint256 amount) external nonReentrant { + uint256 balanceBefore = liquidityToken.balanceOf(address(this)); + + if (amount > balanceBefore) { + revert NotEnoughTokenBalance(); + } + + // @audit-issue can be bypassed if we call it from a constructor + if (!msg.sender.isContract()) { + revert CallerIsNotContract(); + } + + liquidityToken.transfer(msg.sender, amount); + + msg.sender.functionCall(abi.encodeWithSignature("receiveFlashLoan(uint256)", amount)); + + if (liquidityToken.balanceOf(address(this)) < balanceBefore) { + revert FlashLoanNotPaidBack(); + } + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/RewardToken.sol b/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/RewardToken.sol new file mode 100644 index 0000000..72634af --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/RewardToken.sol @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/TheRewarderPool.sol b/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/TheRewarderPool.sol new file mode 100644 index 0000000..6e637bb --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/TheRewarderPool.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@solady/utils/FixedPointMathLib.sol"; +import "@solady/utils/SafeTransferLib.sol"; +import { AccountingToken, RewardToken } from "./AccountingToken.sol"; + +/** + * @title TheRewarderPool + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract TheRewarderPool { + using FixedPointMathLib for uint256; + + // Minimum duration of each round of rewards in seconds + uint256 private constant REWARDS_ROUND_MIN_DURATION = 5 days; + + uint256 public constant REWARDS = 100 ether; + + // Token deposited into the pool by users + address public immutable liquidityToken; + + // Token used for internal accounting and snapshots + // Pegged 1:1 with the liquidity token + AccountingToken public immutable accountingToken; + + // Token in which rewards are issued + RewardToken public immutable rewardToken; + + uint128 public lastSnapshotIdForRewards; + uint64 public lastRecordedSnapshotTimestamp; + uint64 public roundNumber; // Track number of rounds + mapping(address => uint64) public lastRewardTimestamps; + + error InvalidDepositAmount(); + + constructor(address _token) { + // Assuming all tokens have 18 decimals + liquidityToken = _token; + accountingToken = new AccountingToken(); + rewardToken = new RewardToken(); + + _recordSnapshot(); + } + + /** + * @notice Deposit `amount` liquidity tokens into the pool, minting accounting tokens in exchange. + * Also distributes rewards if available. + * @param amount amount of tokens to be deposited + */ + function deposit(uint256 amount) external { + if (amount == 0) { + revert InvalidDepositAmount(); + } + + accountingToken.mint(msg.sender, amount); + distributeRewards(); + + SafeTransferLib.safeTransferFrom(liquidityToken, msg.sender, address(this), amount); + } + + function withdraw(uint256 amount) external { + accountingToken.burn(msg.sender, amount); + SafeTransferLib.safeTransfer(liquidityToken, msg.sender, amount); + } + + function distributeRewards() public returns (uint256 rewards) { + if (isNewRewardsRound()) { + _recordSnapshot(); + } + + uint256 totalDeposits = accountingToken.totalSupplyAt(lastSnapshotIdForRewards); + uint256 amountDeposited = accountingToken.balanceOfAt(msg.sender, lastSnapshotIdForRewards); + + if (amountDeposited > 0 && totalDeposits > 0) { + // @audit-issue doesn't take into consideration deposited time + rewards = amountDeposited.mulDiv(REWARDS, totalDeposits); + if (rewards > 0 && !_hasRetrievedReward(msg.sender)) { + // @audit-issue no CEI + rewardToken.mint(msg.sender, rewards); + lastRewardTimestamps[msg.sender] = uint64(block.timestamp); + } + } + } + + function _recordSnapshot() private { + lastSnapshotIdForRewards = uint128(accountingToken.snapshot()); + lastRecordedSnapshotTimestamp = uint64(block.timestamp); + unchecked { + ++roundNumber; + } + } + + function _hasRetrievedReward(address account) private view returns (bool) { + return ( + lastRewardTimestamps[account] >= lastRecordedSnapshotTimestamp + && lastRewardTimestamps[account] <= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION + ); + } + + function isNewRewardsRound() public view returns (bool) { + return block.timestamp >= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION; + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/ISimpleGovernance.sol b/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/ISimpleGovernance.sol new file mode 100644 index 0000000..6cd2ab1 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/ISimpleGovernance.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface ISimpleGovernance { + struct GovernanceAction { + uint128 value; + uint64 proposedAt; + uint64 executedAt; + address target; + bytes data; + } + + error NotEnoughVotes(address who); + error CannotExecute(uint256 actionId); + error InvalidTarget(); + error TargetMustHaveCode(); + error ActionFailed(uint256 actionId); + + event ActionQueued(uint256 actionId, address indexed caller); + event ActionExecuted(uint256 actionId, address indexed caller); + + function queueAction(address target, uint128 value, bytes calldata data) external returns (uint256 actionId); + function executeAction(uint256 actionId) external payable returns (bytes memory returndata); + function getActionDelay() external view returns (uint256 delay); + function getGovernanceToken() external view returns (address token); + function getAction(uint256 actionId) external view returns (GovernanceAction memory action); + function getActionCounter() external view returns (uint256); +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/SelfiePool.sol b/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/SelfiePool.sol new file mode 100644 index 0000000..c60bf79 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/SelfiePool.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts-v4.7.1/token/ERC20/extensions/ERC20Snapshot.sol"; +import "@openzeppelin/contracts-v4.7.1/interfaces/IERC3156FlashLender.sol"; +import "@openzeppelin/contracts-v4.7.1/interfaces/IERC3156FlashBorrower.sol"; +import "./SimpleGovernance.sol"; + +/** + * @title SelfiePool + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract SelfiePool is ReentrancyGuard, IERC3156FlashLender { + ERC20Snapshot public immutable token; + SimpleGovernance public immutable governance; + bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); + + error RepayFailed(); + error CallerNotGovernance(); + error UnsupportedCurrency(); + error CallbackFailed(); + + event FundsDrained(address indexed receiver, uint256 amount); + + modifier onlyGovernance() { + if (msg.sender != address(governance)) { + revert CallerNotGovernance(); + } + _; + } + + constructor(address _token, address _governance) { + token = ERC20Snapshot(_token); + governance = SimpleGovernance(_governance); + } + + function maxFlashLoan(address _token) external view returns (uint256) { + if (address(token) == _token) { + return token.balanceOf(address(this)); + } + return 0; + } + + function flashFee(address _token, uint256) external view returns (uint256) { + if (address(token) != _token) { + revert UnsupportedCurrency(); + } + return 0; + } + + function flashLoan( + IERC3156FlashBorrower _receiver, + address _token, + uint256 _amount, + bytes calldata _data + ) + external + nonReentrant + returns (bool) + { + if (_token != address(token)) { + revert UnsupportedCurrency(); + } + + token.transfer(address(_receiver), _amount); + if (_receiver.onFlashLoan(msg.sender, _token, _amount, 0, _data) != CALLBACK_SUCCESS) { + revert CallbackFailed(); + } + + if (!token.transferFrom(address(_receiver), address(this), _amount)) { + revert RepayFailed(); + } + + return true; + } + + function emergencyExit(address receiver) external onlyGovernance { + uint256 amount = token.balanceOf(address(this)); + token.transfer(receiver, amount); + + emit FundsDrained(receiver, amount); + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/SimpleGovernance.sol b/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/SimpleGovernance.sol new file mode 100644 index 0000000..494de00 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/SimpleGovernance.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../00.Base/DamnValuableTokenSnapshot.sol"; +import "./ISimpleGovernance.sol"; + +/** + * @title SimpleGovernance + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ + +contract SimpleGovernance is ISimpleGovernance { + uint256 private constant ACTION_DELAY_IN_SECONDS = 2 days; + DamnValuableTokenSnapshot private _governanceToken; + uint256 private _actionCounter; + mapping(uint256 => GovernanceAction) private _actions; + + constructor(address governanceToken) { + _governanceToken = DamnValuableTokenSnapshot(governanceToken); + _actionCounter = 1; + } + + function queueAction(address target, uint128 value, bytes calldata data) external returns (uint256 actionId) { + if (!_hasEnoughVotes(msg.sender)) { + revert NotEnoughVotes(msg.sender); + } + + if (target == address(this)) { + revert InvalidTarget(); + } + + if (data.length > 0 && target.code.length == 0) { + revert TargetMustHaveCode(); + } + + actionId = _actionCounter; + + _actions[actionId] = GovernanceAction({ + target: target, + value: value, + proposedAt: uint64(block.timestamp), + executedAt: 0, + data: data + }); + + unchecked { + _actionCounter++; + } + + emit ActionQueued(actionId, msg.sender); + } + + function executeAction(uint256 actionId) external payable returns (bytes memory) { + if (!_canBeExecuted(actionId)) { + revert CannotExecute(actionId); + } + + GovernanceAction storage actionToExecute = _actions[actionId]; + actionToExecute.executedAt = uint64(block.timestamp); + + emit ActionExecuted(actionId, msg.sender); + + (bool success, bytes memory returndata) = + actionToExecute.target.call{ value: actionToExecute.value }(actionToExecute.data); + if (!success) { + if (returndata.length > 0) { + assembly { + revert(add(0x20, returndata), mload(returndata)) + } + } else { + revert ActionFailed(actionId); + } + } + + return returndata; + } + + function getActionDelay() external pure returns (uint256) { + return ACTION_DELAY_IN_SECONDS; + } + + function getGovernanceToken() external view returns (address) { + return address(_governanceToken); + } + + function getAction(uint256 actionId) external view returns (GovernanceAction memory) { + return _actions[actionId]; + } + + function getActionCounter() external view returns (uint256) { + return _actionCounter; + } + + /** + * @dev an action can only be executed if: + * 1) it's never been executed before and + * 2) enough time has passed since it was first proposed + */ + function _canBeExecuted(uint256 actionId) private view returns (bool) { + GovernanceAction memory actionToExecute = _actions[actionId]; + + if ( + actionToExecute.proposedAt == 0 // early exit + ) { + return false; + } + + uint64 timeDelta; + unchecked { + timeDelta = uint64(block.timestamp) - actionToExecute.proposedAt; + } + + return actionToExecute.executedAt == 0 && timeDelta >= ACTION_DELAY_IN_SECONDS; + } + + function _hasEnoughVotes(address who) private view returns (bool) { + uint256 balance = _governanceToken.getBalanceAtLastSnapshot(who); + uint256 halfTotalSupply = _governanceToken.getTotalSupplyAtLastSnapshot() / 2; + return balance > halfTotalSupply; + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/07.Compromised/Exchange.sol b/contracts/CTF/Damn-Vulnerable-DeFi/07.Compromised/Exchange.sol new file mode 100644 index 0000000..28c7819 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/07.Compromised/Exchange.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-v4.7.1/utils/Address.sol"; +import "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; +import "./TrustfulOracle.sol"; +import "../00.Base/DamnValuableNFT.sol"; + +/** + * @title Exchange + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract Exchange is ReentrancyGuard { + using Address for address payable; + + DamnValuableNFT public immutable token; + TrustfulOracle public immutable oracle; + + error InvalidPayment(); + error SellerNotOwner(uint256 id); + error TransferNotApproved(); + error NotEnoughFunds(); + + event TokenBought(address indexed buyer, uint256 tokenId, uint256 price); + event TokenSold(address indexed seller, uint256 tokenId, uint256 price); + + constructor(address _oracle) payable { + token = new DamnValuableNFT(); + token.renounceOwnership(); + oracle = TrustfulOracle(_oracle); + } + + function buyOne() external payable nonReentrant returns (uint256 id) { + if (msg.value == 0) { + revert InvalidPayment(); + } + + // Price should be in [wei / NFT] + uint256 price = oracle.getMedianPrice(token.symbol()); + + if (msg.value < price) { + revert InvalidPayment(); + } + + id = token.safeMint(msg.sender); + unchecked { + payable(msg.sender).sendValue(msg.value - price); + } + + emit TokenBought(msg.sender, id, price); + } + + function sellOne(uint256 id) external nonReentrant { + if (msg.sender != token.ownerOf(id)) { + revert SellerNotOwner(id); + } + + if (token.getApproved(id) != address(this)) { + revert TransferNotApproved(); + } + + // Price should be in [wei / NFT] + uint256 price = oracle.getMedianPrice(token.symbol()); + if (address(this).balance < price) { + revert NotEnoughFunds(); + } + + token.transferFrom(msg.sender, address(this), id); + token.burn(id); + + payable(msg.sender).sendValue(price); + + emit TokenSold(msg.sender, id, price); + } + + receive() external payable { } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/07.Compromised/TrustfulOracle.sol b/contracts/CTF/Damn-Vulnerable-DeFi/07.Compromised/TrustfulOracle.sol new file mode 100644 index 0000000..d5b0326 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/07.Compromised/TrustfulOracle.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-v4.7.1/access/AccessControlEnumerable.sol"; +import "solady/src/utils/LibSort.sol"; + +/** + * @title TrustfulOracle + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + * @notice A price oracle with a number of trusted sources that individually report prices for symbols. + * The oracle's price for a given symbol is the median price of the symbol over all sources. + */ +contract TrustfulOracle is AccessControlEnumerable { + uint256 public constant MIN_SOURCES = 1; + bytes32 public constant TRUSTED_SOURCE_ROLE = keccak256("TRUSTED_SOURCE_ROLE"); + bytes32 public constant INITIALIZER_ROLE = keccak256("INITIALIZER_ROLE"); + + // Source address => (symbol => price) + mapping(address => mapping(string => uint256)) private _pricesBySource; + + error NotEnoughSources(); + + event UpdatedPrice(address indexed source, string indexed symbol, uint256 oldPrice, uint256 newPrice); + + constructor(address[] memory sources, bool enableInitialization) { + if (sources.length < MIN_SOURCES) { + revert NotEnoughSources(); + } + for (uint256 i = 0; i < sources.length;) { + unchecked { + _setupRole(TRUSTED_SOURCE_ROLE, sources[i]); + ++i; + } + } + if (enableInitialization) { + _setupRole(INITIALIZER_ROLE, msg.sender); + } + } + + // A handy utility allowing the deployer to setup initial prices (only once) + function setupInitialPrices( + address[] calldata sources, + string[] calldata symbols, + uint256[] calldata prices + ) + external + onlyRole(INITIALIZER_ROLE) + { + // Only allow one (symbol, price) per source + require(sources.length == symbols.length && symbols.length == prices.length); + for (uint256 i = 0; i < sources.length;) { + unchecked { + _setPrice(sources[i], symbols[i], prices[i]); + ++i; + } + } + renounceRole(INITIALIZER_ROLE, msg.sender); + } + + function postPrice(string calldata symbol, uint256 newPrice) external onlyRole(TRUSTED_SOURCE_ROLE) { + _setPrice(msg.sender, symbol, newPrice); + } + + function getMedianPrice(string calldata symbol) external view returns (uint256) { + return _computeMedianPrice(symbol); + } + + function getAllPricesForSymbol(string memory symbol) public view returns (uint256[] memory prices) { + uint256 numberOfSources = getRoleMemberCount(TRUSTED_SOURCE_ROLE); + prices = new uint256[](numberOfSources); + for (uint256 i = 0; i < numberOfSources;) { + address source = getRoleMember(TRUSTED_SOURCE_ROLE, i); + prices[i] = getPriceBySource(symbol, source); + unchecked { + ++i; + } + } + } + + function getPriceBySource(string memory symbol, address source) public view returns (uint256) { + return _pricesBySource[source][symbol]; + } + + function _setPrice(address source, string memory symbol, uint256 newPrice) private { + uint256 oldPrice = _pricesBySource[source][symbol]; + _pricesBySource[source][symbol] = newPrice; + emit UpdatedPrice(source, symbol, oldPrice, newPrice); + } + + function _computeMedianPrice(string memory symbol) private view returns (uint256) { + uint256[] memory prices = getAllPricesForSymbol(symbol); + LibSort.insertionSort(prices); + if (prices.length % 2 == 0) { + uint256 leftPrice = prices[(prices.length / 2) - 1]; + uint256 rightPrice = prices[prices.length / 2]; + return (leftPrice + rightPrice) / 2; + } else { + return prices[prices.length / 2]; + } + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/07.Compromised/TrustfulOracleInitializer.sol b/contracts/CTF/Damn-Vulnerable-DeFi/07.Compromised/TrustfulOracleInitializer.sol new file mode 100644 index 0000000..69a9511 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/07.Compromised/TrustfulOracleInitializer.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { TrustfulOracle } from "./TrustfulOracle.sol"; + +/** + * @title TrustfulOracleInitializer + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract TrustfulOracleInitializer { + event NewTrustfulOracle(address oracleAddress); + + TrustfulOracle public oracle; + + constructor(address[] memory sources, string[] memory symbols, uint256[] memory initialPrices) { + oracle = new TrustfulOracle(sources, true); + oracle.setupInitialPrices(sources, symbols, initialPrices); + emit NewTrustfulOracle(address(oracle)); + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/08.Puppet/PuppetPool.sol b/contracts/CTF/Damn-Vulnerable-DeFi/08.Puppet/PuppetPool.sol new file mode 100644 index 0000000..354094c --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/08.Puppet/PuppetPool.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts-v4.7.1/utils/Address.sol"; +import "../00.Base/DamnValuableToken.sol"; + +/** + * @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 uniswapPair; + 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); + uniswapPair = 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); + } + + 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 uniswapPair.balance * (10 ** 18) / token.balanceOf(uniswapPair); + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/09.Puppet-V2/PuppetV2Pool.sol b/contracts/CTF/Damn-Vulnerable-DeFi/09.Puppet-V2/PuppetV2Pool.sol new file mode 100644 index 0000000..fa0f162 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/09.Puppet-V2/PuppetV2Pool.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// import "@uniswap/v2-periphery/contracts/libraries/UniswapV2Library.sol"; +// import "@uniswap/v2-periphery/contracts/libraries/SafeMath.sol"; + +// 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); +// } + +// /** +// * @title PuppetV2Pool +// * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) +// */ +// contract PuppetV2Pool { +// using SafeMath for uint256; + +// 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 +// ) +// public +// { +// _weth = IERC20(wethAddress); +// _token = IERC20(tokenAddress); +// _uniswapPair = uniswapPairAddress; +// _uniswapFactory = uniswapFactoryAddress; +// } + +// /** +// * @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); +// } +// } diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider/FreeRiderNFTMarketplace.sol b/contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider/FreeRiderNFTMarketplace.sol new file mode 100644 index 0000000..57f8cd2 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider/FreeRiderNFTMarketplace.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-v4.7.1/utils/Address.sol"; +import "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; +import "../00.Base/DamnValuableNFT.sol"; + +/** + * @title FreeRiderNFTMarketplace + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract FreeRiderNFTMarketplace is ReentrancyGuard { + using Address for address payable; + + DamnValuableNFT public token; + uint256 public offersCount; + + // tokenId -> price + mapping(uint256 => uint256) private offers; + + event NFTOffered(address indexed offerer, uint256 tokenId, uint256 price); + event NFTBought(address indexed buyer, uint256 tokenId, uint256 price); + + error InvalidPricesAmount(); + error InvalidTokensAmount(); + error InvalidPrice(); + error CallerNotOwner(uint256 tokenId); + error InvalidApproval(); + error TokenNotOffered(uint256 tokenId); + error InsufficientPayment(); + + constructor(uint256 amount) payable { + DamnValuableNFT _token = new DamnValuableNFT(); + _token.renounceOwnership(); + for (uint256 i = 0; i < amount;) { + _token.safeMint(msg.sender); + unchecked { + ++i; + } + } + token = _token; + } + + function offerMany(uint256[] calldata tokenIds, uint256[] calldata prices) external nonReentrant { + uint256 amount = tokenIds.length; + if (amount == 0) { + revert InvalidTokensAmount(); + } + + if (amount != prices.length) { + revert InvalidPricesAmount(); + } + + for (uint256 i = 0; i < amount;) { + unchecked { + _offerOne(tokenIds[i], prices[i]); + ++i; + } + } + } + + function _offerOne(uint256 tokenId, uint256 price) private { + DamnValuableNFT _token = token; // gas savings + + if (price == 0) { + revert InvalidPrice(); + } + + if (msg.sender != _token.ownerOf(tokenId)) { + revert CallerNotOwner(tokenId); + } + + if (_token.getApproved(tokenId) != address(this) && !_token.isApprovedForAll(msg.sender, address(this))) { + revert InvalidApproval(); + } + + offers[tokenId] = price; + + assembly { + // gas savings + sstore(0x02, add(sload(0x02), 0x01)) + } + + emit NFTOffered(msg.sender, tokenId, price); + } + + function buyMany(uint256[] calldata tokenIds) external payable nonReentrant { + for (uint256 i = 0; i < tokenIds.length;) { + unchecked { + _buyOne(tokenIds[i]); + ++i; + } + } + } + + function _buyOne(uint256 tokenId) private { + uint256 priceToPay = offers[tokenId]; + if (priceToPay == 0) { + revert TokenNotOffered(tokenId); + } + + // @audit-issue I can purchase 6 NFTs while paying only for one + // @audit-info msg.value doesn't change even if we sent the ETH out + if (msg.value < priceToPay) { + revert InsufficientPayment(); + } + + --offersCount; + + // transfer from seller to buyer + DamnValuableNFT _token = token; // cache for gas savings + // @audit-issue Medium / High seller can revoke the token spending approval + _token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId); + + // @audit-issue Eth is being send to the buyer instead of the seller + // @audit-info This happens because we transfered the token before sending the ETH and changed it's ownership + // pay seller using cached token + payable(_token.ownerOf(tokenId)).sendValue(priceToPay); + + emit NFTBought(msg.sender, tokenId, priceToPay); + } + + receive() external payable { } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider/FreeRiderRecovery.sol b/contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider/FreeRiderRecovery.sol new file mode 100644 index 0000000..34d4331 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/10.Free-Rider/FreeRiderRecovery.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-v4.7.1/utils/Address.sol"; +import "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts-v4.7.1/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts-v4.7.1/token/ERC721/IERC721Receiver.sol"; + +/** + * @title FreeRiderRecovery + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract FreeRiderRecovery is ReentrancyGuard, IERC721Receiver { + using Address for address payable; + + uint256 private constant PRIZE = 45 ether; + address private immutable beneficiary; + IERC721 private immutable nft; + uint256 private received; + + error NotEnoughFunding(); + error CallerNotNFT(); + error OriginNotBeneficiary(); + error InvalidTokenID(uint256 tokenId); + error StillNotOwningToken(uint256 tokenId); + + constructor(address _beneficiary, address _nft) payable { + if (msg.value != PRIZE) { + revert NotEnoughFunding(); + } + beneficiary = _beneficiary; + nft = IERC721(_nft); + IERC721(_nft).setApprovalForAll(msg.sender, true); + } + + // Read https://eips.ethereum.org/EIPS/eip-721 for more info on this function + function onERC721Received( + address, + address, + uint256 _tokenId, + bytes memory _data + ) + external + override + nonReentrant + returns (bytes4) + { + if (msg.sender != address(nft)) { + revert CallerNotNFT(); + } + + if (tx.origin != beneficiary) { + revert OriginNotBeneficiary(); + } + + if (_tokenId > 5) { + revert InvalidTokenID(_tokenId); + } + + if (nft.ownerOf(_tokenId) != address(this)) { + revert StillNotOwningToken(_tokenId); + } + + if (++received == 6) { + address recipient = abi.decode(_data, (address)); + payable(recipient).sendValue(PRIZE); + } + + return IERC721Receiver.onERC721Received.selector; + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/11.Backdoor/WalletRegistry.sol b/contracts/CTF/Damn-Vulnerable-DeFi/11.Backdoor/WalletRegistry.sol new file mode 100644 index 0000000..19c5926 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/11.Backdoor/WalletRegistry.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@solady/auth/Ownable.sol"; +import "@solady/utils/SafeTransferLib.sol"; +import "@openzeppelin/contracts-v4.7.1/token/ERC20/IERC20.sol"; +import "@gnosis/safe-contracts/GnosisSafe.sol"; +import "@gnosis/safe-contracts/proxies/IProxyCreationCallback.sol"; + +/** + * @title WalletRegistry + * @notice A registry for Gnosis Safe wallets. + * When known beneficiaries deploy and register their wallets, the registry sends some Damn Valuable Tokens + * to the wallet. + * @dev The registry has embedded verifications to ensure only legitimate Gnosis Safe wallets are stored. + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract WalletRegistry is IProxyCreationCallback, Ownable { + uint256 private constant EXPECTED_OWNERS_COUNT = 1; + uint256 private constant EXPECTED_THRESHOLD = 1; + uint256 private constant PAYMENT_AMOUNT = 10 ether; + + address public immutable masterCopy; + address public immutable walletFactory; + IERC20 public immutable token; + + mapping(address => bool) public beneficiaries; + + // owner => wallet + mapping(address => address) public wallets; + + error NotEnoughFunds(); + error CallerNotFactory(); + error FakeMasterCopy(); + error InvalidInitialization(); + error InvalidThreshold(uint256 threshold); + error InvalidOwnersCount(uint256 count); + error OwnerIsNotABeneficiary(); + error InvalidFallbackManager(address fallbackManager); + + constructor( + address masterCopyAddress, + address walletFactoryAddress, + address tokenAddress, + address[] memory initialBeneficiaries + ) { + _initializeOwner(msg.sender); + + masterCopy = masterCopyAddress; + walletFactory = walletFactoryAddress; + token = IERC20(tokenAddress); + + for (uint256 i = 0; i < initialBeneficiaries.length;) { + unchecked { + beneficiaries[initialBeneficiaries[i]] = true; + ++i; + } + } + } + + function addBeneficiary(address beneficiary) external onlyOwner { + beneficiaries[beneficiary] = true; + } + + /** + * @notice Function executed when user creates a Gnosis Safe wallet via + * GnosisSafeProxyFactory::createProxyWithCallback + * setting the registry's address as the callback. + */ + function proxyCreated( + GnosisSafeProxy proxy, + address singleton, + bytes calldata initializer, + uint256 + ) + external + override + { + if (token.balanceOf(address(this)) < PAYMENT_AMOUNT) { + // fail early + revert NotEnoughFunds(); + } + + address payable walletAddress = payable(proxy); + + // Ensure correct factory and master copy + if (msg.sender != walletFactory) { + revert CallerNotFactory(); + } + + if (singleton != masterCopy) { + revert FakeMasterCopy(); + } + + // Ensure initial calldata was a call to `GnosisSafe::setup` + if (bytes4(initializer[:4]) != GnosisSafe.setup.selector) { + revert InvalidInitialization(); + } + + // Ensure wallet initialization is the expected + uint256 threshold = GnosisSafe(walletAddress).getThreshold(); + if (threshold != EXPECTED_THRESHOLD) { + revert InvalidThreshold(threshold); + } + + address[] memory owners = GnosisSafe(walletAddress).getOwners(); + if (owners.length != EXPECTED_OWNERS_COUNT) { + revert InvalidOwnersCount(owners.length); + } + + // Ensure the owner is a registered beneficiary + address walletOwner; + unchecked { + walletOwner = owners[0]; + } + if (!beneficiaries[walletOwner]) { + revert OwnerIsNotABeneficiary(); + } + + address fallbackManager = _getFallbackManager(walletAddress); + if (fallbackManager != address(0)) { + revert InvalidFallbackManager(fallbackManager); + } + + // Remove owner as beneficiary + beneficiaries[walletOwner] = false; + + // Register the wallet under the owner's address + wallets[walletOwner] = walletAddress; + + // Pay tokens to the newly created wallet + SafeTransferLib.safeTransfer(address(token), walletAddress, PAYMENT_AMOUNT); + } + + function _getFallbackManager(address payable wallet) private view returns (address) { + return abi.decode( + GnosisSafe(wallet).getStorageAt(uint256(keccak256("fallback_manager.handler.address")), 0x20), (address) + ); + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberConstants.sol b/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberConstants.sol new file mode 100644 index 0000000..075084e --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberConstants.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/* ########################## */ +/* ### TIMELOCK CONSTANTS ### */ +/* ########################## */ + +// keccak256("ADMIN_ROLE"); +bytes32 constant ADMIN_ROLE = 0xa49807205ce4d355092ef5a8a18f56e8913cf4a201fbe287825b095693c21775; + +// keccak256("PROPOSER_ROLE"); +bytes32 constant PROPOSER_ROLE = 0xb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1; + +uint256 constant MAX_TARGETS = 256; +uint256 constant MIN_TARGETS = 0; +uint256 constant MAX_DELAY = 14 days; + +/* ####################### */ +/* ### VAULT CONSTANTS ### */ +/* ####################### */ + +uint256 constant WITHDRAWAL_LIMIT = 1 ether; +uint256 constant WAITING_PERIOD = 15 days; diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberErrors.sol b/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberErrors.sol new file mode 100644 index 0000000..cc20f84 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberErrors.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +error CallerNotTimelock(); +error NewDelayAboveMax(); +error NotReadyForExecution(bytes32 operationId); +error InvalidTargetsCount(); +error InvalidDataElementsCount(); +error InvalidValuesCount(); +error OperationAlreadyKnown(bytes32 operationId); +error CallerNotSweeper(); +error InvalidWithdrawalAmount(); +error InvalidWithdrawalTime(); diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberTimelock.sol b/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberTimelock.sol new file mode 100644 index 0000000..b21065d --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberTimelock.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Address } from "@openzeppelin/contracts-v4.7.1/utils/Address.sol"; +import "./ClimberTimelockBase.sol"; +import { ADMIN_ROLE, PROPOSER_ROLE, MAX_TARGETS, MIN_TARGETS, MAX_DELAY } from "./ClimberConstants.sol"; +import { + InvalidTargetsCount, + InvalidDataElementsCount, + InvalidValuesCount, + OperationAlreadyKnown, + NotReadyForExecution, + CallerNotTimelock, + NewDelayAboveMax +} from "./ClimberErrors.sol"; + +import "@openzeppelin/contracts-v4.7.1/access/AccessControl.sol"; + +/** + * @title ClimberTimelockBase + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +abstract contract ClimberTimelockBase is AccessControl { + // Possible states for an operation in this timelock contract + enum OperationState { + Unknown, + Scheduled, + ReadyForExecution, + Executed + } + + // Operation data tracked in this contract + struct Operation { + uint64 readyAtTimestamp; // timestamp at which the operation will be ready for execution + bool known; // whether the operation is registered in the timelock + bool executed; // whether the operation has been executed + } + + // Operations are tracked by their bytes32 identifier + mapping(bytes32 => Operation) public operations; + + uint64 public delay; + + function getOperationState(bytes32 id) public view returns (OperationState state) { + Operation memory op = operations[id]; + + if (op.known) { + if (op.executed) { + state = OperationState.Executed; + } else if (block.timestamp < op.readyAtTimestamp) { + state = OperationState.Scheduled; + // @audit-info block.timestamp >= op.readyAtTimestamp + } else { + state = OperationState.ReadyForExecution; + } + } else { + state = OperationState.Unknown; + } + } + + function getOperationId( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata dataElements, + bytes32 salt + ) + public + pure + returns (bytes32) + { + return keccak256(abi.encode(targets, values, dataElements, salt)); + } + + receive() external payable { } +} + +/** + * @title ClimberTimelock + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract ClimberTimelock is ClimberTimelockBase { + using Address for address; + + /** + * @notice Initial setup for roles and timelock delay. + * @param admin address of the account that will hold the ADMIN_ROLE role + * @param proposer address of the account that will hold the PROPOSER_ROLE role + */ + constructor(address admin, address proposer) { + _setRoleAdmin(ADMIN_ROLE, ADMIN_ROLE); + _setRoleAdmin(PROPOSER_ROLE, ADMIN_ROLE); + _setupRole(ADMIN_ROLE, admin); + _setupRole(ADMIN_ROLE, address(this)); // self administration + _setupRole(PROPOSER_ROLE, proposer); + + delay = 1 hours; + } + + function schedule( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata dataElements, + bytes32 salt + ) + external + onlyRole(PROPOSER_ROLE) + { + if (targets.length == MIN_TARGETS || targets.length >= MAX_TARGETS) { + revert InvalidTargetsCount(); + } + + if (targets.length != values.length) { + revert InvalidValuesCount(); + } + + if (targets.length != dataElements.length) { + revert InvalidDataElementsCount(); + } + + bytes32 id = getOperationId(targets, values, dataElements, salt); + + if (getOperationState(id) != OperationState.Unknown) { + revert OperationAlreadyKnown(id); + } + + operations[id].readyAtTimestamp = uint64(block.timestamp) + delay; + operations[id].known = true; + } + + /** + * Anyone can execute what's been scheduled via `schedule` + */ + function execute( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata dataElements, + bytes32 salt + ) + external + payable + { + if (targets.length <= MIN_TARGETS) { + revert InvalidTargetsCount(); + } + + if (targets.length != values.length) { + revert InvalidValuesCount(); + } + + if (targets.length != dataElements.length) { + revert InvalidDataElementsCount(); + } + + bytes32 id = getOperationId(targets, values, dataElements, salt); + + for (uint8 i = 0; i < targets.length;) { + targets[i].functionCallWithValue(dataElements[i], values[i]); + unchecked { + ++i; + } + } + + if (getOperationState(id) != OperationState.ReadyForExecution) { + revert NotReadyForExecution(id); + } + + operations[id].executed = true; + } + + function updateDelay(uint64 newDelay) external { + if (msg.sender != address(this)) { + revert CallerNotTimelock(); + } + + if (newDelay > MAX_DELAY) { + revert NewDelayAboveMax(); + } + + delay = newDelay; + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberTimelockBase.sol b/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberTimelockBase.sol new file mode 100644 index 0000000..15a8c0d --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberTimelockBase.sol @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberVault.sol b/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberVault.sol new file mode 100644 index 0000000..70f8e4f --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberVault.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-upgradeable-v4.7.1/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable-v4.7.1/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable-v4.7.1/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-v4.7.1/token/ERC20/IERC20.sol"; +import "@solady/utils/SafeTransferLib.sol"; + +import "./ClimberTimelock.sol"; +import { WITHDRAWAL_LIMIT, WAITING_PERIOD } from "./ClimberConstants.sol"; +import { CallerNotSweeper, InvalidWithdrawalAmount, InvalidWithdrawalTime } from "./ClimberErrors.sol"; + +/** + * @title ClimberVault + * @dev To be deployed behind a proxy following the UUPS pattern. Upgrades are to be triggered by the owner. + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract ClimberVault is Initializable, OwnableUpgradeable, UUPSUpgradeable { + uint256 private _lastWithdrawalTimestamp; + address private _sweeper; + + modifier onlySweeper() { + if (msg.sender != _sweeper) { + revert CallerNotSweeper(); + } + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address admin, address proposer, address sweeper) external initializer { + // Initialize inheritance chain + __Ownable_init(); + __UUPSUpgradeable_init(); + + // Deploy timelock and transfer ownership to it + transferOwnership(address(new ClimberTimelock(admin, proposer))); + + _setSweeper(sweeper); + _updateLastWithdrawalTimestamp(block.timestamp); + } + + // Allows the owner to send a limited amount of tokens to a recipient every now and then + function withdraw(address token, address recipient, uint256 amount) external onlyOwner { + if (amount > WITHDRAWAL_LIMIT) { + revert InvalidWithdrawalAmount(); + } + + if (block.timestamp <= _lastWithdrawalTimestamp + WAITING_PERIOD) { + revert InvalidWithdrawalTime(); + } + + _updateLastWithdrawalTimestamp(block.timestamp); + + SafeTransferLib.safeTransfer(token, recipient, amount); + } + + // Allows trusted sweeper account to retrieve any tokens + function sweepFunds(address token) external onlySweeper { + SafeTransferLib.safeTransfer(token, _sweeper, IERC20(token).balanceOf(address(this))); + } + + function getSweeper() external view returns (address) { + return _sweeper; + } + + function _setSweeper(address newSweeper) private { + _sweeper = newSweeper; + } + + function getLastWithdrawalTimestamp() external view returns (uint256) { + return _lastWithdrawalTimestamp; + } + + function _updateLastWithdrawalTimestamp(uint256 timestamp) private { + _lastWithdrawalTimestamp = timestamp; + } + + // By marking this internal function with `onlyOwner`, we only allow the owner account to authorize an upgrade + function _authorizeUpgrade(address newImplementation) internal override onlyOwner { } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining/AuthorizerUpgradeable.sol b/contracts/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining/AuthorizerUpgradeable.sol new file mode 100644 index 0000000..9b09f7f --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining/AuthorizerUpgradeable.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts-upgradeable-v4.7.1/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable-v4.7.1/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable-v4.7.1/access/OwnableUpgradeable.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 { } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining/WalletDeployer.sol b/contracts/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining/WalletDeployer.sol new file mode 100644 index 0000000..5fcfe7c --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/13.Wallet-Mining/WalletDeployer.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts-v4.7.1/token/ERC20/IERC20.sol"; + +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/PuppetV3Pool.sol b/contracts/CTF/Damn-Vulnerable-DeFi/14.Puppet-V3/PuppetV3Pool.sol new file mode 100644 index 0000000..4a6f080 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/14.Puppet-V3/PuppetV3Pool.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// import "@uniswap/v3-core/contracts/interfaces/IERC20Minimal.sol"; +// import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +// import "@uniswap/v3-core/contracts/libraries/TransferHelper.sol"; +// import "@uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol"; + +// /** +// * @title PuppetV3Pool +// * @notice A simple lending pool using Uniswap v3 as TWAP oracle. +// * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) +// */ +// contract PuppetV3Pool { +// uint256 public constant DEPOSIT_FACTOR = 3; +// uint32 public constant TWAP_PERIOD = 10 minutes; + +// IERC20Minimal public immutable weth; +// IERC20Minimal public immutable token; +// IUniswapV3Pool public immutable uniswapV3Pool; + +// mapping(address => uint256) public deposits; + +// event Borrowed(address indexed borrower, uint256 depositAmount, uint256 borrowAmount); + +// constructor(IERC20Minimal _weth, IERC20Minimal _token, IUniswapV3Pool _uniswapV3Pool) { +// weth = _weth; +// token = _token; +// uniswapV3Pool = _uniswapV3Pool; +// } + +// /** +// * @notice Allows borrowing `borrowAmount` of tokens by first depositing three times their value in WETH. +// * Sender must have approved enough WETH in advance. +// * Calculations assume that WETH and the borrowed token have the same number of decimals. +// * @param borrowAmount amount of tokens the user intends to borrow +// */ +// function borrow(uint256 borrowAmount) external { +// // Calculate how much WETH the user must deposit +// uint256 depositOfWETHRequired = calculateDepositOfWETHRequired(borrowAmount); + +// // Pull the WETH +// weth.transferFrom(msg.sender, address(this), depositOfWETHRequired); + +// // internal accounting +// deposits[msg.sender] += depositOfWETHRequired; + +// TransferHelper.safeTransfer(address(token), msg.sender, borrowAmount); + +// emit Borrowed(msg.sender, depositOfWETHRequired, borrowAmount); +// } + +// function calculateDepositOfWETHRequired(uint256 amount) public view returns (uint256) { +// uint256 quote = _getOracleQuote(_toUint128(amount)); +// return quote * DEPOSIT_FACTOR; +// } + +// function _getOracleQuote(uint128 amount) private view returns (uint256) { +// (int24 arithmeticMeanTick,) = OracleLibrary.consult(address(uniswapV3Pool), TWAP_PERIOD); +// return OracleLibrary.getQuoteAtTick( +// arithmeticMeanTick, +// amount, // baseAmount +// address(token), // baseToken +// address(weth) // quoteToken +// ); +// } + +// function _toUint128(uint256 amount) private pure returns (uint128 n) { +// require(amount == (n = uint128(amount))); +// } +// } diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling/AuthorizedExecutor.sol b/contracts/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling/AuthorizedExecutor.sol new file mode 100644 index 0000000..49a00ca --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling/AuthorizedExecutor.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts-v4.7.1/utils/Address.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)); + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling/SelfAuthorizedVault.sol b/contracts/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling/SelfAuthorizedVault.sol new file mode 100644 index 0000000..5f2f9c5 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/15.ABI-Smuggling/SelfAuthorizedVault.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-v4.7.1/token/ERC20/IERC20.sol"; +import "@solady/utils/SafeTransferLib.sol"; +import "./AuthorizedExecutor.sol"; + +/** + * @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/lib/openzeppelin-contracts-upgradeable b/foundry/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..625fb3c --- /dev/null +++ b/foundry/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 625fb3c2b2696f1747ba2e72d1e1113066e6c177 diff --git a/foundry/lib/openzeppelin-contracts-upgradeable-v4.7.1 b/foundry/lib/openzeppelin-contracts-upgradeable-v4.7.1 new file mode 160000 index 0000000..5e9bccb --- /dev/null +++ b/foundry/lib/openzeppelin-contracts-upgradeable-v4.7.1 @@ -0,0 +1 @@ +Subproject commit 5e9bccb282ee8f3c9c4abaccc74b40b9d34ccffa diff --git a/foundry/lib/openzeppelin-contracts-v4.7.1 b/foundry/lib/openzeppelin-contracts-v4.7.1 new file mode 160000 index 0000000..3b8b4ba --- /dev/null +++ b/foundry/lib/openzeppelin-contracts-v4.7.1 @@ -0,0 +1 @@ +Subproject commit 3b8b4ba82c880c31cd3b96dd5e15741d7e26658e diff --git a/foundry/lib/safe-contracts b/foundry/lib/safe-contracts new file mode 160000 index 0000000..186a21a --- /dev/null +++ b/foundry/lib/safe-contracts @@ -0,0 +1 @@ +Subproject commit 186a21a74b327f17fc41217a927dea7064f74604 diff --git a/remappings.txt b/remappings.txt index d01d1dd..5e2b4ca 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,9 +1,18 @@ @contracts=contracts + forge-std=foundry/lib/forge-std/src @solmate=foundry/lib/solmate/src @solady=foundry/lib/solady/src @prb/test=foundry/lib/prb-test/src -@openzeppelin=foundry/lib/openzeppelin-contracts + +@gnosis/safe-contracts=foundry/lib/safe-contracts/contracts + +@openzeppelin/contracts=foundry/lib/openzeppelin-contracts/contracts +@openzeppelin/contracts-v4.7.1=foundry/lib/openzeppelin-contracts-v4.7.1/contracts + +@openzeppelin/contracts-upgradeable=foundry/lib/openzeppelin-contracts-upgradeable/contracts +@openzeppelin/contracts-upgradeable-v4.7.1=foundry/lib/openzeppelin-contracts-upgradeable-v4.7.1/contracts + @uniswap/v2-core=foundry/lib/v2-core @uniswap/v2-periphery=foundry/lib/v2-periphery @uniswap/v3-core=foundry/lib/v3-core