https://blog.angle.money/playing-with-yul-cd4785e456d8
Vamos entender os conceitos avançados do Armazenamento de Contratos usando Yul com a ajuda do seguinte código de exemplo.
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract StroageYul {
uint256 a = 1;
uint128 b = 2;
uint120 c = 3;
uint8 d = 4;
function readStorageSlot() public view
returns(
bytes32 valueAtSlotZero,
bytes32 valueAtSlotOne,
bytes32 valueAtSlotTwo,
uint256 slotB,
uint256 slotC,
uint256 slotD,
uint256 offsetB,
uint256 offsetC,
uint256 offsetD) {
assembly {
valueAtSlotZero := sload(0) // valor real do slot em bytes <- sload(NúmeroDoSlot)
valueAtSlotOne := sload(1) // sload(var_name.slot)
valueAtSlotTwo := sload(2)
slotB := b.slot // uint256 <- nome_var.slot (não o valor real)
slotC := c.slot
slotD := d.slot
offsetB := b.offset // uint256 <- nome_var.offset (não o valor real)
offsetC := c.offset
offsetD := d.offset
}
}
function getC_ValueBySlotAndOffset() external view returns (uint256 cValue) {
assembly {
let cSlotPackedBytes32:= sload(c.slot)
let cValueShiftR_Bits128 := shr(128, cSlotPackedBytes32)
cValue := and(0xff, cValueShiftR_Bits128)
}
}
Seção # 1 - .slot e .offset
Saída da função readStorageSlot():
As variáveis de estado b = 2, c = 3, d = 4 foram compactadas pelo Solidity no mesmo slot juntas (marcadas em vermelho).
Você pode ler mais sobre compactar variáveis de estado aqui.
.slotB := b.slot
O operador .slot é sufixado com o var_name como b para retornar o número de slot de armazenamento do contrato onde o valor real (que a variável b se refere) é armazenado.
Neste caso, todas as 3 variáveis são armazenadas no slot # 1 apenas.
MAS... o offset para todas as 3 varia.
Offset é retornado pelo operador .offset (consulte o código acima).
offsetB := b.offset
Mas, o que é o *offset *(deslocamento)?
Offset = número de bytes começando da esquerda em uma palavra de 32 bytes.
O Offset começa com o número 0, e não 1, e vai até 31, que é o último byte de 32.
b = 2 tem um offset = 0, pois está bem no início de nossa palavra de valor bytes32 no slot # 1.
c = 3 tem um offset = 16, pois o valor de b = 2 ocupa os primeiros 16 bytes (0–15) de espaço, sendo do tipo uint128 (128 bits = 16 bytes).
d = 4 tem um offset = 31 (último byte de 32) pois o valor de c = 3 ocupa 15 bytes de espaço, sendo do tipo uint120 (120 bits = 15 bytes).
Observe que tipos definidos pelo usuário / complexos / não elementares, como structs, mapeamentos, arrays, etc. que pegam ponteiros de armazenamento/referências de armazenamento dentro do corpo da função têm offset sempre zero, pois tais tipos não podem ser compactados com outras variáveis de armazenamento.
Seção # 2 - Deslocamento e Máscara Bitwise
Uma limitação ao usar _sload(slot#) _é que ele fornece um valor compactado em bytes para as variáveis de estado compactadas com diferentes valores de deslocamento no mesmo slot.
Não se pode obter o valor de uma única variável usando sload(slot#).
Se tentarmos executar o seguinte trecho de código para obter o valor de uma variável específica c dentro do slot, receberemos um erro.
function getCValueInSlot() external view returns (uint256 cValue) {
assembly {
cValue := sload(c)
}
}
No entanto, isso é possível usando Deslocamento e Máscara Bitwise.
function getC_ValueBySlotAndOffset() external view returns (uint256 cValue) {
assembly {
let cSlotPackedBytes32 := sload(c.slot)
let cValueShiftR_Bits128 := shr(128, cSlotPackedBytes32)
cValue := and(0xff, cValueShiftR_Bits128)
}
}
Aqui, estamos tentando obter o valor da variável c dentro do slot # 1.
Vou explicar cada passo.
(1). sload(c.slot)
cSlotPackedBytes32 = 0x04
000000000000000000000000000003
00000000000000000000000000000002
(2). shr(128, cSlotPackedBytes32)
Ele (bitwise) desloca o valor dos bytes em cSlotPackedBytes32 para o lado direito por 128 bits (16 bytes).
cValueShiftR_Bits128 = 0x0000000000000000000000000000000004
000000000000000000000000000003
Fiz uma pequena edição no código de exemplo acima para obter esse valor intermediário para mostrar a saída.
Por que deslocamos para a direita por 16 bytes?
Fizemos isso usando shr().
b = 2 ocupa os primeiros 16 bytes da esquerda para o início e depois vem c = 3.
Ao deslocar (primeiro) 16 bytes para a direita, trouxemos c = 3 exatamente no início à esquerda.
Além disso, observe como zeros extras foram preenchidos no lado direito de d = 4 e permanecerá assim, e isso é bom para nosso caso.
(3). and(0xff, cValueShiftR_Bits128)
Aqui vem o (bitwise) and.
Multiplicamos o valor deslocado em cValueShiftR_Bits128 com 0xff.
0xff & 0x0000000000000000000000000000000004
000000000000000000000000000003
= 0x000000000000000000000000000000000000000000000000000000000000003
Ele retorna 0x03 (observe atentamente, não há 4 no resultado).
É assim que mascaramos todos os valores, exceto o que estamos procurando, ou seja, 0x03.
Uma maneira mais fácil de entender a máscara é pensar em 0xff como 11 (Não onze), apenas UmUm.
Qualquer coisa que você multiplique por 1 será a mesma.
Usei 0xff para manter as coisas simples quando, idealmente, seria
0x0x0000000000000000000000000000000000000000000000000000000000000ff
Portanto, todos os dígitos, exceto os 2 mais à esquerda (03), são multiplicados por 0 e se tornam 0 e nos restam apenas:
0x000000000000000000000000000000000000000000000000000000000000003
Ufa...
Parece muito para assimilar.
Mas, não se apresse, releia OU, o melhor, execute o código no RemixIDE e veja você mesmo as saídas ajustando os valores de entrada.
Você certamente pegará o jeito.
Aprendizados diversos:
(1). Não é possível atribuir valores explicitamente ao slot e offset de uma Variável de Estado em Assembly.
function assignValues() external {
assembly {
c.slot := 1
c.offset := 16
}
}
(2). Uma advertência ao usar sstore():
function setValueBySlotNumberAndVal(uint256 slotNumber, uint256 value) external {
assembly {
sstore(slotNumber, value)
}
}
Esse trecho de código pode alterar os valores das Variáveis de Estado em todos os slots que se deseja ao interagir com a função setValueBySlotNumberAndVal().
Tenha MUITO cuidado ao usar tal código em seu Contrato Inteligente, pois isso pode levar a mudanças indesejadas nos valores das Variáveis de Estado e é por isso que geralmente não é recomendado, pois pode adicionar uma vulnerabilidade.
No entanto, ainda estou aprendendo mais sobre os casos de uso úteis de sstore() e continuarei atualizando esta seção ao longo do caminho.
Se você encontrar quaisquer fontes relevantes para aprender mais sobre isso, sinta-se à vontade para compartilhar.
Obrigado :)
Isso conclui a parte final.
E, como sempre, ficarei grato se você puder deixar um feedback honesto / reação e/ou feedback construtivo sobre este artigo para que eu possa melhorá-lo.
Até a próxima vez, boa codificação (em Solidity) :)
Bibliografia
https://docs.soliditylang.org/en/v0.8.17/yul.html#evm-dialect
Artigo original publicado por Manu Kapoor. Traduzido por Paulinho Giovannini.
Latest comments (0)