El estándar minimal proxy nos permite parametrizar la creación del clon, pero esto requiere una transacción de inicialización extra. Es posible omitir este paso por completo y parametrizar el valor que nos interesa en el bytecode del proxy, en lugar de utilizar el almacenamiento (storage).
El estándar MetaProxy también es una implementación mínima de bytecode para crear clones de contratos inteligentes con metadatos inmutables únicos añadidos para cada uno de los clones.
Estos metadatos pueden ser cualquier cosa, desde una cadena de texto (string) hasta un número, y pueden tener una longitud arbitraria. Sin embargo, el uso previsto es que sirvan como argumentos de función para parametrizar el comportamiento de los contratos de implementación.
Dado que el bytecode de este estándar es conocido, los usuarios y herramientas de terceros, como Etherscan, pueden utilizarlo para deducir que un clon siempre redirigirá a una dirección de contrato de implementación particular junto con los metadatos adjuntos.
Echemos un vistazo al bytecode del MetaProxy sin los metadatos.
600b380380600b3d393df3363d3d373d3d3d3d60368038038091363936013d73bebebebebebebebebebebebebebebebebebebebe5af43d3d93803e603457fd5bf3
La longitud del bytecode del MetaProxy es de 65 bytes, que consisten en un init code de 11 bytes y un runtime code de 54 bytes.
Aunque el bytecode del contrato MetaProxy es similar al estándar Minimal Proxy, algunas partes del bytecode son diferentes; por ejemplo, la sección verde del bytecode (abajo) tiene algunos comandos de opcode adicionales, que explicaremos más adelante.

La dirección ficticia 0xbebebebebebebebebebebebebebebebebebebebe se reemplaza por la dirección del contrato de implementación después del despliegue.
Autoría
Este artículo fue coescrito por Jesse Raymond (LinkedIn, Twitter) como parte del RareSkills Technical Writing Program.
Creando un contrato ERC20 con el estándar MetaProxy
En esta sección, crearemos un clon MetaProxy de un contrato ERC20. Profundicemos en cómo se puede hacer esto y visualicemos cómo se añaden los metadatos al clon.
Para implementar el contrato ERC20, heredaremos el contrato ERC20Upgradeable de OpenZeppelin, el cual tiene una función ERC20_init que se utiliza para inicializar las variables de estado de ERC20 en lugar de un constructor, el cual no se puede utilizar con patrones proxy como el que estamos construyendo aquí.
Esto se debe a que los constructores se llaman en los despliegues de los contratos, y si siguiéramos este método, las variables de estado como name y symbol del estándar ERC20 no se inicializarían en el bytecode del clon MetaProxy ERC20, ya que el constructor estaría configurando el almacenamiento del contrato de implementación y no el del clon.
Sin embargo, no utilizaremos la función de inicialización porque simplemente podemos obtener name, symbol y totalSupply del clon MetaProxy ERC20 desde su bytecode después de añadirlos como metadatos.
El contrato de implementación ERC20
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
contract ERC20Implementation is ERC20Upgradeable {
// get ERC20 name from the metadata
function name()
public
view
virtual
override
returns (string memory name__)
{
(name__, , ) = getMetadata();
}
// get ERC20 symbol from the metadata
function symbol()
public
view
virtual
override
returns (string memory symbol__)
{
(, symbol__, ) = getMetadata();
}
// get ERC20 total supply from the metadata
function totalSupply()
public
view
virtual
override
returns (uint256 totalSupply_)
{
(, , totalSupply_) = getMetadata();
}
// mint function
function mint(uint amount) public {
_mint(msg.sender, amount * 10 ** 18);
}
/// returns the decoded metadata of this (ERC20 MetaProxy) contract.
function getMetadata()
public
pure
returns (
string memory name__,
string memory symbol__,
uint256 totalSupply__
)
{
bytes memory data;
assembly {
let posOfMetadataSize := sub(calldatasize(), 32)
let size := calldataload(posOfMetadataSize)
let dataPtr := sub(posOfMetadataSize, size)
data := mload(64)
mstore(64, add(data, add(size, 32)))
mstore(data, size)
let memPtr := add(data, 32)
calldatacopy(memPtr, dataPtr, size)
}
//return the decoded metadata
return abi.decode(data, (string, string, uint256));
}
}
Obteniendo los metadatos
La función getMetadata se utiliza en la implementación para devolver los metadatos del clon. Dado que el MetaProxy siempre carga sus metadatos cada vez que se llaman a sus funciones (este es el diseño del estándar, que explicaremos más adelante en este artículo), la función getMetadata se utiliza para extraer los metadatos de la llamada y devolverlos como una tupla en nuestra implementación.
También se está utilizando en las funciones name, symbol y totalSupply de ERC20 para obtener una parte específica de los metadatos, ya sea un string para name y symbol o un uint256 para el totalSupply.
Hemos derivado esta función de la implementación de ejemplo aquí y la hemos modificado para que se adapte a nuestros propósitos para el contrato ERC20.
El contrato Factory
El EIP original también tiene un enlace a la implementación del MetaProxyFactory, que importamos y heredamos aquí.
El MetaProxyFactory contiene el código para crear nuevos clones MetaProxy.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./ERC20Implementation.sol";
import "./MetaProxyFactory.sol";
contract ERC20MetaProxyFactory is MetaProxyFactory {
address[] public proxyAddresses;
function createClone(
string memory _name,
string memory _symbol,
uint256 _initialSupply
) public returns (address) {
// Encode the ERC20 constructor arguments
bytes memory metadata = abi.encode(_name, _symbol, _initialSupply);
// Create the proxy
address proxyAddress = _metaProxyFromBytes(
address(new ERC20Implementation()),
metadata
);
proxyAddresses.push(proxyAddress);
return proxyAddress;
}
}
Creando un clon - explicando el contrato factory
El ERC20MetaProxyFactory es nuestro contrato factory para crear nuevos clones. Utilizamos la función _metaProxyFromBytes, que se hereda del MetaProxyFactory, para desplegar nuevos clones.
La función _metaProxyFromBytes toma dos argumentos, que son; 1. la dirección del contrato de implementación (por esta razón usamos la palabra clave new para desplegar primero un contrato ERC20Implementation). 2. los metadatos.
Dado que el bytecode de los contratos inteligentes se representa en hexadecimal en este código, los metadatos deben ser codificados en ABI antes de poder adjuntarse al bytecode del clon.
Por este motivo codificamos los argumentos de la función createClone antes de pasarlos como metadatos a la función _metaProxyFromBytes, la cual crea el nuevo clon y devuelve la dirección.
Esta es la firma de la función de _metaProxyFromBytes.
function _metaProxyFromBytes (address targetContract, bytes memory metadata) internal returns (address) {
// code that deploys new clones here
}
Desplegando el clon
A continuación, se muestra un script de Hardhat que despliega los contratos e interactúa con un clon desplegado en la red Sepolia:
const hre = require("hardhat");
async function main() {
const ERC20ProxyFactory = await hre.ethers.getContractFactory(
"ERC20MetaProxyFactory"
);
const erc20ProxyFactory = await ERC20ProxyFactory.deploy();
// deploy the erc20 proxy factory contract
await erc20ProxyFactory.deployed();
console.log(
`ERC20 proxy factory contract deployed to ${erc20ProxyFactory.address}`
);
// create clone
const tx1 = await erc20ProxyFactory.createClone(
"Meta Token V1",
"MTV1",
"150000000000000000000000" //150,000 initial supply * 10^18 decimals
);
await tx1.wait();
const proxyCloneAddress = await erc20ProxyFactory.proxyAddresses(0);
console.log("Proxy clone deployed to", proxyCloneAddress);
// load the clone
const proxyClone = await hre.ethers.getContractAt(
"ERC20Implementation",
proxyCloneAddress
);
// retrieve the metadata
const metadata = await proxyClone.getMetadata();
console.log("metadata for clone: ", metadata);
//retrieve the "name" string from the metadata
const name = await proxyClone.name();
console.log("ERC20 name of clone from metadata: ", name);
const tx2 = await proxyClone.mint(150_000);
tx2.wait();
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Después de ejecutar este script, la consola mostró el siguiente resultado:
ERC20 proxy factory contract deployed to 0xd45f2c555ba30aCb89EB0a3fff6a4416f8cC06e2
Proxy clone deployed to 0x5170672424194899F52B29E60e85C1632F0C732e
metadata for clone: [
'MetaProxy Token',
'MPRXT',
BigNumber { value: "150000000000000000000000" },
name__: 'MetaProxy Token',
symbol__: 'MPRXT',
totalSupply__: BigNumber { value: "150000000000000000000000" }
]
ERC20 name of clone from metadata: MetaProxy Token
También hemos desplegado nuestros contratos en la red Sepolia y aquí están los detalles de los 3 contratos.
Observe el “read” y “write as proxy” para el contrato MetaProxy ERC20; esto significa que Etherscan reconoce que el contrato proxy no es solo otro contrato inteligente, sino un contrato proxy.
Por conveniencia, nuestro código mantiene una lista de los clones desplegados para que podamos acceder a ellos fácilmente en nuestro entorno de Hardhat, pero esto es opcional.
Manejando Reverts
Como se indicó en la introducción, si ocurre un error cuando el clon proxy redirige una llamada al contrato de implementación, el payload del revert se devuelve al clon y se muestra a los usuarios.
Pongamos esto a prueba y veamos si funciona como se espera.
En nuestro ejemplo de contrato ERC20 anterior, intentaremos llamar a la función transferFrom sin allowance para ver si la transacción tiene éxito o si se nos devuelve el error.
Hacemos esto con este script de Hardhat.
try {
await proxyClone.transferFrom(
proxyCloneAddress,
erc20ProxyFactory.address,2000000000);
} catch (error) {
console.error(error);
}
¡Y boom! Obtuvimos un error.
Error: VM Exception while processing transaction: reverted with reason
string 'ERC20: insufficient allowance'
¡Esto significa que el revert y el motivo de los reverts se envían de vuelta al clon correctamente!
Explicando el bytecode del clon ERC20 desplegado
Recuerde que antes dijimos que los metadatos del clon se adjuntan al final del clon. En esta sección, explicaremos el bytecode desplegado del clon MetaProxy.
Tenga en cuenta que el bytecode de todos los clones sigue el bytecode mínimo del estándar MetaProxy, excepto que cada clon tiene sus metadatos al final de su bytecode.
Veamos el bytecode del clon ERC20.
0x363d3d373d3d3d3d60368038038091363936013d731bf70065f6b4e424b7b642b3a76a5e01f208e3fc5af43d3d93803e603457fd5bf3000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000174876e800000000000000000000000000000000000000000000000000000000000000000a50726f7879546f6b656e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000650546f6b656e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0
¡Hay muchos ceros ahí! Nuestro bytecode tiene 310 bytes de longitud.
Vamos a desglosar esto aún más.
<=== the runtime bytecode of the MetaProxy standard ===>
0x363d3d373d3d3d3d60368038038091363936013d731bf70065f6b4e424b7b642b3a76a5e01f208e3fc5af43d3d93803e603457fd5bf3
<===>
<=== the abi encoded metadata ===>
000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000174876e800000000000000000000000000000000000000000000000000000000000000000a50726f7879546f6b656e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000650546f6b656e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0
<===>
Los metadatos codificados se componen de los desplazamientos (offsets) en memoria donde se almacenan los valores codificados, la longitud de los valores string codificados, los valores y la siguiente memoria disponible (el free memory pointer). Aquí hay un desglose detallado de los metadatos.
Siguiendo la especificación ABI, los primeros tres conjuntos de palabras de 32 bytes representan los valores de los datos que estamos codificando o el puntero a su ubicación si tienen un tipo dinámico. Tenemos dos strings y un número entero sin signo (unsigned integer), que representan el name, el symbol y el totalSupply respectivamente. Debido a que name y symbol son dinámicos, sus ranuras (slots) contienen punteros, mientras que el totalSupply simplemente se almacena en su ranura.
// memory[0x00 - 0x20] 0000000000000000000000000000000000000000000000000000000000000060 // memory offset for name string
// memory[0x20 - 0x40] 00000000000000000000000000000000000000000000000000000000000000a0 // memory offset for symbol string
// memory[0x40 - 0x60] 000000000000000000000000000000000000000000000000000000174876e800 // the encoded total supply (uint256)
// memory[0x60 - 0x80] 000000000000000000000000000000000000000000000000000000000000000a // the length of the name string (0x0a == 10)
// memory[0x80 - 0xa0] 50726f7879546f6b656e00000000000000000000000000000000000000000000 // the encoded name string
// memory[0xa0 - 0xc0] 0000000000000000000000000000000000000000000000000000000000000006 // the length of the symbol string (6)
// memory[0xc0 - 0xe0] 50546f6b656e0000000000000000000000000000000000000000000000000000 // the encoded symbol string
// memory[0xe0 ] 00000000000000000000000000000000000000000000000000000000000000e0 // the length of the metadata (0xe0 == 224)
Como se indicó anteriormente, el runtime code es de 54 bytes. Si dividimos el bytecode del clon ERC20 en dos, extrayendo los primeros 54 bytes del runtime code, nos quedamos con los metadatos codificados en ABI, que son 224 bytes, y la longitud de los metadatos añadidos al final del código, que es de 32 bytes.
Según el estándar
…todo lo que siga al bytecode de MetaProxy pueden ser metadatos arbitrarios y los últimos 32 bytes (una palabra) del bytecode deben indicar la longitud de los metadatos en bytes.
En nuestro caso, los metadatos tienen 224 bytes de longitud, y su longitud se almacena en los 32 bytes finales (0x000…000e0).
Puede parecer extraño almacenar la longitud de los metadatos al final, ya que la codificación ABI normalmente almacena la longitud antes de que comiencen los datos, pero en este caso, facilita que el contrato de implementación analice los metadatos adicionales con el siguiente código visto anteriormente.
let posOfMetadataSize := sub(calldatasize(), 32)
Si decodificamos los metadatos aquí, obtenemos los datos de inicialización del clon.

Veamos paso a paso los mnemotécnicos del bytecode.
<=== start of the runtime bytecode ===>
// Note that RETURNDATASIZE is used in some parts of the bytecode to push zero to the stack.
// This is because RETURNDATASIZE (2 gas) costs less gas than a PUSH1 0 (3 gas).
// copy transaction calldata
[00] CALLDATASIZE
[01] RETURNDATASIZE
[02] RETURNDATASIZE
[03] CALLDATACOPY
// prepare the stack for a delegate call
[04] RETURNDATASIZE
[05] RETURNDATASIZE
[06] RETURNDATASIZE
[07] RETURNDATASIZE
[08] PUSH1 36 // 0x36 == 54, this is the length of the runtime code
[0a] DUP1
[0b] CODESIZE // get the length of the clone's bytecode + the metadata, which is 310 bytes
[0c] SUB // subtract the runtime code from the bytecode, to get the metadata (the remaining 256 bytes). this is used in the delegatecall
[0d] DUP1
[0e] SWAP2
[0f] CALLDATASIZE
[10] CODECOPY // copy the metadata to memory and forward it to the implementation contract during the delegatecall.
[11] CALLDATASIZE
[12] ADD
[13] RETURNDATASIZE
// push the address of the implementation contract to the stack and perform the delegatecall
[14] PUSH20 1bf70065f6b4e424b7b642b3a76a5e01f208e3fc
[29] GAS
[2a] DELEGATECALL
// copy the return data (the result of the call) to memory and set up the stack for a conditional jump
[2b] RETURNDATASIZE
[2c] RETURNDATASIZE
[2d] SWAP4
[2e] DUP1
[2f] RETURNDATACOPY
[30] PUSH1 34
//jump to line 34 and return the result of the call if it was successful, else revert on line 33
[32] JUMPI
[33] REVERT
[34] JUMPDEST
[35] RETURN
<<=== the metadata starts from here ===>>
Esta es una descripción de alto nivel de cómo funciona el bytecode del clon. En resumen, copia el calldata que se le envía en una transacción y hace un delegatecall al contrato de implementación con ese calldata mientras reenvía los metadatos en el delegatecall.
Tenga en cuenta que, dado que los metadatos también se reenvían en todas las llamadas, se enviarán junto con algunas funciones que no los necesitan, como la función balanceOf() de ERC20, que no tiene relación con los metadatos codificados.
Conclusión
El estándar MetaProxy EIP-3448 se puede ver como una extensión del estándar Minimal Proxy EIP-1167, que permite adjuntar metadatos inmutables al runtime bytecode de cada clon.
El estándar MetaProxy permite a los usuarios parametrizar el valor que les interesa en el bytecode del clon en lugar de usar almacenamiento, lo que reduce los costos de gas.
Además, las herramientas de terceros como Etherscan pueden usar el bytecode conocido del estándar para determinar que un clon siempre redirigirá a una dirección de contrato de implementación específica de una manera específica y consultar los metadatos adjuntos a un clon.
Más información
Este material es parte de nuestro Solidity Bootcamp avanzado. Consulte nuestro blockchain bootcamp para ver todas nuestras ofertas.
Publicado originalmente el 3 de marzo de 2023