WEB3DEV

Cover image for Brincando com o Yul (Assembly)
Fatima Lima
Fatima Lima

Posted on

Brincando com o Yul (Assembly)

O Assembly tem sido muito badalado ultimamente e todas as crianças legais parecem ter começado a aprender isso. Então decidi fazer o mesmo e aprender o que é Yul e como posso escrever meus contratos com ele. Comecei a estudar há algumas semanas e hoje vou mostrar o básico para que você possa começar a apreciá-lo.

Este tutorial é um pouco avançado e não começa desde o início. Se você quiser uma verdadeira introdução, você pode conferir a incrível série por @noxx3xxon Mergulhos profundos na EVM: O Caminho para o Shadowy Super Coder (N.T. você encontra a tradução desses artigos aqui) onde tudo está realmente bem explicado. Ou este ótimo tutorial por @JeanCavallera.

O que é Assembly/Yul

Não vou entrar em muitos detalhes, mas basicamente assembly (ou assembler) é uma linguagem de baixo nível, muito próxima do que seu computador pode entender. É uma sequência de instruções para que seu computador execute (aqui: a EVM).

Yul é apenas o nome do (quase) assembly para a EVM. Eu digo "quase" porque é um pouco mais fácil de escrever do que o assembly puro e tem o conceito de variáveis, funções, loops for, declarações if, ... enquanto que o assembly puro não tem. Então, o Yul torna nossas vidas um pouco mais fáceis😊

Você pode usar Yul quando precisar ter mais controle sobre o que seu código está fazendo. Você pode fazer qualquer coisa em Yul já que controla exatamente o que a EVM vai executar, enquanto que o Solidity é mais restritivo. E na maioria das vezes o Yul é usado para otimizações de gas.

Escrever um contrato inteiro em assembly (Yul) normalmente não faria sentido, mas é isso que vamos fazer aqui para que você possa entender melhor como ele funciona e como a EVM funciona.

O contrato básico

Como já disse, tentarei explicar o máximo possível, mas não vou passar pelo básico. Portanto, se você quiser entender este tutorial, precisará de um bom entendimento de Solidity e de EVM.

Vamos começar! Vamos reescrever este (realmente inseguro e estúpido 😄) contrato de "loteria" para assembly. Ele não tem controle de acesso e as funções são um pouco bobas, mas será mais fácil de escrever/entender quando escrito em assembly 😊

contract AngleExplainsBase {

    uint private secretNumber;

    mapping(address =>  uint) public guesses;

    bytes32 public secretWord;

    //naturalmente isso não faz sentido

    //mas vai ser divertido escrever isso em assembly :D

    function getSecretNumber() external view returns(uint) {

        return secretNumber;

    }

    // isso só pode ser definido por um admin

    // sem controle de acesso porque queremos simplificar no assembly

    function setSecretNumber(uint number) external {

        secretNumber = number;

    }

    // um usuário pode adicionar um palpite (guess)

    function addGuess(uint _guess) external {

        guesses[msg.sender] = _guess;

    }

    // sim eu sei... não faz sentido porque você pode alterar palpites de qualquer usuário

    // é apenas para lhe ensinar como analisar arrays em assembly

    function addMultipleGuesses(address[] memory _users, uint[] memory _guesses) external {

        for (uint i = 0; i < _users.length; i++) {

            guesses[_users[i]] = _guesses[i];

        }

    }

    // isto é inútil já que a "secretWord" não é utilizada em nenhum lugar

    // mas isto nos ensinará a fazer um hash de uma string em assembly. Realmente legal! :)

    function hashSecretWord(string memory _str) external {

        secretWord = keccak256(abi.encodePacked(_str));

    }
}
Enter fullscreen mode Exit fullscreen mode

Temos um secretNumber que é privado e temos um getter para esse número secreto getSecretNumber. Sim, não faz sentido, mas se você estiver lendo isto, você deve saber que nada é privado na blockchain de qualquer maneira. Então não importa realmente se adicionarmos um getter. Será apenas divertido escrevê-lo para o assembly. Então, obviamente, temos um setter setSecretNumber.

O usuário pode adicionar 1 ou vários palpite (s) addGuess / addMultipleGuesses.

Assim, temos uma função extra hashSecretWord. Imaginemos: poderíamos usá-la se decidíssemos mudar de um número secreto para uma string secreta. Aqui isso nos ajudará a entender mais sobre como as strings são manipuladas na memória e aprenderemos como fazer um hash em assembly (muito legal!).

Obter/Definir nosso número secreto

Ao longo do código, você verá muitos 0x20 ou múltiplos disso (0x40, 0x60, 0x80, 0xa0, ...). Isso é uma representação hexadecimal de 32 porque a EVM usa slots (palavras) de 32 bytes de memória. Portanto, os valores são sempre codificados em 32 bytes (sim, você deve saber contar em hexadecimal).

Vamos começar com getSecretNumber. Precisaremos dos opcodes SLOAD, MLOAD, MSTORE e RETURN para essa função. Use o excelente site https://www.evm.codes para aprender mais sobre opcodes EVM.

O SLOAD apenas recupera um valor do armazenamento. Assim, usamos SLOAD para obter o valor do nosso número secreto. Então, em um mundo ideal, deveríamos estar prontos e apenas ser capazes de retornar esse número. Mas a EVM é um pouco mais complexa do que isso e só retorna valores que estão armazenados na memória. O Solidity nos facilita e nos permite retornar apenas um valor e não nos importa o que acontece debaixo dos panos, mas lembre-se de que o Yul é mais de baixo nível. Então precisamos fazer o trabalho duro.

Primeiro vamos armazenar esse valor na memória com MSTORE e depois podemos retorná-lo.

Ponteiro de memória livre

Usaremos o " ponteiro de memória livre ", que é armazenado em 0x40 na memória.

mload(0x40) nos dá o endereço na memória onde podemos escrever (para não gravar em cima de nada). A memória anterior a esse endereço já é utilizada, portanto, se a sobrescrevermos, podemos estragar toda a nossa transação (ou até mesmo o contrato 😮 se algo nessa memória era para ter sido escrito no armazenamento, por exemplo).

Nós armazenamos nosso número lá (MSTORE). E então o retornamos especificando o endereço em memória e o tamanho que deve ser retornado.

function getSecretNumber() external view returns(uint) {

        assembly {

            // Obtemos o valor para o secretNumber que está no slot 0

            //em Yul, você também tem acesso ao número do slot de uma variável por meio de `.slot`

                        // https://docs.soliditylang.org/en/latest/assembly.html#access-to-external-variables-functions-and-libraries

            // então também podemos apenas escrever `sload(secretNumber.slot)`

            // SLOAD https://www.evm.codes/#54

            let _secretNumber := sload(0)

            // então obtemos o "ponteiro de memória livre"

            // isso significa que obtemos o endereço na memória para onde podemos escrever

            // usamos o opcode  MLOAD para isso: https://www.evm.codes/#51

            // Obtemos o valor armazenado em 0x40 (64)

            // 0x40 é apenas uma constante decidida na EVM onde o endereço da memória livre é armazenado

            // veja aqui: https://docs.soliditylang.org/en/latest/assembly.html#memory-management

            let ptr := mload(0x40)

            // escrevemos nosso número neste endereço

            // para fazer isto, usamos o opcode MSTORE: https://www.evm.codes/#52

            // Ele leva 2 parâmetros: o endereço na memória onde nosso valor deve ser armazenado e o valor a ser armazenado

            mstore(ptr, _secretNumber)

            // então RETORNAREMOS o valor: https://www.evm.codes/#f3

            // especificamos o endereço onde o valor é armazenado: `ptr`

            // e o tamanho do parâmetro retornado: 32 bytes (lembre que valores são sempre armazenados em 32 bytes)

            return(ptr, 0x20)

        }

    }
Enter fullscreen mode Exit fullscreen mode

Eu só quis complicar um pouco as coisas para você, usando o ponteiro de memória livre. Mas poderíamos escrever nossa função de uma forma mais curta:

// em vez de usar o ponteiro de memória livre, também poderíamos armazenar o valor em `0`

//porque os 2 primeiros slots na memória são usados como "espaço de trabalho".

// https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html#layout-in-memory

//isto significa que eles são usados para armazenar valores temporários, como valores de retorno

//teríamos tido:

assembly {

    let _secretNumber := sload(0)

    mstore(0, _secretNumber)

    return(0, 0x20)

}
Enter fullscreen mode Exit fullscreen mode

Por quê? Você tem que saber que a EVM reserva os 4 primeiros slots na memória para fins especiais. Aqui estão os slots. Acrescentei um quinto que representa o primeiro slot gravável.

offset value
0x00 (0) espaço de trabalho temporário, pode ser usado para armazenar qualquer coisa
0x20 (32) espaço de trabalho temporário, pode ser usado para armazenar qualquer coisa
0x40 (64) ponteiro de memória livre. O valor inicial é 0x80 (onde começa a memória para a qual podemos escrever)
0x60 (96) slot zero, nunca deve ser alterado
0x80 (128) aqui é onde começa a memória

Como você pode ver, podemos usar os dois primeiros slots para escrever qualquer coisa. Mas temos que ter em mente que eles também podem ser sobrescritos a qualquer momento.

Em seguida, escreveremos setSecretNumber, que é um pouco mais fácil. Só precisamos recuperar o número do slot onde o valor é armazenado e usar SSTORE para armazenar nosso novo valor.

Aqui usaremos apenas o assistente especial .slot que o Yul nos oferece. Isso facilita, de modo que não temos que calcular manualmente o número do slot😄

function setSecretNumber(uint _number) external {

        assembly {

            // Obtemos o número do slot para o`secretNumber`

            let slot := secretNumber.slot

            // Usamos SSTORE para armazenar o novo valor

            // https://www.evm.codes/#51

            sstore(slot, _number)

        }

    }
Enter fullscreen mode Exit fullscreen mode

Adicionar palpites (guesses)

Agora precisamos permitir que um usuário acrescente um palpite. Mas guesses é um mapeamento, então isso complica as coisas. Precisamos primeiro calcular o valor do slot de armazenamento onde guess será armazenado e, em seguida, podemos armazená-lo com SSTORE.

Como mapeamentos funcionam

Para escrever um valor para um mapeamento: concatenamos a chave e o número do slot do mapeamento e fazemos um hash dele (para mais detalhes: https://solidity-fr.readthedocs.io/fr/latest/internals/layout_in_storage.html#mappings-and-dynamic-arrays). Aqui nosso mapeamento guesses está no slot de armazenamento 1.

Assim, em Solidity, obteríamos o slot de armazenamento fazendo keccak256(abi.encode(msg.sender, 1))

Para criar um hash de alguma coisa em Yul, temos que armazenar isso na memória primeiro. Não é possível de outra forma, pois keccak256() só busca na memória.

Aqui estão os passos para obter o nosso número de slot

  • obter o endereço msg.sender
  • obter o número do slot do mapeamento
  • armazenar ambos na memória (em ordem e um ao lado do outro)
  • computar o hash
function addGuess(uint _guess) external {

        assembly {

            // primeiro calculamos o slot onde vamos armazenar o valor

            // https://solidity-fr.readthedocs.io/fr/latest/internals/layout_in_storage.html#mappings-and-dynamic-arrays

            // temos keccak256(abi.encode(_user, 1)) onde 1 é o número do slot para `guesses`

            let ptr := mload(0x40)

            // armazenamos o endereço do msg.sender no endereço `ptr` 

                        // opcode CALLER: https://www.evm.codes/#33

            mstore(ptr, caller())

            // então, logo após isso, armazenamos o número do slot para `guesses`

            mstore(add(ptr, 0x20), guesses.slot)

            // os 2 MSTORE anteriores equivalem a abi.encode(msg.sender, 1)

            // então apenas calculamos o hash do msg.Sender e guesses.slot

            //eles estão atualmente armazenados em `ptr` e utilizam 2 slots (2x 32bytes -> 0x40)

            let slot := keccak256(ptr, 0x40)

            // agora só precisamos armazenar o valor neste slot

            sstore(slot, _guess)

        }

    }
Enter fullscreen mode Exit fullscreen mode

Para obter o endereço do msg.sender em assembly, usamos o opcode CALLER.

Usamos o ponteiro de memória livre para saber onde escrever nossos valores. Em seguida, armazenamos o msg.sender (caller()).

add(ptr, 0x20) nos dá o endereço de memória 32 bytes depois, o que significa "o próximo slot de memória". É lá que armazenaremos nosso segundo valor.

Operações em Assembly

No Assembly não podemos fazer operações simples (+ - * /), elas simplesmente não existem ☹️

Para isso, precisamos utilizar opcodes específicos. Aqui usamos ADD para adicionar 32 bytes ao endereço do ptr

Isso equivale a: ptr = ptr + 32

Então, fazemos hash de tudo isso. O segundo argumento do keccak256 é o tamanho dos dados que receberão um hash. Aqui são 2 slots de memória, portanto 2*32=64 (0x40 em hexadecimal).

Fazer Hash de algumas strings

Vamos fazer uma rápida pausa de nossas principais funções e focar na função mais inútil (mas mais divertida) de nosso contrato Solidity: hashSecretWord. Chegaremos ao ponto de escrevê-la 2 vezes com 2 técnicas diferentes para obter o valor do parâmetro _str. Usaremos os opcodes CALLDATA para ajudá-lo a entender como o calldata (dados de chamada) funciona e como manipulá-lo (de nada!).

Tipos não-valor (non-value)

Primeiro, uma pequena lição sobre tipos não-valor: uma das partes complicadas que notei quando aprendi Yul pela primeira vez foi quando lidei com tipos não-valor (array, mapeamento, bytes ou string). Mas eles não são tão difíceis de entender, basta entender como a EVM lida com eles e os armazena na memória.

É assim: esses valores são geralmente armazenados em 2 partes: primeiro o comprimento e depois o valor real. Imagine você passar a string "angle" como um parâmetro. Ela será armazenada como "5angle" para que a EVM saiba que deve ler os próximos 5 caracteres.

Na verdade, a aparência será um pouco diferente. Como a EVM funciona com slots de memória de 32 bytes, ela será mais parecida com isso: 0000000000000000000000000000000000000000000000000000000000000005616e676c65000000000000000000000000000000000000000000000000000000

observe o número 5 e depois a palavra angle escritos em hexadecimal (616e676c65)

Ok, de volta a nosso código

// computa o hash keccak256 de uma string e armazena-a em uma variável de estado

function hashSecretWord1(string memory _str) external view returns(bytes32) {

    assembly {

        // no assembly `_str`é apenas um ponteiro para a string

        // ele representa o endereço em memória onde os dados para nossa string começam

        // em `_str` temos o comprimento da string

        // em `_str` + 32 -> temos a própria string 

        // aqui obtemos o tamanho da string

        let strSize := mload(_str)

        //aqui acrescentamos 32 àquele endereço, para que tenhamos o endereço da própria string

        let strAddr := add(_str, 32)

        // passamos então o endereço da string e seu tamanho. Isto vai criar um hash para nossa string

        let hash := keccak256(strAddr, strSize)

        // armazenamos o valor do hash no slot 0 na memória

        // tal como explicamos anteriormente, isto é usado como armazenamento temporário (espaço de trabalho temporário)

        // não há necessidade de obter o ponteiro de memória livre. É mais rápido (e mais barato) de usar `0`

        mstore(0, hash)

        // retornamos o que está armazenado no slot 0 (nosso hash) e o comprimento do hash (32)

        return (0, 32)

    }

}
Enter fullscreen mode Exit fullscreen mode

Aqui _str é um ponteiro para o slot na memória onde o comprimento da string é armazenado e então (no próximo slot) começa a própria string. Portanto, só precisamos recuperar esse tamanho, depois podemos simplesmente criar um hash da string diretamente, já que ela já está na memória e sabemos seu endereço. Fácil!

Para facilitar ainda mais, deixe-me mostrar como é a memória no início de nossa função. Digamos que passamos a string "stablecoin".

offset value
0x00 (0)
0x20 (32)
0x40 (64) 0xc0
0x60 (96)
0x80 (128) 10
0xa0 (160) stablecoin
0xc0 (192)

O comprimento da nossa string está escrito em 0x80 e nossa palavra está escrita em 0xa0. O ponteiro de memória livre aponta para o próximo espaço de memória disponível: 0xc0.

E _str é igual a 0x80 (onde nossa string é armazenada).

Mas você pode perguntar: Como ela foi armazenada na memória, em primeiro lugar? Não houve MSTORE para escrever na memória, então por que a memória não está vazia?

A EVM (num passe de mágica) a colocou lá porque pedimos que o fizesse. Quando? Aqui: string memory _str.

Especificando memory no parâmetro, nós solicitamos à EVM que preparasse nossa memória e colocasse nosso parâmetro lá. Por isso usar calldata é mais barato. Não precisa gravar nada na memória 😉

Segunda técnica para criar hash para strings

// é o mesmo que`hashSecretWord1` mas usando uma técnica diferente

// aqui usamos opcodes específicos para manipular calldata em vez de usar os parâmetros da função

// em vez de retornar o hash, vamos atribuí-lo à variável de armazenamento `secretWord`

function hashSecretWord2(string calldata) external {

    assembly {

        // calldata representa todos os dados passados para um contrato ao chamar a função

        // os primeiros 4 bytes sempre representam a assinatura da função e o restante são os parâmetros 

        // aqui podemos pular a assinatura porque já estamos na função. Assim, a assinatura obviamente representa a função atual

        // podemos usar CALLDATALOAD para carregar 32 bytes do calldata.

        // usamos calldataload(4) para pular os bytes da assinatura. Isto irá, portanto, carregar o 1º parâmetro

        // ao usar tipos não-valor (array, mapping, bytes, string) o primeiro parâmetro será o offset (deslocamento) onde começa o parâmetro 

        // nesse offset encontraremos o comprimento do parâmetro e depois o valor

        // este é o offset em `calldata` onde começa a nossa string 

        // aqui usamos calldataload(4) -> carrega o offset onde começa a string

        // -> adicionamos 4 ao offset para levar em consideração os bytes da assinatura

        // https://www.evm.codes/#35

        let strOffset := add(4, calldataload(4))

        // usamos calldataload() de novo com o offset que acabamos de computar. Isso nos dá o comprimento da string (o valor armazenado no offset)

        let strSize := calldataload(strOffset)

        // carregamos o ponteiro de memória livre

        let ptr := mload(0x40)

        // copiamos o valor da nossa string para essa memória livre

        // CALLDATACOPY https://www.evm.codes/#37

        // a string começa no próximo slot de memória, então adicionamos 0x20 a ela

        calldatacopy(ptr, add(strOffset, 0x20), strSize)

        // então calculamos o hash dessa string

        // lembre, a string agora está armazenada no `ptr`

        let hash := keccak256(ptr, strSize)

        // nós a guardamos no armazenamento

        sstore(secretWord.slot, hash)

    }

}
Enter fullscreen mode Exit fullscreen mode

Não precisamos nem mesmo de um nome para nosso parâmetro, já que não vamos usá-lo explicitamente. E note que desta vez não precisamos dele em "memória", então especificamos "calldata" para que ele não seja copiado.

O opcode CALLDATALOAD carrega 32 bytes de calldata a partir do offset especificado. Nós o usamos aqui para carregar os valores 1 por 1.

Mais tipos não-valor

Preciso acrescentar algo à explicação anterior sobre os tipos não-valor. Calldata é apenas um pouco mais complicado do que o que eu lhes disse anteriormente. Foco! Isto não é fácil.

Vamos dar um exemplo de como ficaria a memória para uma função com 3 parâmetros:

function myToken(string memory name, uint randomValue, address[] memory _addresses)

nós passamos os seguintes parâmetros (pseudo aleatórios) 🙂

“angle”, 7, ["0x31429d1856aD1377A8A0079410B297e1a9e214c2", "0x1a7e4e63778B4f12a199C062f3eFdD288afCBce8"]

Esta é a aparência dos dados de chamada (calldata)

050eed260000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005616e676c65000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000031429d1856ad1377a8a0079410b297e1a9e214c20000000000000000000000001a7e4e63778b4f12a199c062f3efdd288afcbce8

Vamos dividir isso em palavras com 32 bytes para ter uma visão melhor.

Primeiro temos a assinatura da função: 050eed26 e depois os parâmetros:

offset (from 1st param) offset (calldata) value
0x00 (0) 0x04 (4) 0000000000000000000000000000000000000000000000000000000000000060
0x20 (32) 0x24 (36) 0000000000000000000000000000000000000000000000000000000000000007
0x40 (64) 0x44 (68) 00000000000000000000000000000000000000000000000000000000000000a0
0x60 (96) 0x64 (100) 0000000000000000000000000000000000000000000000000000000000000005
0x80 (128) 0x84 (132) 616e676c65000000000000000000000000000000000000000000000000000000
0xa0 (160) 0xa4 (164) 0000000000000000000000000000000000000000000000000000000000000002
0xc0 (192) 0xc4 (196) 00000000000000000000000031429d1856ad1377a8a0079410b297e1a9e214c2
0xe0 (224) 0xe4 (228) 0000000000000000000000001a7e4e63778b4f12a199c062f3efdd288afcbce8

Eu usei https://abi.hashex.org/ para codificar facilmente os dados de chamada.

Temos nossos 3 parâmetros de função, em ordem, mas codificados de uma maneira especial.

Comecei a numeração em 0, mas temos que lembrar de adicionar 4 ao offset para contabilizar a assinatura da função.

Para tipos valor, o valor é o do parâmetro. Veja em 0x20, temos 7 que é o valor que passamos para randomValue. Mas para tipos não-valor, nós, na verdade, obtemos o offset onde começam os dados. Veja em 0x00 que temos 0x60. Se verificarmos em 0x60, temos 5: o comprimento da string e logo após, em 0x80, temos a string “angle”.

O último parâmetro começa em 0x40 e novamente este é o offset. Então vamos verificar em 0xa0, temos 2 (o comprimento de nosso array) e depois os 2 valores em 0xc0 e 0xe0.

Legal! Finalmente sabemos como a EVM entende todos os tipos "estranhos" que podemos passar para ela! 🔥

De volta aos nossos dados de chamada, então.

Vamos apenas explicar melhor esta linha let strOffset := add(4, calldataload(4))

Usamos CALLDATALOAD para obter o primeiro parâmetro (no offset 4, lembre-se da assinatura da função…).

Isto retorna o offset onde o comprimento da string é armazenado. Por exemplo, se tomarmos o exemplo anterior, obteríamos 0x60 (o valor armazenado em 0x04 em calldata). Se adicionarmos 4 a ele, obtemos 0x64, que é o endereço onde obtemos nossa string nos dados de chamada.

Então usamos CALLDATACOPY para copiar a string para a memória. Criamos um hash para ela e a armazenamos. E terminamos 😎

Sim, esta foi uma explicação rápida para um código complicado, mas com tudo o que lhe expliquei até agora, vocês devem ser capazes de entendê-lo. Se não, falem comigo no Twitter @0xteddav e eu os ajudarei.

addMultipleGuesses: use todo seu conhecimento deYul

Por fim, vamos reutilizar tudo o que acabamos de aprender para percorrer 2 arrays em assembly e armazenar valores em um mapeamento no armazenamento! uau🤯

function addMultipleGuesses(address[] memory _users, uint[] memory _guesses) external {

        assembly {

            // lembre-se: `_users` é o endereço na memória onde começam os parâmetros

            // É aqui que o tamanho do array é armazenado. E depois de 32 bytes, temos os valores do array

            // então aqui nós carregamos o que está no endereço `_users` -> que é o tamanho do array`_users`

            let usersSize := mload(_users)

            // o mesmo para `_guesses`

            let guessesSize := mload(_guesses)

            // verificamos se ambos os arrays têm o mesmo tamanho

            // verifica os opcodes EQ, ISZERO and REVERT em https://www.evm.codes/

          if iszero(eq(usersSize, guessesSize)) { revert(0, 0) }

            // usamos um loop for para fazer loop dos itens

            for { let i := 0 } lt(i, usersSize) { i := add(i, 1) } {

                // para obter o enésimo valor de i do array, multiplicamos i por 32 (0x20) e o adicionamos a `_users`

                // sempre temos que adicionar 1 a i primeiro, porque lembre-se de que `_users` é o tamanho do array e os valores começam 32 bytes depois

                // também poderíamos fazê-lo desta forma (talvez faça mais sentido):

                // let userAddress := mload(add(add(_users, 0x20), mul(0x20, i)))

                let userAddress := mload(add(_users, mul(0x20, add(i, 1))))

                let userBalance := mload(add(_guesses, mul(0x20, add(i, 1))))

                // usamos o slot 0 de memória como armazenamento temporário para calcular nosso hash

                // armazenamos o endereço aqui

                mstore(0, userAddress)

                // depois, o número do slot para `guesses`

                mstore(0x20, guesses.slot)

                // calculamos o número do slot de armazenamento

                let slot := keccak256(0, 0x40)

                // e armazenamos nosso valor nele

                sstore(slot, userBalance)

            }

        }

    }
Enter fullscreen mode Exit fullscreen mode

Espero que os comentários sejam suficientemente claros. Vou apenas acrescentar algumas explicações.

Para verificar se ambos os arrays têm o mesmo tamanho, usamos os opcodes ISZERO e EQ. O eq() toma 2 números como parâmetros e retorna 1 se forem iguais, 0 se não forem iguais. Então, usamos simplesmente iszero() para verificar o valor retornado de eq(). Se eles não forem iguais, nós revertemos.

iszero(eq(a, b)) é equivalente a a != b em Solidity

Finalmente, o loop for: a linha para obter o valor do parâmetro é um pouco complicada. Vamos explicar isto:

let userAddress := mload(add(_users, mul(0x20, add(i, 1))))

Portanto, lembre-se que os endereços de nossos usuários (_users) são escritos na memória pela EVM.

A variável _users é o endereço na memória onde o comprimento do array é armazenado. Assim, para chegar ao primeiro valor do array, temos que adicionar 32 a ele.

Como estamos em um loop for, a cada iteração precisamos adicionar 32.

Assim, para obter o valor atual temos 32*i + 32 → 32 * (i + 1) → que em assembly é mul(0x20, add(i, 1))

Para facilitar mais (e obter maior eficiência na taxa de gas), poderíamos ter iniciado o loop em i=1 para evitar o add(i, 1).

Você conseguiu!

Então é isso. Acabamos! Escrevemos um contrato inteiro em assembly e até fizemos um pouco de trabalho extra (só por diversão!).

Você pode encontrar todo o código usado nesse tópico neste

Github Gist. Para que, se você for preguiçoso demais para reescrevê-lo, você possa simplesmente copiá-lo.

Se você tiver alguma dúvida, entre em contato no Twitter @0xteddav.

Deixe-me saber o que você quer que eu escreva a seguir. Devo ir mais fundo? Ou escrevo sobre outro assunto?

Jogando com Yul (série de 2 partes)

1Jogando com Yul (Assembly)

2Resolvendo o Ethernaut com

Esse artigo foi escrito por teddav e traduzido por Fátima Lima. O original pode ser lido aqui.

Top comments (0)