WEB3DEV

Cover image for Resolvendo Quebra-Cabeças more-evm-puzzles de Maneira Diferente? - Parte I
Paulo Gio
Paulo Gio

Posted on • Atualizado em

Resolvendo Quebra-Cabeças more-evm-puzzles de Maneira Diferente? - Parte I

Este post é a primeira parte de uma pequena série dedicada a me motivar enquanto aprendo como a EVM (Ethereum Virtual Machine, Máquina Virtual Ethereum) funciona. Vá para a segunda parte.

https://miro.medium.com/max/1100/0*Cxzxesx_0BAab4D1.webp

Ethereum Virtuelle Machine — cryptoast.fr

Com meu retorno ao estudo das tecnologias blockchain, incluí em meu roadmap (roteiro) alguns desafios para tornar meu aprendizado um pouco mais divertido.

Quem não o faria? 😃

Neste caso, falarei apenas de dois desafios ✌️. São os quebra-cabeças “more-evm-puzzles” de Dalton Sweeney, uma inspiração dos quebra-cabeças da evm de Franco Victorio.

A propósito, se você ainda não tentou resolver esses quebra-cabeças, vá em frente e volte mais tarde, por favor! 💨

Cada quebra-cabeça consiste em enviar uma transação bem-sucedida para um contrato. O bytecode do contrato é fornecido e você precisa preencher os dados da transação que não reverterão a execução.

Dalton concentra-se principalmente nos códigos de operação (opcodes) CREATE e CALL.

Por que apenas dois?

Apesar de haver um único objetivo a cumprir em cada desafio, pode haver um grande número de formas de o abordar 😌.

Por curiosidade, verifiquei online as soluções de outras pessoas e encontrei diferenças em duas delas. Os desafios que serão discutidos nesta série são os quebra-cabeças 4 e 9.

Para resolvê-los, usaremos caneta, papel e a referência interativa dos códigos EVM e seus playgrounds.

Quebra-cabeça 4

Link para o playground do desafio

https://miro.medium.com/max/1100/1*c2XLkHmLe2BjDCMtzT20AA.webp

Mnemônico para o bytecode deste quebra-cabeça

Entendendo a lógica

Vamos separar algumas partes do código em pedaços. Eles serão exibidos indicando a posição atual da instrução no programa, seguida pelo mnemônico da instrução naquela posição, e depois de uma barra dupla, por nossa suposição de como a pilha terminará se a instrução for executada.

#. INSTRUÇÃO // [ITEMDAPILHA_0, ITEMDAPILHA_1]
Enter fullscreen mode Exit fullscreen mode

Vamos começar!

Essas duas primeiras instruções fornecem um comportamento semelhante ao da execução de address(this).balance.

00. ADDRESS // [AccountAddress]
01. BALANCE // [AccountBalance]
Ele coloca o endereço da conta atual na pilha e, em seguida, recupera o saldo dessa conta, colocando-o também na pilha.
Enter fullscreen mode Exit fullscreen mode

Por que não digo igual? Porque existe uma instrução específica para conseguir o seu próprio saldo, que é mais barato, chamada de SELFBALANCE. Esta instrução custa apenas 5 gás, e o uso apenas do BALANCE custa 100 gás 😧.

Mas agora, antes de continuarmos, sinto-me obrigado a fazer uma pausa obrigatória para introduzir uma ressalva para estes tipos de desafios.

O playground acumula o msg.value que está sendo enviado - do nada - em cada Execução em sua conta, tornando quase impossível codificar rigidamente uma solução com base nesse tipo de entrada, a menos que você reinicie o playground recarregando o site ou usando SELFDESTRUCT em si mesmo 😅.

No entanto, esse comportamento não acontecerá no momento em que você fornecer a solução por meio do hardhat, mas levar isso em consideração o deixará um pouco mais ciente de como funciona.

Se você quiser testar por si mesmo, basta executar as duas primeiras instruções (ADDRESS, BALANCE) com 1 Wei como exemplo, e você verá o saldo da conta aumentar ao executá-las consecutivamente.

Chega de falar!

— Ok, desculpe!

Agora que temos esta informação, vamos voltar ao quebra-cabeça.

Isso copia tudo o que você enviou em calldata (dados de chamada) para a memória!

02. CALLDATASIZE // [CallDataSize, AccountBalance]
04. PUSH1 0x00 // [0, CallDataSize, AccountBalance]
06. PUSH1 0x00 // [0, 0, CallDataSize, AccountBalance]
07. CALLDATACOPY // [AccountBalance]
CALLDATACOPY reúne tudo começando na posição 0 de calldata, com um deslocamento de 0, até seu tamanho real, desde que CALLDATASIZE tenha sido usado.
Enter fullscreen mode Exit fullscreen mode

Esse pedaço cria um novo contrato com os dados da chamada que foram armazenados na memória e envia todo o nosso saldo atual para ele.

08. CALLDATASIZE // [CallDataSize, AccountBalance]
0A. PUSH1 0x00 // [0, CallDataSize, AccountBalance]
0B. ADDRESS // [AccountAddress, 0, CallDataSize, AccountBalance]
0C. BALANCE // [AccBalance, AccAddr, 0, CallDataSize, AccBalance]
0D. CREATE // [ContractAddress, AccountBalance]
CREATE consome Value, Offset, Size e retorna Address do novo contrato. Basicamente, estabelece o valor do saldo em Wei para inicializar o novo contrato e o tamanho do seu código com o deslocamento para iniciar, diretamente da memória.
Enter fullscreen mode Exit fullscreen mode

Depois, ele verifica se nosso saldo original é o dobro desse novo valor do contrato.

0E. BALANCE // [BalanceOfContract, AccountBalance]
0F. SWAP1 // [AccountBalance, BalanceOfContract]
10. DIV // [AccountBalance / BalanceOfContract]
12. PUSH1 0x02 // [2, AccountBalance / BalanceOfContract]
13. EQ // [ (AccountBalance / BalanceOfContract) == 2? 1 : 0]
Obtém o saldo do novo contrato deixado na pilha por CREATE e ordena que a pilha seja capaz de fazer uma divisão entre nosso saldo inicial versus o do contrato atual. Verifica se o resultado de sua divisão é 2. Se for, então 1 é colocado na pilha, caso contrário, o 0 é colocado.
Enter fullscreen mode Exit fullscreen mode

Se for 2, então você acertou em cheio! 💅

Caso contrário, a execução do programa será interrompida com uma reversão. ❌

15. PUSH1 0x18 // [18, (AccBalance / BalanceOfContract) == 2? 1 : 0]
16. JUMP1 // [ ] -- Jumps to 18 if stack[1] == 1, otherwise reverts.
17. REVERT // Fail
18. JUMPDEST // Solved!
19. STOP
Defina o destino do salto para terminar a execução corretamente se o resultado da divisão for 2, caso contrário, siga com a próxima instrução e reverta. De qualquer maneira, a pilha acaba vazia.
Enter fullscreen mode Exit fullscreen mode

Resolvendo o quebra-cabeça

Tudo isso deve ser bem direto, certo? Só temos que garantir que o novo contrato reduza seu saldo pela metade antes de chegarmos à divisão.

A única maneira de fazer isso, como você pode perceber, é na criação do próprio contrato.

Algumas das soluções que vi enviam um valor fixo. Mas e se você não puder saber seu saldo atual de antemão?

Solução

Link para a solução abaixo no playground de códigos da EVM.

Nossa abordagem aqui será o uso de CALL — claro — , para queimar metade do valor atual fornecido. Basicamente, apenas enviaremos ether para a conta de endereço 0x0.

A instrução de chamada pede 7 parâmetros:

call(gas, address, value, argsOffset, argsSize, retOffset, retSize).
Enter fullscreen mode Exit fullscreen mode

Como não precisamos chamar com argumentos, nem esperar um valor em retorno, podemos ignorar os últimos 4 parâmetros. A construção será a seguinte:

call(currentGas, 0x0, msg.value/2, 0, 0, 0, 0).
Enter fullscreen mode Exit fullscreen mode

Primeiro, vamos obter o valor da metade de nosso msg.value.

00. PUSH1 0x02 // [2]
02. CALLVALUE // [CallValue, 2]
03. DIV // [(CallValueInHalf)]
Empurramos primeiro o divisor, depois o dividendo, e então a divisão é chamada, que coloca o resultado na pilha.
Enter fullscreen mode Exit fullscreen mode

Agora criamos os parâmetros restantes necessários para executar a chamada.

05. PUSH1 0x00 // [0, CallValueInHalf]
06. DUP1 // [0, 0, CallValueInHalf]
07. DUP1 // [0, 0, 0, CallValueInHalf]
08. DUP1 // [0, 0, 0, 0, CallValueInHalf]
09. SWAP4 // [CallValueInHalf, 0, 0, 0, 0]
0A. DUP2 // [0, CallValueInHalf, 0, 0, 0, 0]
0B. GAS // [CurrentGas, 0, CallValueInHalf, 0, 0, 0, 0]
0C. CALL // [Success]
Vamos começar obtendo todos os parâmetros acima mencionados em ordem inversa e trocamos as posições com CallValueInHalf quando chegamos ao valor. Então chamamos e conseguimos saber se deu certo ou não.
Enter fullscreen mode Exit fullscreen mode

Por fim, voltamos à execução do programa torcendo para que tudo tenha funcionado corretamente!

0E. PUSH1 0x00 // [0, Success]
0F. DUP1 // [0, 0, Success]
10. RETURN // [Success]
RETURN espera um deslocamento e um tamanho, como dados de retorno, mas como não precisamos fazer isso, podemos apenas retornar 'vazio'.
Enter fullscreen mode Exit fullscreen mode

Nós apenas usamos esse bytecode de código como o calldata para nossa solução e seria isso, certo?

Mas espere um minuto!

O que aconteceria se tivéssemos um número ímpar como parâmetro de entrada para o nosso valor? O que aconteceria? Ainda funcionaria?!

— Que bom que você perguntou! Felizmente para nós, podemos controlar o valor da mensagem, então apenas enviamos números pares e isso é tudo.

Mas não saber qual seria o saldo não era o propósito deste artigo?

— Bem, sim, mas caso contrário, eu nunca teria uma desculpa adequada para escrever sobre esta solução, então não vamos mais falar sobre isso.

Vá para a segunda parte desta série ➡️

Artigo original publicado por ᴍatías Λereal Λeón. Traduzido por Paulinho Giovannini.

Oldest comments (0)