Imagen de https://pixabay.com/photos/stormtrooper-star-wars-lego-storm-2899993/
Introducción
EIP-1167, que también se conoce como el contrato minimal proxy, es un patrón de solidity comúnmente utilizado para crear clones proxy de manera económica.
Si un caso de uso requiere desplegar un contrato idéntico (o un contrato muy similar) repetidamente, esta es una forma más eficiente en cuanto a gas de hacerlo.
Por ejemplo, gnosis safe utiliza el patrón clone al crear una nueva caja fuerte. Cuando interactúas con gnosis safe, en realidad estás interactuando con un clon del mismo.
Un contrato clone es como un proxy que no puede ser actualizado. Debido a que los proxies son muy pequeños en relación con el contrato de implementación, son económicos de desplegar.
Al igual que el patrón proxy, los clones delegan todas las llamadas al contrato de implementación pero mantienen el estado en su propio storage.
A diferencia del patrón proxy regular, varios clones pueden apuntar al mismo contrato de implementación. Los clones no pueden ser actualizados.
La dirección del contrato de implementación se almacena en el bytecode. Esto ahorra gas en comparación con el storage y evita que el clone apunte a otra implementación.
Este diseño hace que sea considerablemente más barato de desplegar, ya que el bytecode de un clone proxy es típicamente mucho más pequeño que el bytecode de un contrato de implementación. De hecho, el EIP-1167 tiene un tamaño de solo 55 bytes (45 bytes para el runtime), incluyendo el init code. Sin embargo, las llamadas durante la ejecución costarán más, porque siempre hay un delegatecall añadido.
Este artículo describirá tanto el EIP como la función initializer utilizada para inicializar el equivalente a los parámetros del constructor.
Autoría
Este artículo fue coescrito por Jesse Raymond (LinkedIn, Twitter) como parte del RareSkills Technical Writing Program.
Cómo funciona EIP-1167
Como un proxy típico, recibe datos de transacción a través de un call, reenvía estos datos al contrato inteligente de implementación, obtiene el resultado de la llamada externa y devuelve el resultado si la llamada externa fue exitosa o ejecuta un revert si hubo un error.
El bytecode del minimal proxy
El contrato minimal proxy tiene un bytecode conciso de solo 55 bytes. Este bytecode se compone de:
- el init code
- el runtime code que incluye instrucciones para recibir la calldata de la transacción
- la dirección de 20 bytes del contrato de implementación
- y comandos para ejecutar un delegatecall y
- retornar el resultado, o desencadenar un revert si ocurre un error.
Aquí está el bytecode del minimal proxy:
3d602d80600a3d3981f3363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3
La dirección ficticia: 0xbebebebebebebebebebebebebebebebebebebebe se reemplaza con la dirección del contrato de implementación.
Vamos a desglosarlo.

La sección del init code
Los primeros 10 bytes del bytecode contienen el init code, que se ejecuta una vez y se utiliza para desplegar el minimal proxy.
Para aprender más sobre la creación y despliegue de contratos inteligentes, consulta nuestro artículo sobre Ethereum smart contract creation code.
A continuación se muestran los comandos que se llevan a cabo en la EVM.
// copy the runtime bytecode of the minimal proxy
// starting from offset 10, and save it to the blockchain
[00] RETURNDATASIZE
[01] PUSH1 2d
[03] DUP1
//push 10 - offset to copy runtime code from
[04] PUSH1 0a
[06] RETURNDATASIZE
// copy the runtime code and save it to the blockchain
[07] CODECOPY
[08] DUP2
[09] RETURN
Copiando la calldata
El init code despliega el contrato y guarda el bytecode de runtime on-chain, comenzando desde el offset 10 (la sección de copiar la calldata) hasta el final del bytecode.
Después de que un minimal proxy ha sido desplegado y se le envía una llamada, este copia la calldata de la transacción a la memoria, hace un push de la dirección de 20 bytes del contrato de implementación y realiza un delegatecall en el contrato de implementación.
Esta copia de la calldata se realiza con los siguientes opcodes.
//copy the transaction calldata to memory
[0a] CALLDATASIZE
[0b] RETURNDATASIZE // this is a hack to push 0 onto the stack with less gas than doing PUSH 0
[0c] RETURNDATASIZE
[0d] CALLDATACOPY
[0e] RETURNDATASIZE
[0f] RETURNDATASIZE
[10] RETURNDATASIZE
[11] CALLDATASIZE
[12] RETURNDATASIZE
//pushes the 20 bytes address of the implementation contract
[13] PUSH20
La dirección del contrato de implementación
Después de que la calldata de la transacción se copia a la memoria, el stack se prepara para realizar un delegatecall y la dirección de 20 bytes del contrato de implementación se coloca en la parte superior del stack. En la sección anterior, podemos ver que termina con un PUSH20. Lo que sigue a continuación es la dirección del contrato de implementación.
//push the address of the implementation contract to the stack. The address here is just a dummy address
[13] PUSH20 bebebebebebebebebebebebebebebebebebebebe
La sección del delegatecall
Después de copiar la calldata de la transacción a la memoria y obtener la dirección del contrato de implementación en la parte superior del stack, el minimal proxy está listo para realizar un delegatecall al contrato de implementación.
Si necesitas repasar cómo funciona delegatecall, lee nuestro tutorial sobre delegatecall.
Después de que se realiza el delegatecall, el minimal proxy devuelve el resultado de la llamada si fue exitosa o ejecuta un revert si ha ocurrido un error.
La sección del delegatecall tiene los siguientes opcodes.
//perform a delegate call on the implementation contract, and forward all available gas
[28] GAS
[29] DELEGATECALL
//copy the return data of the call to memory
[2a] RETURNDATASIZE
[2b] DUP3
[2c] DUP1
[2d] RETURNDATACOPY
// set up the stack for the conditional jump
[2e] SWAP1
[2f] RETURNDATASIZE
[30] SWAP2
[31] PUSH1 2b
//jump to line 35 and return the result of the call if it was successful, else revert on line 34
[33] JUMPI
[34] REVERT
[35] JUMPDEST
[36] RETURN
Este es un resumen del EIP-1167 y cómo funciona.
Imagina que el contrato de implementación es un token ERC20. En ese caso, el clone se comportará exactamente como un token ERC20.
Implementación de contrato inteligente EIP-1167 con inicialización
Hay algunas situaciones en las que queremos parametrizar la creación de un clone. Por ejemplo, si estuviéramos clonando un token ERC20, cada clone tendría el mismo totalSupply, lo cual podría no ser deseable.
Para poder configurar este parámetro, se puede utilizar el patrón “clone con inicialización”.
Veamos cómo se puede utilizar el EIP-1167 para crear clones proxy con una función de inicialización. Esto sigue una simple secuencia de pasos:
- Crear un contrato de implementación
- Clonar el contrato con el estándar EIP-1167
- Desplegar el clone y llamar a la función de inicialización, que solo puede ser llamada una vez.
Esta restricción de llamar solo una vez es necesaria, o alguien podría alterar el parámetro crítico que establecimos después del despliegue, como cambiar el suministro total.
Repasemos estos pasos con un ejemplo a continuación.
El contrato de implementación a clonar
contractImplementationContract{
boolprivate isInitialized; //initializer function that will be called once, during
deployment.
functioninitializer() external {
require(!isInitialized);
isInitialized =true;
} // rest of the implementation functions go here
}
Utilizamos este código para desplegar el clone
contract MinimalProxyFactory {
address[] public proxies;
function deployClone(address _implementationContract) external returns (address) {
// convert the address to 20 bytes
bytes20 implementationContractInBytes = bytes20(_implementationContract);
//address to assign a cloned proxy
address proxy;
// as stated earlier, the minimal proxy has this bytecode
// <3d602d80600a3d3981f3363d3d373d3d3d363d73><address of implementation contract><5af43d82803e903d91602b57fd5bf3>
// <3d602d80600a3d3981f3> == creation code which copies runtime code into memory and deploys it
// <363d3d373d3d3d363d73> <address of implementation contract> <5af43d82803e903d91602b57fd5bf3> == runtime code that makes a delegatecall to the implementation contract
assembly {
/*
reads the 32 bytes of memory starting at the pointer stored in 0x40
In solidity, the 0x40 slot in memory is special: it contains the "free memory pointer"
which points to the end of the currently allocated memory.
*/
let clone := mload(0x40)
// store 32 bytes to memory starting at "clone"
mstore(
clone,
0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000
)
/*
| 20 bytes |
0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000
^
pointer
*/
// store 32 bytes to memory starting at "clone" + 20 bytes
// 0x14 = 20
mstore(add(clone, 0x14), implementationContractInBytes)
/*
| 20 bytes | 20 bytes |
0x3d602d80600a3d3981f3363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe
^
pointer
*/
// store 32 bytes to memory starting at "clone" + 40 bytes
// 0x28 = 40
mstore(
add(clone, 0x28),
0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000
)
/*
| 20 bytes | 20 bytes | 15 bytes |
0x3d602d80600a3d3981f3363d3d373d3d3d363d73b<implementationContractInBytes>5af43d82803e903d91602b57fd5bf3 == 45 bytes in total
*/
// create a new contract
// send 0 Ether
// code starts at the pointer stored in "clone"
// code size == 0x37 (55 bytes)
proxy := create(0, clone, 0x37)
}
// Call initialization
ImplementationContract(proxy).initializer();
proxies.push(proxy);
return proxy;
}
}
Con el contrato MinimalProxyFactory, se puede desplegar una cantidad infinita de clones EIP-1167, pero para este ejemplo, desplegaremos el contrato de implementación que tenemos arriba.
Aquí hay un script simple de Hardhat que despliega los contratos e interactúa con un clone desplegado.
const hre = require("hardhat");
async function main() {
const ImplementationContract = await hre.ethers.getContractFactory(
"ImplementationContract"
);
// deploy the implementation contract
const implementationContract = await ImplementationContract.deploy();
await implementationContract.deployed();
console.log("Implementation contract ", implementationContract.address);
const MinimalProxyFactory = await hre.ethers.getContractFactory(
"MinimalProxyFactory"
);
// deploy the minimal factory contract
const minimalProxyFactory = await MinimalProxyFactory.deploy();
await minimalProxyFactory.deployed();
console.log("Minimal proxy factory contract ", minimalProxyFactory.address);
// call the deploy clone function on the minimal factory contract and pass parameters
const deployCloneContract = await minimalProxyFactory.deployClone(
implementationContract.address
);
deployCloneContract.wait();
// get deployed proxy address
const ProxyAddress = await minimalProxyFactory.proxies(0);
console.log("Proxy contract ", ProxyAddress);
// load the clone
const proxy = await hre.ethers.getContractAt(
"ImplementationContract",
ProxyAddress
);
console.log("Proxy is initialized == ", await proxy.isInitialized()); // get initialized boolean == true
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Ahora hemos desplegado nuestros contratos en la red goerli y aquí están los detalles de transacción para los 3 contratos.
Ten en cuenta que Etherscan reconoce que el contrato proxy no es simplemente otro contrato inteligente, sino que delega la llamada al contrato de implementación.
Por razones de conveniencia, nuestro código mantiene una lista de los clones desplegados, pero esta no es una característica necesaria.
Conclusión
El estándar minimal proxy EIP-1167 es una forma eficiente de desplegar contratos que replican la implementación de otro. El patrón initializer nos permite desplegar el clone como si tuviera un constructor que acepta argumentos.
El costo de este patrón es que cada ejecución tiene la sobrecarga (overhead) de un delegatecall.
Aprende más
Consulta nuestro blockchain bootcamp avanzado para ver nuestro extenso plan de estudios.
Publicado originalmente el 21 de feb. de 2023