Отправьте эфир, чтобы заработать больше эфира (если ответ правильный)
Создание случайного числа в сети — сложная задача. На самом деле, есть некоторые обходные пути, чтобы сделать это, но в целом настоятельно рекомендуется делать это вне сети, поскольку почти каждый ввод, который может использоваться для энтропии, является общедоступным и/или им можно в некоторой степени манипулировать.
К счастью для нас, в этом задании нам предлагается угадать «случайное» число, созданное в сети. Как так?
Первая строка контракта — это переменная 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.
В следующей статье мы решим задачу «Угадай новое число».