608060405260405160893803806089833981016040819052601e916025565b600055603d565b600060208284031215603657600080fd5b5051919050565b603f80604a6000396000f3fe6080604052600080fdfea26469706673582212204a131c1478e0e7bb29267fd8f6d38a660b40a25888982bd6618b720d4498b6b464736f6c634300080700330000000000000000000000000000000000000000000000000000000000000001
Este artículo explica qué sucede a nivel de bytecode cuando se construye un smart contract en Ethereum y cómo se interpretan los argumentos del constructor.
Tabla de contenidos
Discutiremos los siguientes temas con ejemplos visuales:
- Introducción
- Init code
- Contrato con constructor payable
- Contrato con constructor non-payable
- Runtime code
- Desglose del runtime code
- Constructor con parámetros
Introducción
A un alto nivel, la wallet que despliega el contrato envía una transacción a la dirección nula (null address) con los datos de la transacción estructurados en tres partes:
<init code> <runtime code> <constructor parameters>
Juntos se denominan código de creación (creation code). La EVM comienza ejecutando el init code. Si el init code está codificado correctamente, esta ejecución almacenará el runtime code en la blockchain.
No hay nada en la especificación de la EVM que indique que la estructura deba ser init code, runtime code y parámetros del constructor. Podría ser init code, parámetros del constructor y luego runtime code. Esta es simplemente la convención que utiliza Solidity. Sin embargo, el init code debe ser la primera parte para que la EVM sepa dónde comenzar a ejecutar.
Requisitos previos
Este artículo asume conocimientos sobre los siguientes temas:
- Solidity (Consulta nuestro tutorial gratuito de Solidity si recién estás empezando).
- Conceptos básicos de los opcodes de la EVM
¡Empecemos!
Autoría
Este artículo fue coescrito por Michael Amadi (LinkedIn, Twitter) como parte del RareSkills Technical Writing Program.
creationCode en Solidity
Solidity tiene un mecanismo para obtener el bytecode que se desplegará durante la transacción de creación del smart contract mediante la palabra clave creationCode. Esto se demuestra a continuación.
Esto no incluye los argumentos del constructor, los cuales se incluirán como parte del bytecode ejecutado durante el despliegue del contrato. Cómo se estructuran el init code (creationCode) y los argumentos se explica en este artículo.
contract ValueStorage {
uint256 public value;
constructor(uint256 value_) {
value = value_;
}
}
contract GetCreationCode {
function get() external returns (bytes memory creationCode) {
creationCode = type(Simple).creationCode;
}
}
Init code
El init code es el fragmento del creation code responsable de desplegar un contrato. Veamos el smart contract más simple posible. Más adelante explicaremos por qué añadimos un constructor payable.
Contrato con constructor payable
pragma solidity 0.8.17;// optimizer: 200 runscontract Minimal {
constructor() payable {
}
}
Para obtener el resultado de la compilación, podemos copiar el campo “input” desde Remix después de ejecutar la transacción de despliegue.

extraer el bytecode de creación del contrato
Cuando copiamos el campo resaltado, obtenemos
0x6080604052603f8060116000396000f3fe6080604052600080fdfea2646970667358221220d03248cf82928931c158551724bebac67e407e6f3f324f930c4cf1c36e16328764736f6c63430008110033
Esto es, por supuesto, bastante difícil de leer. Sin embargo, podemos dividirlo en dos partes.

Podría parecer que dividimos el bytecode en un lugar aleatorio, pero se explicará más claramente más adelante.
Si copiamos y pegamos la primera parte en evm.codes y convertimos el bytecode a mnemónicos, obtenemos el siguiente resultado. Se han añadido comentarios.
// allocate free memory pointer
PUSH1 0x80
PUSH1 0x40
MSTORE
// length of the runtime code
PUSH1 0x3f
DUP1
// where the runtime code begins
PUSH1 0x11
PUSH1 0x00// copy the runtime code from calldata into memory
CODECOPY
// runtime code is deployed at this step
PUSH1 0x00
RETURN
INVALID
La sección de código resaltada en la imagen, referida como el runtime code, tiene un tamaño de 63 bytes (0x3f en hexadecimal). Comienza en el índice 17 (0x11 en hexadecimal) en memoria. Esto explica de dónde provienen los valores de 0x3f y 0x11 en el desglose de mnemónicos anterior.
A un alto nivel, ocurren las siguientes tres acciones dentro de este init code:
- Se asigna el puntero de memoria libre (free memory pointer), el cual rastrea la siguiente ubicación de memoria disponible para escritura.
- Luego, el runtime code se copia en esta ubicación de memoria utilizando el opcode CODECOPY.
- Finalmente, la región de memoria que contiene el runtime code se devuelve a la EVM, la cual lo almacena como el runtime bytecode del nuevo contrato.
Contrato con constructor non-payable
pragma solidity 0.8.17;// optimizer: 200 runs
contract Minimal {
constructor() {
}
}
Veamos el bytecode cuando el constructor no es payable y observemos las diferencias. Este es el resultado del compilador.
6080604052348015600f57600080fd5b50603f80601d6000396000f3fe6080604052600080fdfea2646970667358221220a6271a05446e269126897aea62fd14e86be796da8d741df53bdefd75ceb4703564736f6c63430008070033
Desglosando esto en los códigos init y runtime, tenemos

Pongamos el init code payable y non-payable uno al lado del otro.
0x6080604052603f8060116000396000f3fe // payable
0x6080604052348015600f57600080fd5b50603f80601d6000396000f3fe // non-payable
Podemos notar que el init code del contrato payable es más pequeño que el del non-payable. A continuación explicamos por qué.
Al pegar la secuencia más larga (non-payable) en evm.codes, obtenemos el siguiente resultado, con comentarios añadidos.
// initialize free memory pointer
PUSH1 0x80
PUSH1 0x40
MSTORE
// check the amount of wei that was sent
CALLVALUE
DUP1
ISZERO
// Jump to 0x0f (contract deployment step)
PUSH1 0x0f
JUMPI
// revert if wei sent is greater than 0
PUSH1 0x00
DUP1
REVERT
// Jump dest (0x0f)
JUMPDEST
POP
// length of the runtime code
PUSH1 0x3f
DUP1
// where the runtime code begins
PUSH1 0x1d
PUSH1 0x00
CODECOPY
PUSH1 0x00
RETURN
INVALID
Para explicar lo que está sucediendo arriba, explicamos y utilizamos estos conceptos:
Diferencia entre constructores payable y non-payable
1. El init code se revierte si callvalue > 0, de lo contrario, el código continuará su ejecución.
El constructor non-payable tiene una secuencia de bytes adicional de 348015600f57 600080fd 5b50 (12 bytes) entre la inicialización del puntero de memoria libre y la devolución del runtime code en el contrato non-payable.
<init bytecode> <extra 12 byte sequence (payable case)> <return runtime bytecode> <runtime bytecode>
Este código adicional verifica que durante el despliegue no se envíe ningún valor (wei) (secuencia 348015600f57) y se revierte en caso contrario (secuencia 600080fd). Los dos últimos bytes 5b50 son opcodes JUMPDEST y POP que inician la secuencia de despliegue descrita anteriormente si no se envió wei.
(La razón por la que hay un POP es porque el callvalue todavía está en el stack y ya no lo necesitamos. Un JUMPDEST es simplemente un objetivo para JUMPs y JUMPIs. Sin él en una ubicación de salto especificada, los JUMPs no pueden aterrizar y se revertirán).
2. Los desplazamientos de memoria (offsets) para el runtime code están desplazados
También ten en cuenta que la longitud del runtime code no cambia, pero el offset para copiar el runtime code sí cambia porque el init code es más largo, lo que desplaza el offset del runtime code más hacia abajo.
El offset non-payable para el init bytecode es 0x1d, y el offset para el caso payable es menor, 0x11. Si los restamos (0x1d - 0x11 = 0x0c, 12 en decimal) obtenemos el tamaño de la secuencia de bytes adicional que verifica si hay un valor distinto de cero entre el bloque de inicialización del puntero de memoria libre y la secuencia donde se devuelve el runtime bytecode.
Runtime code para un contrato vacío
El runtime code no está vacío en un contrato vacío debido a los metadatos que añade el compilador
El runtime code es el fragmento del creation code que es devuelto por el init code y se establece como el bytecode del contrato al que los usuarios pueden llamar después del despliegue. Se convierte en lo que conocemos como el “smart contract”.
Surge una pregunta: “si el contrato está vacío (no tiene funciones), ¿por qué el runtime code no está vacío?”
El compilador de Solidity añade algunos metadatos sobre tu contrato al runtime code. Más información sobre los metadatos de contratos aquí. El opcode fe INVALID se antepone a los metadatos para evitar que se ejecuten.
(La nueva versión de Solidity 0.8.18 añade una configuración del compilador --no-cbor-metadata donde puedes indicarle al compilador que no adjunte estos metadatos al bytecode de tu contrato).
En un contrato de Yul puro, el compilador no añade metadatos por defecto
Si el contrato estuviera escrito en Yul puro, no habría metadatos. Sin embargo, la sección de metadatos se puede añadir incluyendo .metadata en el objeto global.
// the output of the compilation of this contract// will have no metadata by default
object "Simple" {
code {
datacopy(0, dataoffset("runtime"), datasize("runtime"))
return(0, datasize("runtime"))
}
object "runtime" {
code {
mstore(0x00, 2)
return(0x00, 0x20)
}
}
}
El resultado del compilador es el siguiente 6000600d60003960006000f3fe y al convertirlo a mnemónicos, obtenemos
// copy runtime code to memory
PUSH1 00
PUSH1 0d
PUSH1 00
CODECOPY
// Returning a zero sized region because there is no runtime code
PUSH1 00
PUSH1 00
RETURN
INVALID
En este caso, el área en memoria devuelta es cero, porque no hay runtime code ni metadatos.
(El compilador comienza en 0x0d y copia 0x00 bytes de runtime code en la memoria empezando en el offset 0x00. Luego devuelve 0x00 bytes).
Runtime code para un contrato no vacío
Ahora añadamos la lógica más simple posible al contrato.
pragma solidity 0.8.7;contract Runtime {
address lastSender;
constructor () payable {}
receive() external payable {
lastSender = msg.sender;
}
}
El creation code resultante es
608060405260578060116000396000f3fe608060405236601c57600080546001600160a01b03191633179055005b600080fdfea2646970667358221220e9b731ab28726d97cbf5219f1e5eaec508f23254c60b15ed1d3456572547c5bf64736f6c63430008070033.
Esto se puede separar como

Veamos el runtime code en detalle
Dado que este es un contrato en Solidity, podemos dividirlo en el bytecode ejecutable y los metadatos del contrato, como se explicó anteriormente.
Runtime code := 0x608060405236601c57600080546001600160a01b03191633179055005b600080fdfe
Metadata := 0xa2646970667358221220e9b731ab28726d97cbf5219f1e5eaec508f23254c60b15ed1d3456572547c5bf64736f6c63430008070033a2646970667358221220e9b731ab28726d97cbf5219f1e5eaec508f23254c60b15ed1d3456572547c5bf64736f6c63430008070033.
Profundicemos en lo que hace el runtime code utilizando un resultado de evm.codes. Se ha dividido para simplificarlo.
Primero inicializamos el puntero de memoria libre.
[00] PUSH1 80
[02] PUSH1 40
[04] MSTORE
Aquí verificamos si se enviaron datos con la transacción; de ser así, hacemos un JUMP al contador de programa (PC) 0x1c donde revertimos. Las únicas dos formas válidas para que un contrato reciba datos son las funciones regulares y el fallback. Solo tenemos una función receive, por lo que no hay una forma válida de que el contrato reciba calldata.
[05] CALLDATASIZE
[06] PUSH1 1c
[08] JUMPI
Y luego tenemos el código que almacena msg.sender.
[09] PUSH1 00
[0b] DUP1
[0c] SLOAD
[0d] PUSH1 01
[0f] PUSH1 01
[11] PUSH1 a0
[13] SHL
[14] SUB
[15] NOT
[16] AND
[17] CALLER
[18] OR
[19] SWAP1
[1a] SSTORE
[1b] STOP
Este es el JUMPDEST 0x1c para el caso donde se envió calldata. La transacción se revierte.
[1c] JUMPDEST
[1d] PUSH1 00
[1f] DUP1
[20] REVERT
[21] INVALID
Constructor con parámetros
Los contratos con argumentos en el constructor se codifican de manera un poco diferente. Se espera que los parámetros del constructor se adjunten al final del creation code (después del runtime code) y estén codificados en ABI.
Solidity en particular añade una comprobación extra para asegurar que la longitud de los parámetros del constructor sea al menos la longitud de los argumentos esperados del constructor, de lo contrario, se revierte.
Veamos un ejemplo simple. No incluimos ningún runtime code por simplicidad. El único código que incluimos está en el constructor, el cual no forma parte del runtime code.
// optimizer: 200contract MinimalLogic {
uint256 private x;
constructor (uint256 _x) payable {
x =_x;
}
}
El creation code es
608060405260405160893803806089833981016040819052601e916025565b600055603d565b600060208284031215603657600080fd5b5051919050565b603f80604a6000396000f3fe6080604052600080fdfea26469706673582212204a131c1478e0e7bb29267fd8f6d38a660b40a25888982bd6618b720d4498b6b464736f6c63430008070033
Desglosando esto obtenemos
"Init code": 0x608060405260405160893803806089833981016040819052601e916025565b600055603d565b600060208284031215603657600080fd5b5051919050565b603f80604a6000396000f3fe"Runtime code (metadata only)": 0x6080604052600080fdfea26469706673582212204a131c1478e0e7bb29267fd8f6d38a660b40a25888982bd6618b720d4498b6b464736f6c63430008070033"Constructor arguments are missing!"
Ejecutar el creation code de esta manera se revertiría en el init code porque espera al menos 32 bytes después del runtime code para usar como uint256 _x. Veremos esto en más detalle al desglosar cada opcode. Por ahora, podemos adjuntar al creation code un uint256(1) codificado en ABI para usar como _x.
Ahora, el bytecode corregido
608060405260405160893803806089833981016040819052601e916025565b600055603d565b600060208284031215603657600080fd5b5051919050565b603f80604a6000396000f3fe6080604052600080fdfea26469706673582212204a131c1478e0e7bb29267fd8f6d38a660b40a25888982bd6618b720d4498b6b464736f6c634300080700330000000000000000000000000000000000000000000000000000000000000001
Analicemos esto usando el resultado de evm.codes.
Paso 1: Inicializar el puntero de memoria libre
Como es habitual en los contratos de Solidity, inicializamos el puntero de memoria libre con 6080604052.
Paso 2: Obtener la longitud de los parámetros del constructor
// 6040 51 6089 38 03
PC OPCODE
[05] PUSH1 40
[07] MLOAD
[08] PUSH1 89
[0a] CODESIZE
[0b] SUB
Aquí PUSH1 40 MLOAD aplica un MLOAD al puntero de memoria libre para usarlo más tarde. Empujamos al stack la longitud del creation code (sin los parámetros del constructor) con PUSH1 89 y luego llamamos a CODESIZE (esto incluye los parámetros del constructor). Restamos ambos para obtener la longitud de los parámetros del constructor.
Paso 3: Copiar el parámetro del constructor a la memoria
// 80 6089 83 39
PC OPCODE
[0c] DUP1
[0d] PUSH1 89
[0f] DUP4
[10] CODECOPY
Aquí preparamos el stack para CODECOPY. Duplicamos el resultado de la resta anterior con el opcode DUP1 y empujamos 0x89 (la longitud del creation code sin los argumentos del constructor) al stack con PUSH1 89. Finalmente, usamos DUP4 para llevar el offset de memoria a la cima del stack. Ahora llamamos a CODECOPY para copiar el parámetro del constructor a la memoria en la ubicación del puntero de memoria libre.
Paso 4: Actualizar el puntero de memoria libre
Después de escribir el código en la memoria, Solidity actualiza el puntero de memoria libre de la siguiente manera.
// 81 01 6040 81 90 52
PC OPCODE
[11] DUP2
[12] ADD
[13] PUSH1 40
[15] DUP2
[16] SWAP1
[17] MSTORE
Hacemos esto sumando la longitud del parámetro del constructor (0x20) que duplicamos anteriormente al puntero de memoria libre (0x80), para luego organizarlo con operaciones DUP1 y SWAP1 antes de llamar a MSTORE 40, lo cual almacena el nuevo valor (0xa0) como el puntero de memoria libre.
A continuación tenemos una serie de operaciones dinámicas y JUMPs que no se ejecutan secuencialmente, sino más bien basándose en ciertas condiciones. Profundicemos.
Los pasos están numerados para que puedas seguirlos de forma secuencial sin tener que buscar el JUMPDEST requerido.
También puedes usar el enlace al playground para este bytecode para probarlo tú mismo.
Paso 5: Saltar al JUMPDEST de SSTORE
// 601e 91 6025 56
PC OPCODE
[18] PUSH1 1e
[1a] SWAP2
[1b] PUSH1 25
[1d] JUMP // jump to JUMPDEST 0x25
Queremos saltar al PC que realiza las operaciones para guardar el parámetro del constructor en el storage.
Empujamos 1e al stack. 1e es la posición en el contador de programa donde realmente se ejecuta el SSTORE, pero primero tenemos que verificar que el parámetro del constructor copiado sea de al menos 32 bytes. Esta operación comienza en el contador de programa 0x25, que es el JUMPDEST de arriba.
Paso 8: Almacena el argumento del constructor en el slot de storage 0
Este es el JUMPDEST 0x1e, el JUMPDEST 0x25 se ejecuta primero y se encuentra abajo. Ten en cuenta que este es el paso 8, y la sección anterior era el paso 5. Solo se ejecuta si las condiciones en 6 y 7 se completan con éxito. Lo introducimos aquí fuera de orden para mantener la misma secuencia del bytecode compilado.
// 5b 6000 55 603d 56
PC OPCODE
[1e] JUMPDEST
[1f] PUSH1 00
[21] SSTORE
[22] PUSH1 3d
[24] JUMP
Aquí empujamos 0x00, que es el slot de storage en el que vamos a almacenar _x, y llamamos a SSTORE. Luego empujamos el destino del Jump para el CODECOPY final y el RETURN.
Paso 6: Comprueba si el tamaño del parámetro del constructor es de al menos 32 bytes
Este es el JUMPDEST 0x25
// 5b 6000 6020 82 84
PC OPCODE
[25] JUMPDEST
[26] PUSH1 00
[28] PUSH1 20
[2a] DUP3
[2b] DUP5
// continue// 03 12 15 6036 57
[2c] SUB
[2d] SLT
[2e] ISZERO
[2f] PUSH1 36
[31] JUMPI // Jump to 0x36 if ISZERO returns 1// else continue and revert// 6000 80 fd
[32] PUSH1 00
[34] DUP1
[35] REVERT
Aquí verificamos que el parámetro del constructor sea de al menos 32 bytes.
Primero, empujamos 0x00 al stack (para usarlo más tarde) y empujamos la longitud mínima aceptable 0x20 (32 bytes) al stack. A continuación, podemos obtener la longitud del parámetro del constructor que se va a comparar verificando su offset y el puntero de memoria libre actual que empujamos al stack anteriormente. Así que usamos DUP3 para llevar el offset al stack y luego DUP5 para llevar el puntero de memoria libre actual a la cima del stack.
Llamar a SUB resta y empuja la longitud al stack. Ahora podemos llamar directamente a SLT (signed less than) para comprobar si llega a 32 bytes, empujando 0 si es falso y 1 si es verdadero; el opcode ISZERO comprueba si la cima del stack (el resultado de SLT) es 0, lo extrae y empuja el resultado booleano al stack. Empujamos la siguiente ubicación de JUMP al stack y saltamos a ella si ISZERO devolvió 1; de lo contrario, revertimos para evitar la ejecución con calldata inválido.
Paso 7: Carga el parámetro al stack y organiza el stack para guardar el parámetro del constructor en el storage
Este es el JUMPDEST 0x36
// 5b 50 51 91 90 50 56
PC OPCODE
[36] JUMPDEST
[37] POP
[38] MLOAD
[39] SWAP2
[3a] SWAP1
[3b] POP
[3c] JUMP // jump to 0x1e
Aquí hacemos POP de 0 (el contador de programa 26 del paso 6) ya que no lo necesitamos más. Hacemos MLOAD del parámetro del constructor al stack y limpiamos el offset de memoria de los parámetros del constructor, ya que tampoco se necesita más.
Paso 9: Copia el runtime code a la memoria y lo devuelve
Este es el JUMPDEST 0x3d, el JUMPDEST 0x1e se ejecuta primero arriba
// 5b 603f 80 604a 6000 39 6000 f3 fe
PC OPCODE
[3d] JUMPDEST
[3e] PUSH1 3f
[40] DUP1
[41] PUSH1 4a
[43] PUSH1 00
[45] CODECOPY
[46] PUSH1 00
[48] RETURN
[49] INVALID
// Unexecutable code (contract metadata)0x6080604052600080fdfea26469706673582212204a131c1478e0e7bb29267fd8f6d38a660b40a25888982bd6618b720d4498b6b464736f6c63430008070033
Aquí devolvemos el runtime code del contrato como de costumbre desde la memoria.
Memoria justo antes de que se ejecute RETURN
De 0x00 a 0x40 se encuentra el bytecode del runtime code (vacío) y los metadatos. 0x40 contiene el puntero de memoria libre. 0x80 contiene el argumento del constructor, uint256(1).
0x00<->0x20 = 0x6080604052600080fdfea26469706673582212208f9ffa7a3ab43f0ff61d30330x20<->0x40 = 0x624bf0e9d398f9a91213656b13d9ffc8fd90fdbc64736f6c63430008070033000x40<->0x60 = 0x00000000000000000000000000000000000000000000000000000000000000a00x60<->0x80 = 0x00000000000000000000000000000000000000000000000000000000000000000x80<->0xa0 = 0x0000000000000000000000000000000000000000000000000000000000000001
Conclusión
El despliegue de smart contracts incluye un par de operaciones de bajo nivel que la mayoría de los lenguajes abstraen. Aprendimos cómo se ejecutan los smart contracts utilizando un creation code enviado a la dirección nula, las diferentes partes de este creation code, sus roles al desplegar un contrato y cómo funcionan juntas. Vimos cómo se almacenan, validan y utilizan los argumentos del constructor para configurar el contrato.
RareSkills Blockchain Bootcamp
Por favor, consulta nuestra oferta avanzada de blockchain bootcamp para obtener más información sobre la capacitación para desarrolladores de nivel experto que ofrecemos.
Publicado originalmente el 6 de febrero de 2023