Pular para o conteúdo
0%

Como funciona o storage em Yul?

Mark Jonathas headshot

Escrito por Mark Jonathas

Publicado em 1 de agosto de 20234 min de leitura

Yul é uma linguagem de programação intermediária que pode ser usada para escrever uma forma de linguagem assembly dentro de smart contracts. Entender Yul pode elevar o nível dos seus smart contracts e ajudar a economizar gas dos usuários. Para começar a escrever smart contracts com Yul, é importante entender como funciona o storage.

Como funciona o storage no Yul?

Antes de nos aprofundarmos em como funciona o Yul, precisamos entender bem como funciona o storage em smart contracts. O storage é composto por uma série de slots. Existem 2²⁵⁶ slots para um smart contract.

Ao declarar variáveis, começamos no slot 0 e incrementamos a partir daí. Cada slot tem 256 bits de comprimento (32 bytes), e é daí que vêm os nomes uint256 e bytes32. Todas as variáveis são convertidas para hexadecimal.

Se uma variável, como um uint128, é usada, não ocupamos um slot inteiro para armazená-la. Em vez disso, ela é preenchida com 0's do lado esquerdo. Vamos ver um exemplo para entender melhor.

var1: Como variáveis uint256 equivalem a 32 bytes, var1 ocupa a totalidade do slot 0.

Aqui está o que é armazenado no slot 0:

0x0000000000000000000000000000000000000000000000000000000000000100

var2: Endereços são um pouco mais complexos. Como ocupam apenas 20 bytes de storage, endereços são preenchidos com 0's do lado esquerdo.

Aqui está o que é armazenado no slot 1:

0x0000000000000000000000009acc1d6aa9b846083e8a497a661853aae07f0f00.

var3: Este pode parecer simples, o slot 2 é consumido pela totalidade da variável bytes32.

var4 & var5: Lembra quando mencionei que uint128's são preenchidos com 0's? Bem, se ordenarmos nossas variáveis de forma que a soma do seu storage fique abaixo de 32 bytes, podemos encaixá-las juntas em um slot! Isso é chamado de packing de variáveis, e pode economizar gas.

Vamos ver o que é armazenado no slot 3:

0x0000000000000000000000000000000200000000000000000000000000000001. 

Perceba que 0x000000000000000000000000000002 e 0x000000000000000000000000000001 se encaixam perfeitamente juntos no mesmo slot. Isso acontece porque ambos ocupam 16 bytes (metade de um slot).

Instruction
Explanation

sload(p)

Carrega a variável no slot p a partir do storage.

sstore(p,v)

Atribui ao slot de storage p o valor v.

v.slot

Retorna o slot de storage da variável v.

v.offset

Retorna o índice em bytes de onde a variável v começa em um slot de storage. Variáveis são organizadas (packed) da direita para a esquerda.

Vamos ver outro exemplo!

x = 3. Isso faz sentido já que sabemos que var5 está armazenada no slot 3.

y = 16. Isso também deve fazer sentido já que sabemos que var4 ocupa metade do slot 3. Como as variáveis são organizadas da direita para a esquerda, obtemos o byte 16 como índice inicial de var5.

z = 1. O sstore() está atribuindo ao slot 0 o valor 1. Em seguida, atribuímos a z o valor do slot 0 com o sload().

Antes de continuarmos, você deve adicionar essa função ao seu arquivo do remix. Ela vai ajudar a visualizar o que está sendo armazenado em cada slot de storage.

Agora vamos olhar algumas estruturas de dados mais complexas!

Ao trabalhar com arrays estáticos, a EVM sabe quantos slots alocar para nossos dados. Neste array em particular, estamos organizando (packing) 2 elementos por slot.

Então, se você chamar getValInHex\(4\), isso retornará 0x0000000000000000000000000000000100000000000000000000000000000000.

Como era de se esperar, lendo da direita para a esquerda, vemos o valor 0 e o valor 1.

O slot 5 contém 0x0000000000000000000000000000000300000000000000000000000000000002.

A seguir, vamos olhar arrays dinâmicos.

Tente chamar getValInHex\(6\). Você verá que retorna 0x00. Como a EVM não sabe quantos slots de storage precisam ser alocados, não podemos armazenar o array aqui.

Em vez disso, o hash keccak256 do slot de storage atual (slot 6) é usado como índice inicial do array. A partir daí, tudo o que precisamos fazer é somar o índice do elemento desejado para recuperar o valor.

Aqui está um exemplo de código demonstrando como encontrar um elemento de um array dinâmico:

Aqui recuperamos o slot do array, e então realizamos uma operação add() junto com um sload() para obter o valor do elemento desejado do array.

Você pode estar se perguntando o que impede que ocorra uma colisão com o slot de outra variável?

Isso é totalmente possível, porém extremamente improvável, já que 2²⁵⁶ é um número muito grande.

Mappings se comportam de forma semelhante a arrays dinâmicos, exceto que fazemos o hash do slot junto com a chave.

Para esta demonstração, defini o valor do mapping var8[1] = 2. Agora vamos ver um exemplo de como obter o valor de uma chave de um mapping.

Como você pode ver, o código é bem parecido com quando encontramos um elemento de um array dinâmico. A principal diferença é que fazemos o hash da chave e do slot juntos.

A última parte da nossa seção sobre storage é aprender sobre mappings aninhados (nested). Antes de continuar a leitura, encorajo você a escrever sua própria implementação de como ler um valor de um mapa aninhado com base no que aprendeu até agora.

Para este exemplo, defini o valor do mapping var9[0][1] = 2.

Aqui está o código, vamos direto ao ponto!

Aqui está o que está acontecendo:

  1. Primeiro obtemos o hash da primeira chave (0).
  2. Em seguida, pegamos o hash disso com a segunda chave (1).
  3. Por fim, carregamos o slot do storage para obter nosso valor.

Parabéns, você concluiu a seção sobre storage com Yul!

Leia mais sobre Yul Memory, Yul Contract Calls, e Yul Packed Storage Variables.

Background gradient

Construa magia blockchain

A Alchemy combina os produtos e ferramentas de desenvolvimento Web3 mais poderosos com recursos, comunidade e suporte lendário.