Foto de Raymond Rasmusson no Unsplash.
Introdução
Quando comecei a aprender o básico do Solidity, tive dificuldade em entender a estrutura de armazenamento. Uma boa maneira de interpretá-la é como uma estrutura de dados de valor-chave que armazena variáveis de estado de contratos inteligentes.
Pensar no armazenamento como um array nos ajudará a entendê-lo melhor. O Solidity fornece 2²⁵⁶ slots (indexados de 0 a 2²⁵⁶- 1), cada um com comprimento fixo de 32 bytes.
slot[0] = data
slot[1] = data
slot[2] = data
.
.
.
slot[n] = data
Variáveis de estado são armazenadas de acordo com sua declaração de posição, comprimento e se são um valor ou um tipo dinâmico.
Este artigo tem como objetivo apresentar uma exploração concisa e não muito técnica do layout de armazenamento do Solidity, na esperança de que seja acessível e compreensível até mesmo para desenvolvedores iniciantes de contratos inteligentes.
Uma série de exemplos curtos e breves são apresentados para estudar diferentes cenários sobre como as variáveis de estado são armazenadas no armazenamento.
Se você quiser fazer os exemplos sozinho (o que eu recomendo totalmente), você pode usar o Remix IDE para implantar e usar o console para interagir com os contratos.
Layout de armazenamento para tipos de valor
Vamos estudar a série de exemplos a seguir:
Observação: todos os exemplos de contratos neste artigo são implantados na rede de teste Sepolia e os endereços são fornecidos para cada exemplo.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
// endereço do contrato: 0x511aC56388Bea7c42ECC3F80631f297B22045A93
contract StorageLayout {
uint256 num = 1; // vai para o slot 0
}
StorageLayout
declara uma única variável de estado num
do tipo uint256
. Esse tipo de dados contém 32 bytes, que é o comprimento máximo de um slot, portanto, esta variável ocupa o slot 0
.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
// endereço do contrato: 0xB36DdD815E90E0699BFaD90Ad1658F0f59715201
contract StorageLayout {
uint256 x = 1; // slot 0
uint256 y = 2; // slot 1
uint256 z = 3; // slot 2
}
Neste exemplo, são declaradas três variáveis do tipo uint256
, x
será armazenada no slot 0
porque é a primeira variável de estado declarada, y
irá para o slot 1
e z
para o slot 2
.
Até aí tudo fácil, certo?
Mas o que acontece quando as variáveis de estado são menores que 32 bytes?
O Solidity pode agrupar dados em um único slot, se eles couberem. Vejamos o próximo exemplo:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
// endereço do contrato: 0xeBa088B4182EC4261FA4fd2526F58995Dc1Ec117
contract StorageLayout {
uint16 x = 1;
uint16 y = 2;
uint16 z = 3;
}
StorageLayout
declara três variáveis de estado do tipo uint16
, este tipo pode ser representado com 2 bytes. Vamos usar o método web3.eth.getStorageAt(contractAddress, slotPosition)
da biblioteca web3.js para ler o conteúdo do slot 0. Para utilizar esse método, precisamos passar como parâmetros o endereço do contrato que queremos consultar e a posição do slot que queremos acessar. O método retornará o conteúdo do slot em uma representação de 32 bytes.
Vamos resolver este exemplo com o Remix:
- Crie um novo arquivo com o contrato e compile.
- Conecte a Metamask à rede de teste Sepolia:
Imagem 1. Remix IDE.
- Agora implante o contrato com o botão “Implantar” (Deploy). Após a implantação do contrato, use o terminal para interagir com o contrato:
Imagem 2. Remix IDE.
Executar o comando no terminal retornará:
slot[0] = 0x0000000000000000000000000000000000000000000000000000**000300020001**
Como você pode ver, as três variáveis de estado foram compactadas juntas da direita para a esquerda em um único slot, o que aconteceu porque variáveis do tipo uint16
não podem preencher o slot inteiro. Para preencher o slot, o Solidity deixou os dados preenchidos para 32 bytes com zeros.
Todas as variáveis de estado no Solidity são codificadas em ABI e, ao recuperar seus valores, são decodificadas automaticamente.
Podemos decodificar cada saída usando o método web3.eth.abi.decodeParameter(TYPE, DATA)
, por exemplo:
web3.eth.abi.decodeParameter("uint16", "0x0000000000000000000000000000000000000000000000000000000000000001")
Este comando retornará 1
.
Vejamos outro exemplo:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
// endereço do contrato: 0xa5418D2de1c56fdde47aCdBDeCE927a241B45d8F
contract StorageLayout {
bool status = true;
address addr = 0xCc8188e984b4C392091043CAa73D227Ef5e0d0a7;
}
Se implantarmos isso na rede de teste Sepolia e executarmos os mesmos comandos do exemplo anterior para ler o armazenamento, obteremos:
slot[0] = 0x0000000000000000000000cc8188e984b4c392091043caa73d227ef5e0d0a7**01**
address
é um tipo integrado exclusivo no Solidity, que tem um comprimento de 20 bytes, os booleanos podem ser representados com um byte - 0x00
para false
e 0x01
para true
.
Da direita para a esquerda, o primeiro byte (em negrito) representa o boolean
status
(estado booleano) com valor 0x01
. Os próximos 20 bytes representam o endereço e o restante dos dados são preenchidos com zeros para completar os 32 bytes.
Vamos dar uma olhada neste exemplo final para tipos de valor:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
// endereço do contrato: 0x92e8d7602b86Fa66F579F2aDE30Fd0493791C4f9
contract StorageLayout {
uint16 x = 1;
uint256 y = 2;
uint16 z = 3;
}
O layout de armazenamento para esse exemplo é:
slot[0] = 0x00000000000000000000000000000000000000000000000000000000000000**01**
slot[1] = 0x00000000000000000000000000000000000000000000000000000000000000**02**
slot[2] = 0x00000000000000000000000000000000000000000000000000000000000000**03**
Neste caso, o Solidity não pode empacotar as variáveis em um único slot como antes porque y
é do tipo uint256
, que está entre x
e z
e preenche sozinho todo o slot 1
. Por isso, cada variável será armazenada em slots individuais.
Layout de armazenamento para tipos dinâmicos
Os tipos dinâmicos podem mudar dinamicamente de tamanho, aumentando ou diminuindo a quantidade de dados que contêm. Por esse motivo, os elementos desses tipos não podem ser armazenados sequencialmente entre outras variáveis de estado da mesma maneira que o Solidity armazena tipos de valor.
Arrays
Vamos estudar o seguinte contrato:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
// endereço do contrato: 0xf9AaF13CefB378d22672578719E0fa5114Fc1484
contract StorageLayout {
bool private status = true; // slot 0
uint256[] private numArray = [1, 2, 3, 4, 5]; // o slot 1 contém o comprimento do array
address private z = 0xCc8188e984b4C392091043CAa73D227Ef5e0d0a7; // slot 2
function getSlotForArrayElement(uint256 _elementIndex) public pure returns (bytes32) {
bytes32 startingSlotForArrayElements = keccak256(abi.encode(1));
return bytes32(uint256(startingSlotForArrayElements) + _elementIndex);
}
}
Se verificarmos os dados armazenados nos slots 0
e 2
após a implantação deste contrato, saberemos que encontraremos status
e z
, respectivamente, de acordo com as regras de armazenamento para tipos de valor.
Se acessarmos o conteúdo do slot 1
com o seguinte comando:
web3.eth.getStorageAt("0xf9AaF13CefB378d22672578719E0fa5114Fc1484", "1")
Obteremos:
0x0000000000000000000000000000000000000000000000000000000000000005
O array contém 5 elementos, mas a única coisa que obtivemos foi um único byte (0x05), que representa o comprimento do array.
Para arrays, o Solidity armazena o comprimento do array no slot em que o array foi declarado, os dados que o array contém são armazenados em outros slots. Para determinar o slot no qual os dados do array estão armazenados, precisamos obter o keccak256
do índice da declaração do slot do array. Vamos simplificar isso com a seguinte fórmula:
keccak256(abi.encode(ARRAY_SLOT_DECLARATION))
numArray
foi declarado no slot 1
e apenas o comprimento do array é armazenado lá. Agora, vamos aplicar a fórmula acima para calcular o slot a partir do qual os dados do array serão armazenados, executando a função getArraySlotForElement
e passando 0
como parâmetro, isso retornará:
// representação do índice do slot em hexadecimal
0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
Podemos converter esse valor hexadecimal em decimal para obter o seguinte grande número:
// índice de slot a partir do qual os dados de `numArray` serão armazenados
80084422859880547211683076133703299733277748156566366325829078699459944778998
Agora, se lermos o conteúdo desse slot, obteremos:
0x0000000000000000000000000000000000000000000000000000000000000001
Como você pode ver, acessamos o primeiro elemento do array, que é 1
.
Se você executar getArraySlotElement
e passar o índice do restante dos elementos do array, você notará que os elementos são armazenados sequencialmente. Uma vez que o Solidity determina o slot inicial, os slots para cada um dos elementos do array são:
// para o elemento`1` no índice 0 do array, o slot é:
8008442285988054721168307613370329973327774815656636632582907869945994477899**8**
// para o elemento`2` no índice 1 do array , o slot é:
8008442285988054721168307613370329973327774815656636632582907869945994477899**9**
// para o elemento`3` no índice 2 do array, o slot é:
8008442285988054721168307613370329973327774815656636632582907869945994477900**0**
// para o elemento`4` no índice 3 do array, o slot é:
8008442285988054721168307613370329973327774815656636632582907869945994477900**1**
// para o elemento`5` no índice 4 do array, o slot é:
8008442285988054721168307613370329973327774815656636632582907869945994477900**2**
Mapeamentos
Assim como nos arrays, os elementos dos mapeamentos não são armazenados sequencialmente, o slot no qual mapping
é declarado não contém nenhuma informação, pois os mapeamentos não têm comprimento. A fórmula para determinar o slot no qual cada elemento de um mapping
será armazenado é a seguinte:
keccak256(abi.encode(KEY, SLOT_INDEX_DECLARATION))
Essa fórmula calcula o hash da concatenação da chave com o slot no qual o mapping
foi declarado. O slot do mapeamento atua como salt para evitar que os elementos do mapeamento substituam os slots de outros mapeamentos.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
// endereço do contrato: 0x2eD8B9aAAba437253fC3B2D9fDf04fac99dDc208
contract StorageLayout {
mapping(address => uint256) private addressToBalance;
uint8 private constant mappingSlotIndex = 0;
constructor() {
addressToBalance[0x5B38Da6a701c568545dCfcB03FcB875f56beddC4] = 123;
addressToBalance[0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2] = 456;
addressToBalance[0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db] = 789;
}
function getMappingElementSlotIndex(address _key) public pure returns (bytes32) {
return keccak256(abi.encode(_key, mappingSlotIndex));
}
}
O construtor deste contrato inicializa addressToBalance
com três endereços e algum saldo. getMappingElementSlotIndex
pega uma chave do mapeamento e calcula o slot no qual o valor mapeado para essa chave está armazenado.
Se tivermos interesse em saber o slot para o endereço 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, podemos chamar getMappingElementSlotIndex
e com o endereço e ele retornará:
0x58f8e73c330daffe64653449eb9a999c1162911d5129dd8193c7233d46ade2d5
Se lermos os dados nesse slot:
web3.eth.getStorageAt("0x2eD8B9aAAba437253fC3B2D9fDf04fac99dDc208","0x58f8e73c330daffe64653449eb9a999c1162911d5129dd8193c7233d46ade2d5")
Nós obteremos:
0x000000000000000000000000000000000000000000000000000000000000007b
Se transformarmos essa string hexadecimal em decimal, obteremos 123
.
Os slots para os outros dois endereços são:
// para 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 o slot é:
0x1a1017a437881fd8fee8ab135586d886995df9286bd91e5d3c250f79b2327f02
// para 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db o slot é:
0xbc67542bfa83c3e43faa1ce49daa83c7bb0610df1c8f6899b8fbb170f5c183ee
Se compararmos os três slots, podemos verificar que os dados não são armazenados sequencialmente, na verdade, eles estão muito distantes um do outro.
Vamos calcular para ver quantos slots separam 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 de 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db:
// subtrai os valores em hexadecimal:
bc67542bfa83c3e43faa1ce49daa83c7bb0610df1c8f6899b8fbb170f5c183ee
- 1a1017a437881fd8fee8ab135586d886995df9286bd91e5d3c250f79b2327f02
// resultado da subtração em hexadecimal:
A2573C87C2FBA000000000000000000000000000000000000000000000000000
// para decimal:
73428814930032578957074960535759132369224003477876394282500466786575899951104 // slots
Ufa! Eles estão longe um do outro!
Agora podemos concluir que, diferentemente dos arrays, os dados nos mapeamentos não são armazenados sequencialmente.
Agora vamos estudar o caso de ter 2 mapeamentos declarados com o mesmo tipo de chave.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// endereço do contrato: 0x94850369761233deD50Ab12E1F1fcDC0AfbddF7D
contract StorageLayout {
mapping(address => uint256) private addressToBalance1;
mapping(address => uint256) private addressToBalance2;
uint8 private constant mappingSlotIndex1 = 0;
uint8 private constant mappingSlotIndex2 = 1;
constructor() {
addressToBalance1[0x5B38Da6a701c568545dCfcB03FcB875f56beddC4] = 123;
addressToBalance1[0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2] = 456;
addressToBalance1[0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db] = 789;
addressToBalance2[0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB] = 789;
addressToBalance2[0x617F2E2fD72FD9D5503197092aC168c91465E7f2] = 456;
addressToBalance2[0x17F6AD8Ef982297579C203069C1DbfFE4348c372] = 123;
}
function getMappingElementSlotIndex1(address _key) public pure returns (bytes32) {
return keccak256(abi.encode(_key, mappingSlotIndex1));
}
function getMappingElementSlotIndex2(address _key) public pure returns (bytes32) {
return keccak256(abi.encode(_key, mappingSlotIndex2));
}
}
Neste caso, podemos perceber a importância de calcular o slot com o hash da concatenação da chave e o slot onde o mapeamento foi declarado. As funções hash são determinísticas - isso significa que, para a mesma entrada, a função hash gerará sempre a mesma saída, sem que a declaração do slot de mapeamento atue, pois os mapeamentos salt que compartilham a mesma chave substituirão os dados de outros mapeamentos.
Chamar getMappingElementSlotIndex1
para 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 retorna:
0x58f8e73c330daffe64653449eb9a999c1162911d5129dd8193c7233d46ade2d5
Mas chamar getMappingElementSlotIndex2
para a mesma chave retorna:
0x36306db541fd1551fd93a60031e8a8c89d69ddef41d6249f5fdc265dbc8fffa2
Os slots são completamente diferentes, o salt adicionado garante que os dados não serão sobrescritos por mapeamentos com as mesmas chaves.
Caso os valores dos mapeamentos sejam outros mapeamentos ou arrays de arrays, as regras explicadas antes se aplicam recursivamente.
Strings e Bytes
Strings e bytes codificados e armazenados no armazenamento acontecem exatamente da mesma maneira. Portanto, abordarei nesta seção apenas string
.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// endereço do contrato: 0xb2e0a6814389eF64257FFD01b9Da263d585D7790
contract StorageLayout {
string public name = "Pacelli";
}
Implantamos este contrato e acessamos o slot 0
:
web3.eth.getStorageAt("0xb2e0a6814389eF64257FFD01b9Da263d585D7790", "0")
E obtemos:
0x506163656c6c690000000000000000000000000000000000000000000000000e
O resultado é interessante, vemos dois conjuntos de bytes que representam dados separados por zeros.
No lado direito, temos 0x0e
, que representa o comprimento da string. Se convertermos esse valor hexadecimal para decimal obtemos 14
bytes.
No lado esquerdo, vemos os dados reais, 0x506163656c6c69, e, se contarmos os bytes, a soma totaliza 14. Converta esse valor hexadecimal em texto e obteremos Pacelli
.
Podemos concluir que se a string
e o length
(comprimento) da string couberem no mesmo slot, o Solidity os empacotará juntos. Mas o que acontece se a string
e o comprimento não couberem no mesmo slot? Nesse caso, as regras para arrays se aplicam a strings, da mesma forma que o comprimento da string
é armazenado na declaração do slot e os dados são divididos em pedaços de 32 bytes armazenados sequencialmente.
Vejamos o seguinte contrato:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// endereço do contrato: 0x05b34B6c48129e5Df2Fc36A1162B2dedB7D751a3
contract StorageLayout {
string public text = "No mês passado esqueci de pagar meu serviço de telefonia móvel e o provedor quase cortou meu serviço :(, lixo lixo lixo lixo teste teste teste, hoje é sábado, está chovendo muito e esqueci meu guarda-chuva.";
bytes32 public startingSlotString = keccak256(abi.encode(0));
function getStartingSlotForString(uint256 _index) public view returns (bytes32) {
return bytes32(uint256(startingSlotString) + _index);
}
}
No slot 0
obtemos:
web3.eth.getStorageAt("0x2180EA94D776CF9F9Cc1526d86A9c4036b0fC25b", "0")
// retorna:
0x0000000000000000000000000000000000000000000000000000000000000199
0x0199
é o comprimento da string, em decimal, o valor é 409
bytes.
Agora vamos ler nos slots:
// no slot 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e56**3** obtemos:
0x4c617374206d6f6e7468204920666f72676f7420746f20706179206d79206d6f
// em texto:
Mês passado eu esqueci de pagar meu ser
// no slot 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e56**4** obtemos:
0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564
// em texto:
viço de telefonia móvel e o prove
... e assim por diante
Para continuar lendo o resto do texto, basta continuar adicionando um.
Structs
Para variáveis de estado struct
, o índice do slot onde a struct
é declarada é reservado para o primeiro elemento, o próximo slot para o segundo e assim por diante.
O próximo contrato declara uma struct
Car
, a declaração de tipo não ocupa um slot no armazenamento. Em seguida, um novo Car
é criado: Car(Toyota, 2012, ABCDEF, sedan, 10000)
.
// SPDX-License-Idenfitier: MIT
pragma solidity ^0.8.0;
// endereço do contrato: 0xe3A9d8B1d576D50B0B4f3B7Ae76A6b729881eEF9
contract StorageLayout {
struct Car {
string brand;
uint256 year;
uint256 price;
bool isSold;
}
Car public car = Car({brand: "Toyota", year: 2012, price: 10000, isSold: true});
}
Se acessarmos os slots:
web3.eth.getStorageAt("0xe3A9d8B1d576D50B0B4f3B7Ae76A6b729881eEF9", "0")
web3.eth.getStorageAt("0xe3A9d8B1d576D50B0B4f3B7Ae76A6b729881eEF9", "1")
web3.eth.getStorageAt("0xe3A9d8B1d576D50B0B4f3B7Ae76A6b729881eEF9", "2")
web3.eth.getStorageAt("0xe3A9d8B1d576D50B0B4f3B7Ae76A6b729881eEF9", "3")
Obteremos:
slot[0] = 0x546f796f7461000000000000000000000000000000000000000000000000000c
slot[1] = 0x00000000000000000000000000000000000000000000000000000000000007dc
slot[2] = 0x0000000000000000000000000000000000000000000000000000000000002710
slot[3] = 0x0000000000000000000000000000000000000000000000000000000000000001
Como esperado, no slot 0
obtemos a marca brand
, no slot 1
obtemos o ano year
, no slot 2
obtemos o preço e, finalmente, no slot 3
obtemos se está vendido isSold
.
O empacotamento também ocorre em estruturas se as variáveis couberem no mesmo slot.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// endereço do contrato: 0xf2137C5c3Ed626B1462F3Ac3E8145a38b86E3049
contract StorageLayout {
struct Values {
uint24 value1;
uint24 value2;
uint24 value3;
uint24 value4;
}
Values public values = Values(10, 20, 30, 40);
}
Se lermos o slot 0
, obteremos:
slot[0] = 0x000000000000000000000000000000000000000000002800001e00001400000a
Da direita para a esquerda: 0x0a
é 10, 0x14
é 20, 0x1e
é 30 e 0x28
é 40.
Se os elementos de uma struct
forem do tipo dinâmico, as regras discutidas anteriormente se aplicam da mesma maneira.
Conclusão
Compreender o layout de armazenamento no Solidity é importante porque ajuda na eficiência do espaço e pode reduzir os custos de gás de implantação e execução.
Dependendo dos valores que nossas variáveis de estado irão conter, precisamos selecionar corretamente os tipos uintN
e bytesN
e declará-las na ordem correta para que sejam empacotadas juntas. Desta forma, podemos acessar múltiplas variáveis em uma única chamada.
Outra coisa a dizer é que, na maioria dos exemplos, as variáveis de estado foram declaradas com visibilidade private
(privada). Há um equívoco comum de que variáveis private
não são acessíveis ou são secretas, mas vimos que, usando uma biblioteca como web3.js
com seu método web3.eth.getStorageAt
, podemos ler qualquer slot de armazenamento para acessar dados independentemente da visibilidade. Todos os dados armazenados em uma blockchain pública como a Ethereum são acessíveis a qualquer pessoa, portanto, nunca armazene informações confidenciais, a menos que tenham sido criptografadas.
Leitura adicional
Este artigo foi escrito por Eugenio Pacelli Flores Voitier e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.
Oldest comments (0)