12 min read

Manual de Referencia de Yul/Assembly

El Manual de Referencia, Parte I/IV, es una guía completa para entender Yul, el lenguaje de ensamblador de la EVM. Cubre desde operaciones básicas hasta manejo avanzado de almacenamiento, todo respaldado por ejemplos y explicaciones detalladas.
Manual de Referencia de Yul/Assembly
Portada para "Manual de Referencia de Yul/Assembly"

(Parte I/IV)

Introducción

Este manual reducido está diseñado para personas que aspiran a convertirse en expertos en Solidity. Si ya tienes experiencia en programación en C o C++, o mejor aún, en otra versión de ensamblador, este curso será más fácil para ti. Yo no tengo ni idea ni de C o C++, y menos de ensamblador... aunque parece que decir esa frase venda más, ¿No o qué? Bienvenido! :D

¿Por qué aprender Yul/Assembly?

  1. Comprensión Profunda: Aunque puedes ser un desarrollador exitoso en Solidity sin entender el ensamblador, aprenderlo te da una comprensión más profunda del tema.
  2. Riesgo y Conocimiento: Warren Buffett dijo que el riesgo proviene de no entender lo que estás haciendo. Esto es especialmente cierto en la programación de contratos inteligentes.
  3. Identificación de Brechas de Conocimiento: Programar en ensamblador te expone a las brechas en tu conocimiento y te obliga a llenarlas.
  4. Optimización de Gas: Aunque el compilador de Solidity es eficiente, hay circunstancias en las que usar ensamblador resulta en un código más pequeño y eficiente.
  5. Actualizaciones de Ethereum: Cuando Ethereum pasa por una actualización importante, las nuevas API suelen estar disponibles en ensamblador antes que en Solidity.

Advertencias

  • Programar en ensamblador puede ser peligroso; no lo incluyas en contratos de producción de inmediato.
  • Este curso es avanzado y no cubre temas básicos como el alcance de variables, bucles, funciones, etc.

Metodología del manual

  • El manual es extremadamente rápido y denso.
  • Se asume que ya eres un programador competente.

¿Qué es Yul?

Yul es un lenguaje de representación que sirve como un paso intermedio entre el código de alto nivel y el bytecode que se ejecuta en la Máquina Virtual de Ethereum (EVM). Este último es la salida del compilador de Solidity y es lo que se almacena en la blockchain. Aunque la terminología sobre lo que constituye el ensamblador en Solidity puede ser confusa, Yul sirve para inyectar instrucciones a un nivel más bajo de abstracción lo que permite interactuar más directamente con la EVM.

Lo más destacable de Yul es que:

  • No tiene variables de almacenamiento.
  • No maneja memoria por ti.
  • No tiene arrays / matrices.
  • No descompone argumentos de función para ti.

Por lo que es el usuario el que debe programarlo todo perfectamente paso por paso.


Tipos de datos en Yul

Yul tiene un solo tipo de dato: la palabra de 32 bytes o 256 bits, que es similar a lo que estás acostumbrado a ver en Solidity.

Retornar un Número en Solidity y Yul

En Solidity, podrías hacer algo como esto para retornar un número:

pragma solidity 0.8.17;

contract YulTypes {
    function getNumber() external pure returns (uint256) {
        return 38;
    }
}

En Yul, el mismo código se vería así:

pragma solidity 0.8.17;

contract YulTypes {
    function getNumber() external pure returns (uint256) {
        uint256 x;
        assembly {
            x := 38
        }
        return x;
    }
}

Observaciones:

  • Yul no utiliza punto y coma (;) al final de las instrucciones.
  • La asignación de variables en Yul se hace con :=

Usar Hexadecimales en Yul

function getHex() external pure returns (uint256) {
    uint256 x;
    assembly {
        x := 0xa
    }
    return x;
}

Este contrato devolverá 10 porque 0xa es 10 en decimal. Si compilamos el contrato y llamamos a la función getHex() el resultado va a ser "10" por la razón de equivalencia aunque Solidity interpreta que es un decimal cuando está definido como uint256.

Trabajando con Cadenas de Texto

En Yul, no puedes asignar directamente una cadena de texto a una variable de tipo string aunque puedes hacerlo con bytes32:

function demoString() external pure returns (string memory) {
    bytes32 myString = "";
    assembly {
        myString := "Hello this is Pepe from https://pepe.ghost.io!"
    }
    return string(abi.encode(myString));
}

En este ejemplo usamos la función string(abi.encode(myString)) en el return para ver el resultado myString en formato texto, sino lo que veríamos al compilar sería un hash largo representando ese string. Hay que tener en cuenta de no poner un string de más de 32 bytes, ya que si nos pasamos de 1 nos va a dar error el compilador: "String literal too long". Solidity interpreta los datos de manera diferente según el tipo de dato que declares, aunque en Yul todo es una palabra de 32 bytes.

Usar Diferentes Tipos de Datos

Estos siguientes son los ejemplo tanto si usamos un valor tipo boolean como retorno o un uint16. Aunque no se indique el último bit es 1, por lo tanto al compilar nos dará un resultado de "true" para el tipo boolean y "1" para el uint16. Para comprobar que el último bit es 1 podemos compilar este ejemplo usando un tipo address:

function representation() external pure returns (address) {
    address x;
    assembly {
        x := 1
    }
    return x;
}

Este contrato devolverá una dirección de Ethereum donde todo es cero excepto el último bit, que es uno.


Operaciones Básicas en Yul

En esta sección exploraremos las operaciones básicas que puedes realizar en Yul. Aunque Yul es un lenguaje de bajo nivel, ofrece una serie de operaciones que son fundamentales para cualquier lenguaje de programación.

¿Por qué es importante conocer las operaciones básicas en Yul?

  • Optimización de Código: Conocer las operaciones básicas te permite escribir código más eficiente.
  • Flexibilidad: Entender cómo funcionan estas operaciones te da más control sobre tu código.
  • Depuración: Cuando algo va mal, entender las operaciones básicas puede ayudarte a encontrar y corregir errores más rápidamente.

Operaciones Aritméticas

Yul ofrece operaciones aritméticas básicas como suma, resta, multiplicación y división.

Operaciones Aritméticas Básicas
// Suma en Yul
assembly {
    let a := add(1, 2)  // a = 3
}

// Resta en Yul
assembly {
    let b := sub(5, 2)  // b = 3
}

// Multiplicación en Yul
assembly {
    let c := mul(2, 3)  // c = 6
}

// División en Yul
assembly {
    let d := div(8, 2)  // d = 4
}

Operaciones de Comparación

Yul también ofrece operaciones de comparación como menor que (lt), mayor que (gt), etc.

Operaciones de Comparación
// Menor que en Yul
assembly {
    let e := lt(2, 3)  // e = true
}

// Mayor que en Yul
assembly {
    let f := gt(3, 2)  // f = true
}

Operaciones Lógicas

Yul incluye operaciones lógicas como AND (and), OR (or), y XOR (xor).

Operaciones Lógicas
// AND lógico en Yul
assembly {
    let g := and(1, 1)  // g = 1
}

// OR lógico en Yul
assembly {
    let h := or(1, 0)  // h = 1
}

// XOR lógico en Yul
assembly {
    let i := xor(1, 1)  // i = 0
}

True / False en Yul

Verificar Verdad y Falsedad
// Código en Solidity para verificar verdad y falsedad
pragma solidity ^0.8.0;
contract Ejemplo {
    function isTrue(uint256 x) external pure returns (bool result) {
        assembly {
            result := iszero(iszero(x))
        }
    }
    
    function isFalse(uint256 x) external pure returns (bool result) {
        assembly {
            result := iszero(x)
        }
    }
}

Estructuras de Control en Yul

Yul tiene bucles for y sentencias if, lo que lo hace un poco más versátil que el ensamblador tradicional.

Bucles For y Sentencias If
// Código en Solidity para verificar si un número es primo
pragma solidity ^0.8.0;
contract Ejemplo {
    function isPrime(uint256 x) external pure returns (bool) {
        uint256 halfX = x / 2 + 1;
        for (uint256 i = 2; i < halfX; i++) {
            if (x % i == 0) {
                return false;
            }
        }
        return true;
    }
}

// El mismo código en Yul
pragma solidity ^0.8.0;
contract Ejemplo {
    function isPrime(uint256 x) external pure returns (bool result) {
        assembly {
            let halfX := add(div(x, 2), 1)
            for { let i := 2 } lt(i, halfX) { i := add(i, 1) } {
                if eq(mod(x, i), 0) {
                    result := 0
                    leave
                }
            }
            result := 1
        }
    }
}

Limitaciones de las Sentencias If en Yul

Yul no tiene sentencias else. Si necesitas manejar múltiples escenarios, debes verificarlos explícitamente.

Ejemplo 5: Encontrar el Máximo de Dos Números
// Código en Yul para encontrar el máximo de dos números
assembly {
    let x := 5
    let y := 10
    let max := x
    if gt(y, x) {
        max := y
    }
}

Las operaciones básicas en Yul son fundamentales para cualquier tipo de desarrollo en este lenguaje. Aunque son básicas, ofrecen una gran cantidad de control y flexibilidad, lo que las hace indispensables para cualquier desarrollador de Yul.


Manejo de Variables de Almacenamiento en Yul

Variables de Almacenamiento Tradicionales en Solidity

En Solidity, una variable de almacenamiento tradicional se declara en el nivel del contrato y es accesible a través de todo el contrato. Además, Solidity proporciona métodos getter y setter para acceder y modificar estas variables.

Variable de Almacenamiento Tradicional en Solidity
pragma solidity ^0.8.0;

contract Ejemplo {
    uint256 public x;

    function setX(uint256 newVal) public {
        x = newVal;
    }
}

Acceso a Variables de Almacenamiento en Yul

En Yul, las instrucciones sload y sstore se utilizan para leer y escribir variables de almacenamiento, respectivamente.

Leer una Variable de Almacenamiento en Yul
pragma solidity ^0.8.0;

contract Ejemplo {
    uint256 public x;

    function getXInYul() external view returns (uint256) {
        uint256 result;
        assembly {
            result := sload(x.slot)
        }
        return result;
    }
}

Explicación Detallada

sload: Esta instrucción carga el valor almacenado en una ubicación de memoria específica. En el ejemplo, sload(x.slot) lee el valor de la variable de almacenamiento x.

sstore: Esta instrucción almacena un nuevo valor en una ubicación de memoria específica. Aunque no se muestra en el ejemplo, sería algo así como sstore(x.slot, nuevoValor) para establecer un nuevo valor para x.

variable.slot: Esta es una propiedad especial generada por el compilador de Solidity que devuelve la ubicación de memoria de la variable de almacenamiento. En el ejemplo, x.slot devuelve la ubicación de memoria de x.

Observaciones

sload y sstore son instrucciones de bajo nivel que interactúan directamente con el almacenamiento de Ethereum. Deben usarse con cuidado para evitar errores y vulnerabilidades.

variable.slot es específico de Solidity y se utiliza para obtener la ubicación de almacenamiento de una variable cuando se trabaja con Yul.

Variables Empaquetadas

En Solidity, si tienes variables de almacenamiento más pequeñas que 32 bytes, se empaquetan juntas en el mismo slot de almacenamiento.

Variables Empaquetadas

pragma solidity ^0.8.0;

contract Ejemplo {
    uint8 public a;
    uint8 public b;

    function getSlot() external pure returns (uint256) {
        uint256 slot;
        assembly {
            slot := a.slot
        }
        return slot;
    }
}

Observaciones:

  • a y b comparten el mismo slot de almacenamiento.
  • Para acceder a a o b, se necesitarían operaciones de desplazamiento de bits y máscaras.

Manipulación de Bits y Máscaras en Yul

Variables Empaquetadas y Desplazamiento de Bits

En situaciones donde múltiples variables están almacenadas en un mismo slot de almacenamiento, es necesario utilizar operaciones de desplazamiento de bits y máscaras de bits para acceder a una variable específica.

Leer una Variable Empaquetada
pragma solidity ^0.8.0;

contract Ejemplo {
    uint8 public a;
    uint8 public b;
    uint8 public c;
    uint8 public d;

    function readE() external view returns (uint16) {
        uint16 result;
        assembly {
            let value := sload(a.slot)
            let shifted := shr(mul(28, 8), value)
            result := and(shifted, 0xFFFF)
        }
        return result;
    }
}

Explicación Detallada

sload(a.slot): Esta instrucción carga el valor del slot de almacenamiento donde se encuentra la variable a.

shr(mul(28, 8), value): Aquí, primero multiplicamos 28 por 8 utilizando mul. Luego, desplazamos los bits del valor cargado hacia la derecha utilizando shr. El resultado es almacenado en la variable shifted.

and(shifted, 0xFFFF): Finalmente, aplicamos una máscara de bits utilizando la operación and para obtener solo los bits que nos interesan. El resultado se almacena en la variable result.

Observaciones

shr (shift right): Esta operación desplaza los bits de un número hacia la derecha. Es útil para extraer bits específicos de un número.

and: Esta es una operación AND bit a bit que se utiliza para aplicar una máscara de bits a un número.

mul: Esta es una operación de multiplicación que se utiliza para calcular el número de bits que se deben desplazar.

Escritura de Variables Empaquetadas en Yul

La escritura de variables que son más pequeñas que 32 bytes es un desafío en la Máquina Virtual de Ethereum (EVM), ya que solo se pueden escribir datos en incrementos de 32 bytes. En esta sección, aprenderemos cómo manejar este desafío en Yul.

Escribir una Variable Empaquetada
pragma solidity ^0.8.0;

contract Ejemplo {
    uint8 public a;
    uint8 public b;
    uint8 public c;
    uint8 public d;

    function writeE(uint16 newE) public {
        assembly {
            let slot := a.slot
            let original := sload(slot)
            let cleared := and(original, not(0xFF << (28 * 8)))
            let shiftedNewE := shl(mul(28, 8), newE)
            let updated := or(cleared, shiftedNewE)
            sstore(slot, updated)
        }
    }
}

Explicación Detallada

let slot := a.slot: Aquí, obtenemos el slot de almacenamiento de la variable a.

let original := sload(slot): Cargamos el valor original del slot de almacenamiento en la variable original.

let cleared := and(original, not(0xFF << (28 * 8))): Limpiamos los bits que queremos actualizar utilizando la operación and y not.

let shiftedNewE := shl(mul(28, 8), newE): Desplazamos los bits del nuevo valor newE hacia la izquierda utilizando shl.

let updated := or(cleared, shiftedNewE): Combinamos los bits limpios con los nuevos bits desplazados utilizando la operación or.

sstore(slot, updated): Finalmente, almacenamos el valor actualizado en el slot de almacenamiento original.

Observaciones

shl (shift left): Esta operación desplaza los bits de un número hacia la izquierda.

or: Esta es una operación OR bit a bit que se utiliza para combinar dos números.

not: Esta es una operación NOT bit a bit que se utiliza para invertir los bits de un número.

sstore: Esta instrucción almacena un nuevo valor en un slot de almacenamiento específico.

Resumen

Operaciones de Desplazamiento de Bits y Máscaras de Bits: Estas operaciones son fundamentales para manipular variables de almacenamiento empaquetadas en Yul. Nos permiten acceder, limpiar y actualizar bits específicos dentro de un slot de almacenamiento.

Limitaciones de la EVM: La escritura de variables que son más pequeñas que 32 bytes es un desafío en la Máquina Virtual de Ethereum (EVM). Esto requiere un enfoque especial que hemos detallado en esta sección.

Eficiencia en Términos de Gas: Las operaciones de desplazamiento de bits (shl, shr) son más eficientes en términos de gas que las operaciones de división y multiplicación cuando se utilizan con el mismo propósito. Esto es especialmente relevante para la optimización del contrato en un entorno como Ethereum, donde el gas es un recurso valioso.


Cálculo de Slots de Almacenamiento en Solidity para Arrays y Mappings

Vamos a explorar cómo Solidity calcula los slots de almacenamiento para estructuras de datos complejas como arrays y mappings. Este conocimiento es crucial para optimizar el uso del almacenamiento en la blockchain y para interactuar eficientemente a nivel de ensamblador.

Arrays Fijos

Los arrays fijos en Solidity se comportan de manera bastante convencional cuando se trata de almacenamiento. Cada elemento del array se almacena en un slot de almacenamiento consecutivo.

uint256[3] fixedArray = [1, 2, 3];

En este caso, el primer elemento (1) se almacenará en el slot x, el segundo (2) en el slot x+1, y así sucesivamente.

Arrays Dinámicos

Los arrays dinámicos son más complejos en términos de almacenamiento. El tamaño del array se almacena en un slot, y los elementos del array se almacenan a partir del hash keccak256 del slot.

uint256[] dynamicArray = [1, 2, 3];

Aquí, el tamaño del array (3) se almacenará en un slot, digamos y. Los elementos del array se almacenarán en los slots keccak256(y), keccak256(y) + 1, etc.

Mappings

Los mappings son aún más complejos en términos de almacenamiento. Utilizan una función hash para determinar la ubicación de almacenamiento de un valor en función de su clave.

mapping(uint256 => uint256) myMapping;
myMapping[1] = 1;

El valor 1 se almacenará en el slot keccak256(1 + keccak256(slot del mapping)).

Entender cómo Solidity maneja el almacenamiento de arrays y mappings es crucial para escribir contratos eficientes y seguros. Este conocimiento es especialmente útil cuando se trabaja a nivel de ensamblador para optimizaciones.


Conclusión final y mirada al futuro

Estimado lector,

Espero que este Manual de Referencia, parte I de IV, te haya sido de gran utilidad para introducirte en el apasionante mundo de Yul y la programación de contratos inteligentes en la Máquina Virtual de Ethereum (EVM). Este manual es el primero de una serie de cuatro partes que he diseñado para proporcionarte una comprensión completa y detallada de cómo trabajar con Yul y Solidity en un nivel más avanzado.

¿Por Qué Este Manual?

La razón detrás de la creación de este manual es simple: quiero ofrecer una fuente de referencia sólida y accesible para la comunidad de habla española interesada en profundizar en la programación de contratos inteligentes. Sé que el camino hacia el dominio de cualquier lenguaje de programación es largo y, a veces, complejo. Por eso, he tratado de hacer este recorrido más fácil y enriquecedor para ti.

¿Qué Viene Después?

En las próximas entregas de este manual, abordaré temas aún más avanzados y específicos. Estos incluirán:

  • Manejo de Operaciones en la Memoria: Cómo gestionar eficientemente la memoria en la EVM.
  • Llamadas entre Contratos: Entenderás cómo interactuar entre diferentes contratos inteligentes de manera segura y eficiente.
  • Desarrollo de un Token ERC-20: En la última parte, pondremos en práctica todo lo aprendido para desarrollar un token ERC-20 desde cero.

Palabras Finales

Si sientes que no puedes esperar para aprender más, te animo a buscar en otras fuentes y a practicar por tu cuenta. La mejor manera de aprender es haciendo, y cada desafío superado es un paso más hacia el dominio del arte de la programación de contratos inteligentes. Aquí dejo algunos enlaces de interés por si surje la ocasión.

👉🏻 Documentación de Yul

👉🏻 The University of Texas - Yul on Yul

👉🏻 Alchemy - Yul language

Gracias por acompañarme en esta primera parte del viaje. Espero que te unas a mí en las próximas entregas para continuar explorando las profundidades de Yul y Solidity.

Hasta entonces, ¡feliz programación!


Agradecimiento y Cómo Puedes Apoyar

Es un placer para mí compartir mis investigaciones, conocimientos, experiencias y pensamientos sobre blockchain y tecnología en este espacio. Si sientes que el contenido que ofrezco te ha sido útil y quieres contribuir a que este proyecto siga creciendo, estaré eternamente agradecido por tu apoyo.

Tu generosidad no solo valida el esfuerzo que pongo en la educación y divulgación en este campo, sino que también me da el impulso para seguir investigando, aprendiendo y compartiendo con todos vosotros.

Direcciones para Contribuciones:

  • Ethereum (ETH): 0x05B6a65F4a384Da41457C60cDCEb52c1cB847F79
  • Bitcoin (BTC): bc1qfe32clv6xjagv89095gdk36df08tj396zt3hws

Si te ha gustado lo que has leído y quieres estar al día con futuros contenidos, no olvides suscribirte a mi newsletter. No cuesta nada y es una excelente manera de mantenerse informado. Puedes hacerlo enviando un correo a [tu email aquí].

Gracias de corazón por siquiera considerar la posibilidad de apoyar mi trabajo. Cada contribución, sin importar su tamaño, es un estímulo para continuar en este camino. Y recuerda, si tienes alguna pregunta o comentario, no dudes en enviarme un correo. Tu feedback es invaluable para mí.

Si este manual o cualquier otro contenido te ha sido de ayuda, y te gustaría ver más de esto, considera hacer una donación o simplemente suscribirte a mi newsletter. Tu apoyo significa el mundo para mí y me anima a seguir compartiendo.

Con gratitud, Pepe.

Back cover para "Manual de Referencia de Yul/Assembly"