Отправьте эфир, чтобы заработать больше эфира (если ответ правильный)

Создание случайного числа в сети — сложная задача. На самом деле, есть некоторые обходные пути, чтобы сделать это, но в целом настоятельно рекомендуется делать это вне сети, поскольку почти каждый ввод, который может использоваться для энтропии, является общедоступным и/или им можно в некоторой степени манипулировать.

К счастью для нас, в этом задании нам предлагается угадать «случайное» число, созданное в сети. Как так?

Первая строка контракта — это переменная uint8, answer. Помните, что переменные uint8 содержат до 256 возможных целых чисел: от 0 до 255.

Эта переменная присваивается в конструкторе хэшу keccak256 двух входных данных: хэшу предыдущего блока того, в который была включена наша развертывающая транзакция (block.blockhash(block.number - 1), типа bytes32) и отметке времени, когда наш блок был добыт (now, типа uint256).

Имейте в виду, что этот контракт использует версию компилятора ^0.4.21, и с тех пор некоторый синтаксис изменился: block.blockhash() теперь blockhash(), а now - block.timestamp. Мы увидим это дальше.

Как мы видим в этой строке, функция keccak256 (массив байтов фиксированного размера bytes32) затем явно преобразуется в uint8 и присваивается нашей переменной.

Итак, это кажется довольно случайным, верно? Как мы должны угадать число от 0 до 255, полученное в результате применения функции хэширования к хэшу какого-то блока и отметке времени неизвестно когда?!

Ну, на самом деле довольно легко. Помните, что все, что находится в блокчейне, является общедоступным. Итак, идем дальше и ищем нужную нам информацию.

Наша цель находится под функцией guess, мы должны вызвать ее и отправить uint8 плюс 1 ether (мы уже отправили один при развертывании), затем, если наше uint8 равно переменной answer, контракт отправит нам 2 эфира, сливая баланс , поэтому функция isComplete() вернет true.

Есть несколько способов взаимодействия с контрактом, но я решил сделать это через другой контракт. Это не самый простой способ и в данном случае даже не необходимый, но определенно тот, которым мы можем воспользоваться с большей пользой.

Вот код, который я написал для решения задачи:

// SPDX-License-Identifier: No License
pragma solidity ^0.8.0;
interface IGuessTheRandomNumberChallenge {
  function guess(uint8) external payable;
}
contract GuessTheRandomNumberSolver {
  IGuessTheRandomNumberChallenge public _interface;
  bytes32 public previousBlockHash = 0x66bcdb5e320c9e0c04a9fdeaa15de33a4c8a040db342f4f955fa54f170dba9ce;
  uint public previousTimestamp = 1641520092;
  constructor(address _interfaceAddress) {
    require(_interfaceAddress != address(0), "Address can not be Zero");
    _interface = IGuessTheRandomNumberChallenge(_interfaceAddress);
  }
  function solve() public payable {
    uint8 answer = uint8(uint256(keccak256(abi.encodePacked(previousBlockHash, previousTimestamp))));
    _interface.guess{value: 1 ether}(answer);
  }
  function getBalance() public view returns(uint){
    return address(this).balance;
  }
  function withdraw() public {
    payable(msg.sender).transfer(address(this).balance);
  }
  receive() external payable {}
}

Первое, что вы видите после версии компилятора — это интерфейс. Мы можем использовать их для взаимодействия с другими контрактами через наш код. По сути, это более простой контракт с некоторыми правилами:

  • Они не могут наследовать от других контрактов, но могут наследовать от других интерфейсов.
  • Все объявленные функции должны быть внешними.
  • Они не могут объявить конструктор.
  • Они не могут объявлять переменные состояния.
  • Они не могут объявлять модификаторы.

Поскольку нам нужно вызвать только функцию ‘guess’, это единственная функция, которую мы объявили в нашем интерфейсе.

Затем в нашем контракте GuessTheRandomNumberSolver мы объявим переменную _interface и назначим адрес вызова (тот, который вы получили при его развертывании в CTE) через наш конструктор.

Это все, что нам нужно на данный момент, чтобы вызвать функцию в нашем развернутом вызове, поэтому давайте продолжим и соберем информацию, чтобы воссоздать номер random, который был развернут с ним.

Все это доступно в etherscan, нам просто нужно найти адрес нашего вызова.

Blockhash(block.number - 1): чтобы получить это, перейдите на вкладку Внутренние транзакции и щелкните номер блока в той же строке, что и Создание контракта. В моем случае блок был #11766860:

Теперь мы можем увидеть много информации об этом блоке, но нам нужно получить доступ к предыдущему, так что продолжайте искать его. В моем случае это #11766859.

Внизу мы видим hash. Это первая часть информации, которая нам нужна.

Block.timestamp:вернитесь к нашему блоку, и вы увидите Timestamp во второй строке. Но подождите, это в удобочитаемом формате, и нам нужно это в формате Unix Timestamp. Что это такое? По сути, это количество секунд, прошедших с 1 января 1970 года. И это стандартный способ измерения времени. На момент написания это 10-значное число.

Чтобы преобразовать эту удобочитаемую временную метку в время Unix, я использую очень удобный сайт под названием epochconverter. С этим номером у нас, наконец, есть последний кусочек головоломки, и мы можем позвонить, чтобы решить задачу.

Возвращаясь к контракту GuessTheRandomNumberSolver, давайте создадим функцию solve, которую мы будем вызывать, чтобы связаться с нашим контрактом вызова.

Для удобства чтения я также создал две новые переменные:

  • bytes32 public previousBlockHash
  • uint public previousTimestamp.

Создайте их, но назначьте им значения вашей задачи.

Затем в нашей функции solve мы создадим переменную uint8 answer и присвоим ей значение:

uint8(uint256(keccak256(abi.encodePacked(previousBlockHash, previousTimestamp))))

Изменение синтаксиса и форматирования связано с тем, что мы используем версию компилятора ^0.8.0, а задача использует версию ^0.4.21.

Теперь, когда у нас есть ответ, назначенный нашей переменной, нам просто нужно вызвать вызов через интерфейс. Вот что делает следующая строка:

_interface.guess{value: 1 ether}(answer)

Я предполагаю, что вы используете remix, так что продолжайте, подключите свой кошелек метамаски через среду Injected Web3 и разверните свой контракт, указав адрес вашего вызова для назначения вашему интерфейсу.

Теперь, имея 1 эфир на входеvalue, вызовите функцию guess.

Я добавил еще пару функций:

  • getBalance()
  • withdraw()
  • receive()

Это связано с тем, что msg.sender вызова будет нашим GuessTheRandomNumberSolver контрактом, а не нашим EOA, поэтому нам нужно получить 2 эфира и иметь возможность отправить их в наш EOA.

В следующей статье мы решим задачу «Угадай новое число».