Los tipos de tamaño dinámico en Solidity (a veces denominados tipos complejos) son tipos de datos con tamaño variable. Incluyen mappings, mappings anidados, arrays, arrays anidados, strings, bytes y structs que contienen cualquiera de esos tipos. Este artículo muestra cómo se codifican y mantienen en el almacenamiento.
Mappings
Los mappings se utilizan para almacenar datos en forma de pares clave-valor.
Los valores de clave coloreados a continuación se mencionarán en un próximo bloque de código:

Considere este ejemplo que utiliza mappings para asociar una dirección de Ethereum con un valor. Los valores de clave rojo y verde, como se muestra en el diagrama anterior, se establecen en el siguiente código:
// SPDX-License-Identifier: MIT
pragma solidity =0.8.26;
contract MyMapping {
mapping(address => uint256) private balance; // storage slot 0
function setValues() public {
balance[address(0x01)] = 9; // RED
balance[address(0x03)] = 10; // GREEN
}
}
La función setValues mapea las direcciones 0x01 y 0x03 a 9 y 10 respectivamente, almacenándolas en la variable de mapping balance. Obtener el valor asignado a address(0x01) usando Solidity es sencillo. Pero, ¿qué ranura de almacenamiento está utilizando y cómo accedemos a ella con assembly?
Ranura de almacenamiento para mappings
Para calcular la ranura de almacenamiento del valor, seguimos los siguientes pasos:
- Concatenar la clave asociada con el valor y la ranura de almacenamiento de la variable del mapping (ranura base o base slot)
- Aplicar una función hash al resultado concatenado.
Fórmula para los pasos anteriores
donde significa concatenar
La siguiente animación muestra cómo se disponen los datos en la fórmula anterior:
Internamente, la clave y la ranura base se almacenan como valores de 256 bits (32 bytes). Cuando se concatenan, forman un valor de 64 bytes.
A continuación se muestra una animación de cómo se concatenan estos valores (clave y ranura base). Los valores utilizados son:
- address key =
0x504DbB5Dc821445b142312b74693d778a1B60b2f - uint256 baseSlot =
6
Observe cómo los valores de la clave y la ranura base primero se rellenaron con ceros hasta alcanzar valores de 32 bytes antes de concatenarse. El resultado de la concatenación (un array de 64 bytes) es a lo que se le aplica el hash para determinar la ranura de almacenamiento.
Calcular la ranura de almacenamiento del mapping
Ahora que tenemos una idea de cómo se calculan la clave y la ranura base para obtener la ranura de almacenamiento de un mapping, estamos listos para ver cómo se hace manualmente en Solidity.
Recuerde que necesitamos dos valores para calcular la ranura de un mapping (clave y ranura base). El código para lograr esto se encuentra en la función getStorageSlot():
contract MyMapping {
mapping(address => uint256) private balance; // storage slot 0
function setValues() public {
balance[address(0x01)] = 9; // RED
balance[address(0x03)] = 10; // GREEN
}
//*** NEWLY ADDED FUNCTION ***//
function getStorageSlot(address _key) public pure returns (bytes32 slot) {
uint256 balanceMappingSlot;
assembly {
// `.slot` returns the state variable (balance) location within the storage slots.
// In our case, balance.slot = 0
balanceMappingSlot := balance.slot
}
slot = keccak256(abi.encode(_key, balanceMappingSlot));
}
}
La función getStorageSlot toma _key como argumento y utiliza un bloque de assembly para obtener la ranura base (balanceMappingSlot) para la variable balance. Luego, utiliza abi.encode para rellenar cada valor a 32 bytes y concatenarlos, y después aplica un hash al valor concatenado utilizando keccak256 para producir la ranura de almacenamiento.
Para probar esto, llamemos a la función con address(0x01) como argumento, ya que previamente hemos asignado un valor a la ranura de almacenamiento asociada con esta _key en la función setValues.
La ranura devuelta tras la llamada: 0xada5013122d395ba3c54772283fb069b10426056ef8ca54750cb9bb552a59e7d

A continuación, creamos una función getValue() que cargará la ranura de almacenamiento que calculamos. Esta función sirve para demostrar que la ranura calculada por getStorageSlot() es, de hecho, la ranura de almacenamiento correcta que contiene dicho valor.
function getValue(address _key) public view returns (uint256 value) {
// CALL HELPER FUNCTION TO GET SLOT
bytes32 slot = getStorageSlot(_key);
assembly {
// Loads the value stored in the slot
value := sload(slot)
}
}
Llamar a la función getValue con address(1) como argumento devolvió 9, que es el valor correcto asignado a la clave address(1):

Aquí tiene el código completo para que lo pruebe en Remix.
// SPDX-License-Identifier: MIT
pragma solidity =0.8.26;
contract MyMapping {
mapping(address => uint256) private balance; // storage slot 0
function setValues() public {
balance[address(0x01)] = 9;
balance[address(0x03)] = 10;
}
function getStorageSlot(address _key) public pure returns (bytes32 slot) {
uint256 balanceMappingSlot;
assembly {
// `.slot` returns the state variable (balance) location within the storage slots.
// In our case, 0
balanceMappingSlot := balance.slot
}
slot = keccak256(abi.encode(_key, balanceMappingSlot));
}
function getValue(address _key) public view returns (uint256 value) {
// Call helper function to get
bytes32 slot = getStorageSlot(_key);
assembly {
// Loads the value stored in the slot
value := sload(slot)
}
}
}
Mappings anidados
Un mapping anidado es un mapping dentro de otro mapping. Un caso de uso común para esto es almacenar los saldos de diferentes tokens para una dirección específica, como se muestra en el diagrama a continuación.

Esto muestra que la variable balance contiene dos direcciones diferentes, 0xbob y 0xAlice; cada una de estas direcciones está asociada con múltiples tokens, que a su vez se mapean a diferentes saldos, de ahí los mappings anidados.
Ranura de almacenamiento para mappings anidados
El cálculo de las ranuras de almacenamiento para mappings anidados es similar al de los mappings simples, con la diferencia de que el “nivel” del mapping corresponde al número de operaciones hash. A continuación se muestra una animación y un ejemplo de código que demuestra un mapping de dos niveles con dos operaciones hash:
Ahora mostremos un ejemplo de código para obtener el valor de un array anidado desde el almacenamiento usando assembly
En la captura de pantalla a continuación, el valor 5 se asigna al mapping balance con las claves address(0xb0b) (propietario) y 1111 (tokenID), como se resalta en el recuadro amarillo. El contrato tiene dos funciones;
- La función
getStorageSlot, que toma dos argumentos que son las claves necesarias para derivar la ranura deseada. También ocurren dos operaciones hash en la función, como se ve en el recuadro rojo:- la primera es el hash de
_key1(propietario) y la ranura del mappingbalance, que luego se almacena en la variableinitialHash. - la segunda es el hash de
_key2(tokenID) einitialHash, para obtener la ranura debalance[_key1][_key2]. Si fuera un mapping de 3 niveles, la tercera clave (_key3) se sometería a hash con el valor de la segunda operación hash para obtener la ranura de almacenamiento deseada, y así sucesivamente.
- la primera es el hash de
- La función
getValue, que toma una ranura como argumento y devuelve el valor que contiene, comportándose igual que en el ejemplo anterior.

Llamar a la función getStorageSlot con los siguientes argumentos, address(0xb0b) y 1111, devuelve la siguiente ranura:
0x0b061f98898a826aef6fdfc2d8eb981af54b85700e4516b39466540f69aced0f

Para demostrar que la ranura calculada contiene el valor 5, llamaremos a la función getValue y pasaremos la ranura como argumento. Esta función utiliza el opcode sload para cargar la ranura y luego devolver su valor:

¡Y sí! Obtuvimos el mismo valor 5 que insertamos en el constructor.
Array
Este es un tipo dinámico en Solidity que se utiliza para almacenar una colección indexada de elementos del mismo tipo, ya sean primitivos o dinámicos. Solidity soporta dos tipos de arrays: de tamaño fijo y dinámicos, con diferentes métodos de asignación de almacenamiento.
Arrays de tamaño fijo
Este tipo de array tiene un tamaño predeterminado que no se puede cambiar después de que el array es declarado.
Asignación de ranuras para arrays de tamaño fijo
Si el tipo de elemento del array ocupa la capacidad de una ranura de almacenamiento (256 bits, 32 bytes o 1 palabra), el compilador de Solidity trata a estos elementos como variables de almacenamiento individuales, asignándoles ranuras de manera secuencial comenzando desde la ranura de la variable de almacenamiento del array.
Considere el siguiente contrato:
contract MyFixedUint256Array {
uint256 public num; // storage slot 0
uint256[3] public myArr = [
4, // storage slot 1
9, // storage slot 2
2 // storage slot 3
];
}
Dado que num es de tipo uint256 y es la primera variable de estado en el contrato, ocupa la totalidad de la ranura de almacenamiento 0. La segunda variable de estado, myArr, es un array de tamaño fijo de uint256 con tres elementos, lo que significa que cada elemento ocupará su propia ranura de almacenamiento, comenzando desde la ranura 1.
La siguiente animación muestra cómo se asignan las ranuras de almacenamiento a cada variable, detallando cómo se almacenan los valores de cada variable de almacenamiento en las ranuras.
Veamos otro ejemplo, similar al anterior, pero esta vez usando uint32 como el tipo de datos para el array:
contract MyFixedUint32Array {
uint256 public num; // storage slot 0
uint32[3] public myArr = [
4, // storage slot ???
9, // storage slot ???
2 // storage slot ???
];
}
Antes de seguir leyendo, ¿puede adivinar la ranura de almacenamiento para el tercer elemento del array? Si está pensando que podría ser la ranura 3, de manera similar al ejemplo anterior, es posible que desee reconsiderarlo.
Si el tipo de cada elemento del array no ocupa una ranura de almacenamiento completa, como el uint32 en este ejemplo, el compilador empaqueta múltiples elementos juntos dentro de una sola ranura hasta que se llena o hasta que no hay suficiente espacio para el siguiente elemento, antes de pasar a la siguiente ranura. Esto es similar a cómo el compilador empaqueta las variables de almacenamiento cuando individualmente no ocupan una ranura completa.
Cómo se asignan las ranuras a los valores empaquetados:
Nota: Acceder a un elemento empaquetado incurrirá en más gas ya que la EVM necesita agregar instrucciones adicionales aparte del sload habitual. Solo es aconsejable empaquetar sus elementos si generalmente se accede a ellos en la misma transacción y, por lo tanto, pueden compartir los costos de carga en frío.
Arrays dinámicos
A diferencia de los arrays de tamaño fijo que tienen su tamaño predeterminado en tiempo de compilación, los arrays dinámicos pueden cambiar de tamaño en tiempo de ejecución.
Asignación de ranuras para arrays dinámicos
Generalmente, los arrays dinámicos tienen su longitud almacenada en algún lugar ya que no se conoce en tiempo de compilación. Solidity sigue este principio almacenando la longitud de un array dinámico en una ranura de almacenamiento separada. A continuación se muestra cómo se asignan las ranuras tanto para la longitud como para el(los) elemento(s) de un array dinámico.
La ranura de almacenamiento asignada para la longitud del array es la misma ranura para la variable de almacenamiento del array (ranura base). A continuación se muestra un ejemplo que ilustra esto:

La variable myArr tiene tres elementos, lo que le da una longitud de 3. La función getSlotValue, como su nombre indica, toma un número de ranura y devuelve el valor almacenado en ella. En nuestro caso, pasamos la ranura 0 como argumento porque esa es la ranura asignada para la variable de almacenamiento myArr. Luego usamos el opcode sload para cargar el valor de la ranura.
Los valores del array se mantienen en las ranuras de almacenamiento de manera secuencial, siendo cada ranura de almacenamiento un índice en el array. La ranura de almacenamiento para el primer elemento (índice 0) está determinada por el hash keccak256 de la ranura de almacenamiento base (la ranura donde se declara la variable). La imagen a continuación ilustra esto.
El hash keccak de la ranura 2 apunta a la ranura que contiene el primer elemento, luego seguimos sumando 1 a ese valor para obtener las ubicaciones de almacenamiento de los otros índices en el array:

Las ranuras de almacenamiento están numeradas del 0 al 2²⁵⁶ - 1, y ese es exactamente el rango de valores que produce un keccak256. El primer valor en rojo en la imagen (0x405787...5ace) representa la ubicación de almacenamiento hasheada derivada de la ranura 2, que contiene el primer elemento del array. Cada valor posterior (0x405787...5acf, 0x405787...5ad0) es un incremento del anterior, correspondiendo al siguiente elemento en el array. Este patrón continúa para cada elemento adicional, con la ubicación de almacenamiento incrementándose secuencialmente según el tamaño del array.
Por ejemplo, considere un array de longitud 5 ubicado en la ranura de almacenamiento 2, que contiene los elementos [3, 4, 5, 9, 7] de tipo uint256:
contract MyDynArray {
uint256 private someNumber; // storage slot 0
address private someAddress; // storage slot 1
uint256[] private myArr = [3, 4, 5, 9, 7]; // storage slot 2
function getSlotValue(uint256 _index) public view returns (uint256 value) {
uint256 _slot = uint256(keccak256(abi.encode(2))) + _index;
assembly {
value := sload(_slot)
}
}
}
Para encontrar la ranura de almacenamiento que contiene el valor 9, primero aplicamos un hash a la ranura base (2) usando keccak256. Luego sumamos el índice del elemento (índice = 3) al valor hasheado. Este cálculo nos da la ranura de almacenamiento específica que contiene el valor 9. Por último, aplicamos sload al valor en el _slot obtenido.
Probar en Remix:

¿Qué sucede cuando los elementos no consumen todo el espacio de una ranura de almacenamiento?
Los elementos se empaquetan en ranuras de almacenamiento hasta que se llena el espacio disponible. Solo los tipos de 128 bits (16 bytes) o menores pueden ser empaquetados. Sin embargo, las direcciones, que ocupan 20 bytes cada una, no se empaquetan, ya que dos direcciones (40 bytes) superan el tamaño de una sola ranura de almacenamiento.
Cambiemos myArr para que use uint32 en lugar de uint256 en el contrato MyDynArray:
contract MyDynArray {
uint256 private someNumber; // storage slot 0
address private someAddress; // storage slot 1
uint32[] private myArr = [3, 4, 5, 9, 7]; // storage slot 2
function getSlotValue(uint256 _index) public view returns (bytes32 value) {
uint256 _slot = uint256(keccak256(abi.encode(2))) + _index;
assembly {
value := sload(_slot)
}
}
}
Se han realizado los siguientes cambios:
uint256[]⇒uint32[]: el tipo de datos para el array dinámico.uint256 value⇒bytes32 value: el valor de retorno, para que podamos ver fácilmente cómo se empaquetan los valores.
Cada elemento ocupa 4 bytes de los 32 bytes disponibles por ranura de almacenamiento. Con 5 elementos, el tamaño total es 4 * 5 = 20 bytes. Esto significa que todos los elementos pueden caber dentro de una sola ranura de almacenamiento, sobrando algo de espacio.
Probar en Remix:

El valor de retorno:

Array anidado
Un array anidado es un array que contiene otros arrays. Puede ser utilizado para representar datos tipo matriz, donde los elementos dentro de cada fila conforman un array, y la columna es un índice dentro de ese array.
La animación explicativa a continuación usa C para referirse a las columnas y R para referirse a las filas.
C ⇒ verde
R ⇒ rojo
Ranura de almacenamiento para arrays anidados de tamaño fijo
El compilador asigna ranuras para los elementos en un array anidado de tamaño fijo al igual que lo hace para un array de tamaño fijo regular. A cada elemento se le asigna una ranura de manera incremental, comenzando desde la ranura base, si ocupa una ranura entera. De lo contrario, se empaqueta junto con otros elementos hasta que se llena el espacio de la ranura.
Aquí hay una animación simple que ilustra cómo un array anidado de tamaño fijo almacena datos:
Ranura de almacenamiento para arrays anidados dinámicos
Como ya sabemos, los pasos para determinar la ranura de almacenamiento para un elemento específico en un array dinámico son los siguientes:
- aplicar un hash keccak a la ranura base
- luego sumar el índice del elemento al valor del hash
Para los arrays anidados dinámicos, el proceso implica repetir los pasos anteriores por cada nivel de anidamiento para encontrar la ranura final.
Supongamos que tenemos un array anidado de dos niveles, es decir, array(s) dentro de un array:
Los pasos para determinar la ranura de almacenamiento para el elemento f son:
- aplicar un hash keccak a la ranura base del array y luego sumar el índice del sub-array que contiene el elemento. En nuestro caso, el segundo sub-array.
- aplicar un hash keccak al resultado del paso 1 y luego sumar el índice del elemento
fdentro del sub-array.
Aquí hay una animación que ilustra los pasos anteriores:
Primero aplicamos un hash a la ranura base y sumamos el índice del sub-array (sub-array1, que es el índice 1 en el array base), lo que nos da el hash inicial (la ranura que contiene el sub-array). A continuación, aplicamos un hash a este hash inicial y sumamos el índice del elemento f (que es 2) dentro de sub-array1 para determinar la ranura final.
Un ejemplo práctico para obtener la ranura de un elemento en un array anidado dinámico de tipo uint256:
contract MyNestedArray {
uint256 private someNumber; // storage slot 0
// Initialize nested array
uint256[][] private a = [[2,9,6,3],[7,4,8,10]]; // storage slot 1
function getSlot(uint256 baseSlot, uint256 _index1, uint256 _index2) public pure returns (bytes32 _finalSlot) {
// keccak256(baseSlot) + _index1
uint256 _initialSlot = uint256(keccak256(abi.encode(baseSlot))) + _index1;
// keccak256(_initialSlot) + _index2
_finalSlot = bytes32(uint256(keccak256(abi.encode(_initialSlot))) + _index2);
}
function getSlotValue(uint256 _slot) public view returns (uint256 value) {
assembly {
value := sload(_slot)
}
}
}
Supongamos que queremos encontrar la ranura de almacenamiento que contiene el elemento 8 en el array [[2,9,6,3],[7,4,8,10]] del contrato anterior.
-
Necesitamos identificar tres cosas:
- la ranura base del array anidado
- el índice del sub-array que contiene el elemento,
- y el índice del elemento dentro de ese sub-array.
Estos índices son necesarios para obtener nuestra ranura deseada.
-
Llamamos a la función
getSlotpasando los valores parabaseSloty los índices:- baseSlot: la ranura para el array
a, que es la ranura 1. - _index1: el sub-array (
[7,4,8,10]) que contiene el elemento está en el índice 1. - _index2: el elemento
8dentro del sub-array está en el índice 2.
La ranura devuelta tras la llamada:
0xea7809e925a8989e20c901c4c1da82f0ba29b26797760d445a0ce4cf3c6fbd33 - baseSlot: la ranura para el array
-
Por último, llamamos a la función
getSlotValuepasando la ranura devuelta en el paso 2.
String
Los strings en Solidity son tipos dinámicos, lo que significa que no tienen una longitud fija. Algunos strings pueden caber dentro de una sola ranura de almacenamiento, mientras que otros pueden requerir múltiples ranuras.
Considere el siguiente contrato de ejemplo:
contract String {
string public myString;
uint256 public num;
}
La ranura de almacenamiento del string es 0 y la ranura de almacenamiento del uint256 es 1.
Si almacenamos un string corto en myString (uno que tenga menos de 32 bytes, luego discutiremos por qué un string de 32 bytes también se considera un string largo), podemos recuperarlo desde la ranura 0 sin ningún problema.
Sin embargo, si almacenamos un string más largo, digamos uno que ocupa 42 bytes, se desbordaría de la ranura 0 y sobrescribiría la ranura 1, que está reservada inicialmente para la variable num.
Esto sucede porque la ranura 0 no es lo suficientemente grande como para contener el string más largo. Para prevenir este problema, Solidity utiliza diferentes métodos para asignar ranuras de almacenamiento para los tipos string, dependiendo de la longitud del string.
Ranura de almacenamiento para strings
La ranura de la variable de almacenamiento (ranura base) almacena el string junto con la información sobre su longitud para strings cortos o solo la información sobre su longitud para strings largos, y estos casos se estudiarán en diferentes secciones a continuación.
String corto (≤ 31 bytes):
Los datos del string y su longitud se almacenan juntos en la ranura base. El string se empaqueta desde la izquierda, con su longitud almacenada en el byte más a la derecha de la ranura. Para strings cortos, la longitud máxima del string es de 31 caracteres. Sin embargo, lo que realmente almacena el protocolo es la longitud del string multiplicada por 2, ya que cada carácter ocupa un byte en el almacenamiento. Esto significa que el valor máximo que se puede almacenar para un string corto es 31 * 2 = 62, que es 0x3e en hexadecimal.
A continuación se muestra un ejemplo de un string corto Hello World en hexadecimal. Los ceros son espacio libre que se puede usar para almacenar un string más largo de hasta 31 bytes, y el último byte contiene (longitud del string) * 2.

Aquí 0x16 = 22 es 2 * 11, donde 11 es la longitud del string Hello World
String largo (> 31 bytes):
La (longitud del string * 2) + 1 (explicaremos la razón para sumar 1 en breve) se almacena en la ranura base, luego el string en hexadecimal se almacena en un espacio continuo de ranuras de almacenamiento. Los primeros 32 bytes de los datos del string se almacenan en el hash keccak256 de la ranura base. Los siguientes 32 bytes se almacenan en el hash de la ranura base más uno, y los siguientes en el hash más dos, y así sucesivamente, hasta que se almacena el string completo.
La siguiente animación muestra cómo se almacenan la longitud y un string largo (en hexadecimal) en las ranuras de almacenamiento:
Antes de que se almacene la longitud de un string largo, el compilador le suma uno (haciendo que pase de ser par a impar). Por ejemplo, el string en la animación anterior ocupa 47 bytes (32 + 15), lo que significa que su longitud es 47 * 2 = 94 (0x5e en hexadecimal). El compilador de Solidity luego suma 1 a esta longitud, convirtiéndola en 95 (0x5f en hexadecimal), y almacena este valor en la ranura base.
La razón de esto es permitir que el bytecode en tiempo de ejecución diferencie de manera eficiente entre strings cortos y largos. Para los strings cortos, la longitud es siempre par, por lo que el último bit del valor almacenado en la ranura base siempre será cero. Por otro lado, los strings largos (de 32 bytes o más) siempre tienen una longitud impar, lo que significa que el último bit siempre será uno.
Comprobación optimizada de par e impar
El método común en la mayoría de los lenguajes de programación para comprobar si un número es par o impar es utilizando el operador módulo (num % 2) y comprobando si el resto es 0. Esto también se aplica en Solidity. Sin embargo, una forma más optimizada es usar la operación AND a nivel de bits (bitwise AND): num & 1 == 0. A continuación se muestra un ejemplo de ambos métodos y sus respectivos costos:
contract ModMethod {
// Gas cost: 761
function isEven(uint256 num) public pure returns (bool x) {
x = (num % 2) == 0;
}
}
contract BitwiseAndMethod {
// Gas cost: 589
function isEven(uint256 num) public pure returns (bool x) {
x = (num & 1) == 0;
}
}
Obtener la longitud de un string
El tipo string en Solidity no tiene la propiedad de longitud (length). Esto se debe a que algunos caracteres, en particular los no ASCII, pueden ocupar más de un byte, por lo que rastrear cuántos caracteres hay cuando podrían tener un tamaño diferente crea demasiada sobrecarga. Sin embargo, podemos ver cuántos bytes ocupa el string haciendo un casting (conversión) a bytes, como muestra el siguiente ejemplo:

En text2, cada carácter ocupa 3 bytes, lo que suma 6 bytes. Para usar la propiedad length en un string, necesita convertir el string a bytes como en la captura de pantalla.
Bytes
Al igual que los strings, bytes es un tipo dinámico en Solidity y sigue el mismo conjunto de reglas para la asignación de ranuras.
- Bytes cortos (≤ 31 bytes): Se almacenan completamente en la ranura base, incluyendo su longitud (
número de bytes * 2). - Bytes largos (> 31 bytes): La ranura base almacena la longitud (
(número de bytes * 2) + 1), y los datos reales se almacenan en ranuras consecutivas comenzando desde el hashkeccak256de la ranura base.
Bytes de tamaño fijo
Son tipos utilizados para almacenar un número fijo de bytes. Estos tipos van desde bytes1 a bytes32, lo que significa que puede tener arrays de bytes de tamaño fijo que contengan entre 1 y 32 bytes.
Almacenar un valor que es mayor o menor que el tamaño en bytes utilizado lanzará un error en tiempo de compilación. En la imagen a continuación, a las variables value2 y value4 se les asignan valores que no corresponden a sus tamaños en bytes esperados, lo que resulta en un error de compilación.

Hemos utilizado bytes32 en la mayoría de nuestros ejemplos de código anteriores para contener los hashes de keccak256.
Acceder a un byte individual
Se puede acceder a un byte dentro de un array de bytes de tamaño fijo utilizando su índice.
Por ejemplo, el siguiente contrato accede al primer byte:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract FixedBytes {
bytes4 value = hex"01020304";
function accessFirstByte() public view returns (bytes1) {
bytes1 individualByte = value[0]; // Access the first byte
return individualByte; // Returns the first byte
}
}
La función accessFirstByte devuelve un solo byte (bytes1). Dentro de la función, value[0] accede al primer byte del array value. Luego, este byte es devuelto.
Antes de la versión 0.8.0 de Solidity, se usaba el tipo byte en lugar de bytes1 para representar un solo byte. En la versión 0.8.0 y superiores, bytes1 es ahora el tipo preferido para contener valores de un solo byte.
Comparación de string/bytes y bytes1[]
Ambos son tipos dinámicos que almacenan valores en bytes, y en ambos casos, se accede a los bytes por su índice. Sin embargo, la diferencia clave radica en cómo se almacenan los valores de los bytes.
Considere los siguientes contratos que almacenan el mismo valor en bytes tanto para el tipo bytes como para bytes1[]:
contract Bytes {
bytes foo_bytes = hex"ffeedd";
// helper to get slot value
function getSlotValue() public view returns (bytes32 x) {
assembly {
x := sload(0)
}
}
}
contract Bytes1Array {
bytes1[] bar_bytes = [bytes1(hex"ff"), bytes1(hex"ee"), bytes1(hex"dd")];
// helper to get slot value
function getSlotValue() public view returns (bytes32 x) {
bytes32 _slot = keccak256(abi.encode(0));
assembly {
x := sload(_slot)
}
}
}
Dado que el valor asignado a la variable foo_bytes es una secuencia corta de bytes (es decir, ≤ 31 bytes), tanto el valor como su longitud (número de bytes * 2) se almacenan en la misma ranura de almacenamiento (ranura base), de la siguiente manera:
Por otro lado, la variable bar_bytes, que es de tipo bytes1[] (un array dinámico), almacena la longitud del array y los valores en ranuras separadas:
La longitud se almacena en la ranura base:
Los valores se almacenan en el hash de la ranura base:
En otras palabras, el tipo bytes con una secuencia corta utiliza menos ranuras de almacenamiento que bytes1[]. Sin embargo, para secuencias de más de 31 bytes, el tipo bytes utiliza el mismo cálculo de ranuras que bytes1[], lo que resulta en el mismo número de ranuras utilizadas.
Otra diferencia entre bytes y bytes1[] es cómo se almacenan sus valores en las ranuras. Para foo_bytes, el valor entero se coloca en su(s) ranura(s) de una sola vez. En contraste, para bar_bytes, el primer elemento se almacena en el byte menos significativo, seguido por el siguiente elemento, y este patrón continúa hasta el último byte.
La animación muestra cómo los nuevos valores asignados a las variables foo_bytes y bar_bytes ocupan dos ranuras cada una (ranuras en verde y amarillo), con foo_bytes ocupando la ranura 0 y bar_bytes ocupando la ranura 1:
Struct
Los structs en Solidity nos permiten agrupar múltiples variables de diferentes tipos de datos bajo un solo nombre y usarlo como un nuevo tipo. Por ejemplo, si necesitamos un contrato para almacenar información del jugador como playerId, score y level, utilizar un struct sería la opción ideal. De esta manera, podemos agrupar todos los detalles relevantes de cada jugador en una estructura única y organizada.
Ranura de almacenamiento en un Struct
Un struct en Solidity actúa como un contenedor de variables, y la asignación de la ranura de almacenamiento para cada campo dentro de un struct sigue las mismas reglas que hemos discutido anteriormente.
Veamos un ejemplo:
contract MyStruct {
// Define a Player struct
struct Player {
address playerId;
uint256 score;
uint256 level;
}
uint256 private someNumber = 99;
/*
AFTER DIFFERENT DECLARATIONS ABOVE, THE NEXT AVAILABLE SLOT IS: 6
*/
// Declare a state variable of type Player
Player private thePlayer;
}
Sin ejecutar el código, ¿puede adivinar el valor en la ranura 0? Si piensa que es 99, está en lo correcto. Esto se debe a que definir un struct en Solidity no ocupa espacio en una ranura de almacenamiento hasta que se declara, por lo que el compilador ve la variable someNumber como la primera variable de almacenamiento.
Declarar una variable de tipo Struct Player:
Examinemos cómo funciona el almacenamiento con los structs, primero declarando un struct — esto hará que realmente ocupe almacenamiento. Tenga en cuenta que lo estamos declarando, no definiéndolo como hicimos antes.
/*
AFTER DIFFERENT DECLARATIONS ABOVE, THE NEXT AVAILABLE SLOT IS: 6
*/
// Declare a state variable of type Player
Player private thePlayer;
Los campos dentro del struct Player ocuparán tres ranuras de almacenamiento consecutivas, comenzando desde la ranura base al ser declarados. El campo playerId, que es de tipo address, usa 20 bytes de los 32 bytes disponibles en una ranura. Los campos score y level, al ser de tipo uint256, ocupan cada uno el espacio completo de una ranura de 256 bits (32 bytes). Con esto en mente, podemos decir que las ranuras de almacenamiento para los campos son 6, 7 y 8, respectivamente.
Asignación de ranuras de almacenamiento para tipos dinámicos dentro de un Struct
Otro ejemplo es tener tipos dinámicos dentro de un struct. Modifiquemos el ejemplo anterior para usar mappings y también asignarle algunos valores:
contract MyStruct {
// Define a Player struct
struct Player {
address playerId;
mapping(uint256 level => uint256 score) playerScore;
}
uint256 private someNumber = 23; // storage slot 0
uint256 private someNumber1 = 77; // storage slot 1
// Declare a state variable of type Player
Player private thePlayer;
constructor () {
// Set deployer's address as player's id
thePlayer.playerId = msg.sender;
// Set player's score to 100 for level 1 and 68 for level 2
thePlayer.playerScore[1] = 100;
thePlayer.playerScore[2] = 68;
}
}
Los pasos para calcular la ranura de almacenamiento de un valor en el mapping dentro del struct son:
- Identificar la ranura base del struct
thePlayer: esta ranura se determina cuando el struct es declarado en el contrato. - Calcular la ranura para el mapping
playerScoredentro del struct: esta ranura se determina por el orden en que el mapping es declarado en el struct. - Aplicar un hash a la concatenación de la clave y la ranura base del mapping, es decir, la ranura obtenida en el paso 2.
Conociendo estos pasos, podemos calcular la ranura de almacenamiento que contiene la puntuación del jugador para el nivel 2.
Paso 1: Identificar la ranura base de thePlayer
thePlayeres un struct declarado en el contrato anterior, y dado que se declara después de las variablessomeNumberysomeNumber1, su ranura base será la ranura2(porquesomeNumberocupa la ranura 0 ysomeNumber1ocupa la ranura 1).
Paso 2: Calcular la ranura para el mapping playerScore dentro del struct
// Define a Player struct
struct Player {
address playerId;
mapping(uint256 level => uint256 score) playerScore;
}
- Comenzando desde la ranura base del struct (ranura 2), a cada campo se le asigna una ranura de manera secuencial. Esto significa que el primer campo
playerIdde tipoaddress, ocupa la ranura 2 (ranura base), mientras que el segundo campo, el mappingplayerScore, se coloca en la siguiente ranura, que es la ranura3(la ranura base del mapping).
Paso 3: Aplicar un hash a la concatenación de la clave y la ranura base del mapping
-
Con la clave y la ranura base del mapping conocidas, podemos calcular nuestra ranura de almacenamiento objetivo concatenándolas y aplicándoles un hash.
La siguiente imagen muestra cómo se determina la ranura objetivo (recuadro verde) pasando la clave correcta (en este caso, el nivel
2) y la ranura base del mapping (3), y luego aplicandosloada la ranura objetivo:
En el recuadro azul, se encuentra el valor devuelto (la puntuación del jugador para el nivel 2) al aplicar sload a la ranura objetivo.