El Patrón Diamond (ERC-2535) es un patrón proxy donde el contrato proxy utiliza múltiples contratos de implementación simultáneamente, a diferencia del Transparent Upgradeable Proxy y UUPS, que dependen de un solo contrato de implementación a la vez. El contrato proxy determina a qué contrato de implementación hacer delegatecall basándose en el function selector del calldata que recibe (el mecanismo exacto se describe más adelante):

Una de las ventajas de tener múltiples contratos de implementación es que no hay un límite superior práctico en la cantidad de lógica que el contrato proxy puede usar. Recuerde que la EVM limita el tamaño del bytecode del contrato inteligente a 24kb. Si el desarrollador necesita desplegar hasta 48kb de bytecode, una solución viable es usar el fallback-extension pattern. Para más de 48kb, el patrón diamond es la solución más común.
En la nomenclatura del patrón diamond, el “contrato proxy” se llama diamond y los “contratos de implementación” se llaman “facets”. Tener dos términos que se refieren a lo mismo ha llevado a confusión, así que queremos dejar este punto muy claro ahora:
- diamond = contrato proxy
- facet = contrato de implementación
Un diamond (el proxy) puede ser actualizado cambiando uno o más de los contratos de implementación (facets). Alternativamente, un diamond puede ser no actualizable (inmutable) al no soportar un mecanismo para cambiar los facets (contratos de implementación).
Aprendiendo el Patrón Diamond
El diamond proxy tiene la reputación de ser un “patrón de diseño para expertos” debido a la complejidad emergente de lidiar con múltiples contratos de implementación al mismo tiempo. De hecho, el patrón diamond es algo controvertido entre los desarrolladores de la EVM debido a su supuesta complejidad (no tomamos partido en el debate ni emitimos un juicio de complejidad aquí).
La especificación en sí es bastante pequeña. Requiere cuatro funciones public view, pero si el diamond es actualizable, se requiere una quinta función que cambie el estado para intercambiar los contratos de implementación. El patrón diamond exige un solo evento (independientemente de si el contrato es actualizable). A pesar de la pequeña especificación, implementar esas cuatro (o cinco) funciones es considerablemente más complejo que en otros patrones proxy.
Sin embargo, con los prerrequisitos adecuados (¡que son considerables!), el patrón diamond no es particularmente difícil de conceptualizar. Asumimos que el lector está familiarizado con los temas cubiertos en los primeros trece capítulos de nuestro Proxy Patterns Book. Si no ha leído esos capítulos o no está ya familiarizado con esos temas, aprender el patrón diamond será un desafío, así que asegúrese de cumplir con los prerrequisitos.
Aquí hay algunos problemas con los que el patrón necesita lidiar:
- Cuando el diamond recibe una transacción, ¿cómo sabe a qué contrato de implementación llamar?
- Si un facet (contrato de implementación) se actualiza, ¿cómo sabe el contrato proxy (el diamond) qué función soporta el nuevo facet (contrato de implementación) y, potencialmente, qué funciones ya no son soportadas?
- ¿Dónde debería estar la lógica de actualización: en el bytecode del proxy o en un facet?
- Dado que cada contrato de implementación no tiene conocimiento directo sobre las otras implementaciones, ¿cómo se pueden evitar las colisiones de almacenamiento?
- ¿Cómo puede un actor externo saber qué funciones son soportadas por el diamond proxy? Heredar una interfaz no es suficiente, ya que las funciones podrían cambiar durante una actualización.
- ¿Qué pasa si una función dentro de un contrato de implementación quiere llamar a una función en otro contrato de implementación? ¿Cómo se facilitaría esta transacción?
En este artículo, mostraremos cómo abordar todos los problemas anteriores. Tenga en cuenta que el ERC-2535 no requiere que un diamond proxy sea actualizable. El proxy puede tener contratos de implementación hardcodeados y seguir siendo un diamond válido.
Para mantener las cosas simples al principio, comenzamos mostrando el diamond inmutable.
Diamond inmutable
Un diamond inmutable, también llamado diamond estático o diamond de un solo corte, es un contrato proxy con múltiples contratos de implementación, y ninguno de los contratos de implementación puede ser actualizado. (Es posible que un diamond actualizable se vuelva inmutable eliminando la funcionalidad de actualización, pero discutiremos los diamonds actualizables más adelante).
El patrón diamond como contrato proxy
El código para la porción proxy del diamond proxy debería resultar familiar, si se compara con el OpenZeppelin Proxy utilizado por UUPS y Transparent Upgradeable Proxy:
// Find facet for function that is called and execute the
// function if a facet is found and return any value.
fallback() external payable {
// get facet from function selector
address facet = facetAddress(msg.sig);
require(facet != address(0));
// The code below is the same as OpenZeppelin Proxy.sol
// Execute external function from facet using delegatecall and return any value.
assembly {
// copy function selector and any arguments
calldatacopy(0, 0, calldatasize())
// execute function call using the facet
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
// get any return value
returndatacopy(0, 0, returndatasize())
// return any return value or error back to the caller
switch result
case 0 {revert(0, returndatasize())}
default {return (0, returndatasize())}
}
}
La única diferencia material entre el código anterior y un proxy tradicional son las dos líneas:
address facet = facetAddress(msg.sig);
require(facet != address(0));
El código anterior obtiene los primeros cuatro bytes de la transacción (el function selector) usando msg.sig, luego usa facetAddress() para determinar la dirección del facet que implementa la función (tenga en cuenta que facetAddress() podría ser parte de la lógica del proxy o vivir dentro de otro facet y ser llamado mediante delegatecall; esto se discutirá más adelante). El proxy luego hace un delegatecall a esa dirección con el mismo calldata que recibió.

El EIP-2535 no especifica cómo mapear los function selectors a las direcciones de los contratos de implementación. Para un diamond estático, una solución razonable es hardcodear la relación. Este será el enfoque que tomaremos en la siguiente sección.
Para el diamond actualizable, hardcodear los selectores claramente no es una opción, y debemos depender de mappings, como veremos en las secciones correspondientes.
Bifurcación Condicional en el Function Selector
A continuación, mostramos un contrato proxy con dos contratos de implementación (facets):
- El primer contrato de implementación
Addexpone una única función públicaadd()que devuelve la suma de sus argumentos. - El segundo contrato de implementación
Multiplyexpone dos funciones públicasmultiply()yexponent()que hacen lo que sus nombres sugieren.
El código a continuación muestra un solo proxy usando dos contratos de implementación. La función facetAddress() en Diamond toma msg.sig y devuelve la dirección del facet que implementa la función con esa firma, si existe. El código a continuación aún no cumple con el estándar diamond, pero lo evolucionaremos para que sea compatible.
// first implementation contract
contract Add {
// selector: 0x771602f7
function add(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x + y;
}
}
// second implementation contract
contract Multiply {
// selector: 0x165c4a16
function multiply(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x * y;
}
// selector: 0x2f8cd8b1
function exponent(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x ** y;
}
}
// proxy contract
contract Diamond {
address immutable ADD_ADDR;
address immutable MULTIPLY_ADDR;
constructor() {
ADD_ADDR = address(new Add());
MULTIPLY_ADDR = address(new Multiply());
}
function facetAddress(bytes4 selector) public view returns (address) {
if (selector == 0x771602f7) return ADD_ADDR;
else if (selector == 0x165c4a16) return MULTIPLY_ADDR;
else if (selector == 0x2f8cd8b1) return MULTIPLY_ADDR;
else return address(0);
}
fallback() external payable {
address facet = facetAddress(msg.sig);
require(facet != address(0), "Function does not exist");
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
El estándar EIP-2535 no especifica el mensaje de error al llamar a una función que no existe. En nuestro caso, hacemos revert con el string "Function does not exist", pero un custom error sería más eficiente en gas.
Por simplicidad, nuestro diamond despliega los dos facets en el constructor, pero esto no se hace en la práctica. En la práctica, los facets se despliegan por separado, y el proxy es “informado” de ellos más tarde de alguna manera, como a través de los argumentos del constructor o una función separada.
Algunos diamonds implementan los facets como librerías con funciones externas en lugar de contratos, pero el EIP-2535 no requiere que los facets sean librerías o contratos.
Por simplicidad, usamos una serie de declaraciones else-if para emparejar el function selector con la dirección del facet, pero esto no es eficiente en gas si hay muchas opciones. Para un diamond estático, ordenar los selectores por adelantado y luego hacer una búsqueda binaria es más eficiente; pero discutiremos esto más adelante.
Caso extremo — Transferencias de Ether
Cuando se transfiere ether, no habrá calldata. En esta situación, msg.sig devolverá 0x00000000 y esto no se mapeará a una dirección de un facet. Esto está bien si el contrato no tiene la intención de recibir ether. Sin embargo, si se espera que el contrato reciba ether, o se espera que reaccione al ether entrante, entonces 0x00000000 debería mapearse a una función con la lógica deseada, o al menos a una función que no haga revert. Tenga en cuenta que un atacante puede activar esta función tanto enviando sin calldata como enviando 0x00000000 como calldata, por lo que la lógica debe manejar ambos escenarios con elegancia.
Para hacer que nuestro contrato cumpla con el EIP-2535, debemos implementar cuatro funciones public view obligatorias, cada una discutida a continuación.
Cuatro Funciones Public View Obligatorias
1/4 facetAddress()
Note que facetAddress() es pública; el EIP-2535 requiere que un proxy Diamond exponga una función pública con la firma:
function facetAddress(bytes4 selector) external view returns (address);
Ya cumplimos en ese aspecto.
2/4 facetAddresses()
Además de facetAddress(bytes4 selector), el EIP-2535 exige una función llamada facetAddresses() (en plural) que devuelve todos los facets utilizados por el diamond — en otras palabras, una lista de direcciones. La firma es la siguiente:
function facetAddresses() public view returns (address[] memory addresses);
Dado que los facets en nuestro diamond no pueden cambiar, simplemente hardcodeamos la lista de direcciones de facets:
contract Diamond {
address immutable ADD_ADDR;
address immutable MULTIPLY_ADDR;
constructor() {
ADD_ADDR = address(new Add());
MULTIPLY_ADDR = address(new Multiply());
}
function facetAddress(bytes4 selector) public view returns (address) {
if (selector == 0x771602f7) return ADD_ADDR;
else if (selector == 0x165c4a16) return MULTIPLY_ADDR;
else if (selector == 0x2f8cd8b1) return MULTIPLY_ADDR;
else return address(0);
}
// ┌────────────────────┐
// │ │
// │ THIS CODE IS NEW │
// │ │
// └────────────────────┘
function facetAddresses() public view returns (address[2] memory addresses) {
addresses[0] = ADD_ADDR;
addresses[1] = MULTIPLY_ADDR;
}
// --------
fallback() external payable {
address facet = facetAddress(msg.sig);
require(facet != address(0), "Function does not exist");
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
No hay un orden forzado en la lista de direcciones (direcciones de facets) devueltas por facetAddresses().
3/4 facetFunctionSelectors()
Dada una dirección de facet como argumento, facetFunctionSelectors() devuelve los selectores de todas las funciones públicas del facet. Tiene la siguiente firma:
function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_);
Podemos implementarlo de la siguiente manera:
contract Diamond {
address immutable ADD_ADDR;
address immutable MULTIPLY_ADDR;
constructor() {
ADD_ADDR = address(new Add());
MULTIPLY_ADDR = address(new Multiply());
}
function facetAddress(bytes4 selector) public view returns (address) {
if (selector == 0x771602f7) return ADD_ADDR;
else if (selector == 0x165c4a16) return MULTIPLY_ADDR;
else if (selector == 0x2f8cd8b1) return MULTIPLY_ADDR;
else return address(0);
}
function facetAddresses() public view returns (address[] memory addresses) {
addresses[0] = ADD_ADDR;
addresses[1] = MULTIPLY_ADDR;
}
// ┌────────────────────┐
// │ │
// │ THIS CODE IS NEW │
// │ │
// └────────────────────┘
function facetFunctionSelectors(address _facet) public view returns (bytes4[] memory) {
if (_facet == ADD_ADDR) {
bytes4[] memory facetFunctionSelectors_ = new bytes4[](1);
facetFunctionSelectors_[0] = 0x771602f7;
return facetFunctionSelectors_;
}
else if (_facet == MULTIPLY_ADDR) {
bytes4[] memory facetFunctionSelectors_ = new bytes4[](2);
facetFunctionSelectors_[0] = 0x165c4a16;
facetFunctionSelectors_[1] = 0x2f8cd8b1;
return facetFunctionSelectors_;
}
else {
bytes4[] memory facetFunctionSelectors_ = new bytes4[](0);
return facetFunctionSelectors_;
}
}
// --------
fallback() external payable {
address facet = facetAddress(msg.sig);
require(facet != address(0), "Function does not exist");
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
4/4 facets()
Finalmente, el EIP-2535 exige una función facets() que no toma argumentos y devuelve una lista de structs donde cada struct contiene una dirección de facet y una lista de los function selectors de ese facet.
Tiene la siguiente firma:
struct Facet {
address facetAddress;
bytes4[] functionSelectors;
}
function facets() external view returns (Facet[] memory facets_);
La información que devuelve facets() se puede obtener de la siguiente manera:
- llamando primero a
facetAddresses()para obtener todas las direcciones de facets- iterando a través de cada dirección de facet y poniendo esa dirección en el campo
facetAddressdel structFacet- y llamando a
facetFunctionSelectors()en la dirección y poniendo la lista de function selectors en el campofunctionSelectorsdel struct.
- y llamando a
- iterando a través de cada dirección de facet y poniendo esa dirección en el campo
Una alternativa es hardcodear la respuesta directamente en la función, lo que puede ser más eficiente en algunos casos (para los diamonds actualizables, el hardcoding obviamente no funcionará). En nuestro diamond, implementaremos facets() usando el método de bucle, como se muestra a continuación. Desplácese hasta la parte inferior de este bloque de código para ver el nuevo código:
contract Add {
// selector: 0x771602f7
function add(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x + y;
}
}
contract Multiply {
// selector: 0x165c4a16
function multiply(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x * y;
}
// selector: 0x2f8cd8b1
function exponent(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x ** y;
}
}
contract Diamond {
address immutable ADD_ADDR;
address immutable MULTIPLY_ADDR;
constructor() {
ADD_ADDR = address(new Add());
MULTIPLY_ADDR = address(new Multiply());
}
function facetAddress(bytes4 selector) public view returns (address) {
if (selector == 0x771602f7) return ADD_ADDR;
else if (selector == 0x165c4a16) return MULTIPLY_ADDR;
else if (selector == 0x2f8cd8b1) return MULTIPLY_ADDR;
else return address(0);
}
function facetAddresses() public view returns (address[] memory) {
address[] memory addresses = new address[](2);
addresses[0] = ADD_ADDR;
addresses[1] = MULTIPLY_ADDR;
return addresses;
}
function facetFunctionSelectors(address _facet) public view returns (bytes4[] memory) {
if (_facet == ADD_ADDR) {
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = 0x771602f7;
return selectors;
}
else if (_facet == MULTIPLY_ADDR) {
bytes4[] memory selectors = new bytes4[](2);
selectors[0] = 0x165c4a16;
selectors[1] = 0x2f8cd8b1;
return selectors;
}
// Return empty array for unknown facets
return new bytes4[](0);
}
// ┌────────────────────┐
// │ │
// │ THIS CODE IS NEW │
// │ │
// └────────────────────┘
struct Facet {
address facetAddress;
bytes4[] functionSelectors;
}
function facets() public view returns (Facet[] memory) {
address[] memory fa = facetAddresses();
Facet[] memory _facets = new Facet[](2);
for (uint256 i = 0; i < fa.length; i++) {
_facets[i].facetAddress = fa[i];
_facets[i].functionSelectors = facetFunctionSelectors(fa[i]);
}
return _facets;
}
// --------
fallback() external payable {
address facet = facetAddress(msg.sig);
require(facet != address(0), "Function does not exist");
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
Habiendo implementado estas cuatro funciones públicas, ahora hemos implementado todas las funciones externas obligatorias de un proxy Diamond.
IDiamondLoupe
Colectivamente, estas cuatro funciones están definidas en una interfaz llamada IDiamondLoupe. Todos los diamonds deben implementar IDiamondLoupe. Se puede recordar que una “loupe” (lupa) es un pequeño cristal de aumento para mirar (“viewing” o ver) diamantes, y cada una de estas funciones es una función view:
interface IDiamondLoupe {
struct Facet {
address facetAddress;
bytes4[] functionSelectors;
}
/// @notice Gets all facet addresses and their four byte function selectors.
/// @return facets_ Facet
function facets() external view returns (Facet[] memory facets_);
/// @notice Gets all the function selectors supported by a specific facet.
/// @param _facet The facet address.
/// @return facetFunctionSelectors_
function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_);
/// @notice Get all the facet addresses used by a diamond.
/// @return facetAddresses_
function facetAddresses() external view returns (address[] memory facetAddresses_);
/// @notice Gets the facet that supports the given selector.
/// @dev If facet is not found return address(0).
/// @param _functionSelector The function selector.
/// @return facetAddress_ The facet address.
function facetAddress(bytes4 _functionSelector) external view returns (address facetAddress_);
}
Todavía estamos a un paso de un Diamond Proxy completamente compatible: registrar los cambios realizados en el diamond. Este es el tema que discutiremos a continuación.
DiamondCut — registrando los selectores de facets
Nuestro ejemplo de diamond en curso no es actualizable. En la práctica, los diamonds son actualizables y pueden cambiar sus facets y los function selectors asociados con ellos.
Cualquier cambio en un facet debe ser registrado; incluso para los diamonds no actualizables (estáticos), los cambios (la adición de facets y function selectors) ocurren en el despliegue y, por lo tanto, deben ser registrados.
La teoría detrás de registrar los selectores de facets es que debería haber dos formas de determinar los function selectors soportados por el diamond:
- Usando las funciones públicas descritas anteriormente, o
- Analizando los logs.
Incluso los facets que están hardcodeados como los nuestros deben registrarse, por lo que se deben emitir eventos. En nuestro caso, la emisión debe ocurrir durante el despliegue. Tales logs son requeridos por el estándar.
Cuando agregamos un facet (o hacemos cualquier alteración como reemplazar o eliminar), esa acción se llama un diamond cut (corte de diamante).
Diamond cut no significa eliminar facets, como podría implicar “cortar”. Corresponde a alterar el diamond de alguna manera. Cualquier cambio a un facet requiere emitir un evento DiamondCut que se define en el siguiente bloque de código. (Uno puede recordar esta nomenclatura para “cut” notando que cuando una gema de diamante real se “corta”, obtiene un lado adicional —o faceta— donde ocurrió el corte).
El evento DiamondCut está definido en una nueva interfaz llamada IDiamond. Los eventos deben ser emitidos cada vez que se agrega, reemplaza o elimina una función de algún facet. La definición del evento DiamondCut se muestra a continuación:
interface IDiamond {
enum FacetCutAction {Add, Replace, Remove}
// Add=0, Replace=1, Remove=2
struct FacetCut {
address facetAddress;
FacetCutAction action;
bytes4[] functionSelectors;
}
event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);
}
Dado que FacetCut[] es una lista, se pueden cambiar múltiples facets a la vez.
A diferencia de otros patrones proxy como Transparent Upgradeable Proxies o UUPS Proxies, los diamonds no tienen un mecanismo para actualizarse cambiando la dirección del facet. Un facet (contrato de implementación) se elimina cuando se eliminan todos los function selectors asociados. Los facets se agregan implícitamente cuando se agrega un function selector con una nueva dirección de implementación.
Los parámetros _init y _calldata sirven para el mismo propósito que los OpenZeppelin Initializers; discutiremos esto más adelante. Si no es necesaria ninguna información de inicialización, entonces _init debería ser address(0) y _calldata debería ser "" o bytes vacíos.
Actualicemos nuestro diamond para emitir este evento en el constructor:
contract Add {
// selector: 0x771602f7
function add(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x + y;
}
}
contract Multiply {
// selector: 0x165c4a16
function multiply(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x * y;
}
// selector: 0x2f8cd8b1
function exponent(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x ** y;
}
}
interface IDiamond {
enum FacetCutAction {Add, Replace, Remove}
// Add=0, Replace=1, Remove=2
struct FacetCut {
address facetAddress;
FacetCutAction action;
bytes4[] functionSelectors;
}
event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);
}
contract Diamond is IDiamond {
address immutable ADD_ADDR;
address immutable MULTIPLY_ADDR;
constructor() {
ADD_ADDR = address(new Add());
MULTIPLY_ADDR = address(new Multiply());
// ┌────────────────────┐
// │ │
// │ THIS CODE IS NEW │
// │ │
// └────────────────────┘
// there are a total of 3 facets:
// [add, [add]]
// [multiply, [multiply, exponent]]
// [this, [facets, facetAddress, facetAddresses, facetFunctionSelectors]]
FacetCut[] memory _diamondCuts = new FacetCut[](3);
enum FacetCutAction {Add, Replace, Remove}
_diamondCuts[0].facetAddress = ADD_ADDR;
bytes4[] memory _addFacets = new bytes4[](1);
_addFacets[0] = 0x771602f7;
// add to _diamondCuts
_diamondCuts[0].action = Add;
_diamondCuts[0].functionSelectors = _addFacets;
_diamondCuts[1].facetAddress = MULTIPLY_ADDR;
bytes4[] memory _mulFacets = new bytes4[](2);
_mulFacets[0] = 0x165c4a16;
_mulFacets[1] = 0x2f8cd8b1;
// add to _diamondCuts
_diamondCuts[1].action = Add;
_diamondCuts[1].functionSelectors = _mulFacets;
// Note that the IDiamondLoupe interface functions are also logged.
_diamondCuts[2].facetAddress = address(this);
bytes4[] memory _loupeFacets = new bytes4[](4);
_loupeFacets[0] = this.facetAddress.selector;
_loupeFacets[1] = this.facetAddresses.selector;
_loupeFacets[2] = this.facets.selector;
_loupeFacets[3] = this.facetFunctionSelectors.selector;
// add to _diamondCuts
_diamondCuts[2].action = Add;
_diamondCuts[2].functionSelectors = _loupeFacets;
emit DiamondCut(_diamondCuts, address(0), "");
// --------
}
function facetAddress(bytes4 selector) public view returns (address) {
if (selector == 0x771602f7) return ADD_ADDR;
else if (selector == 0x165c4a16) return MULTIPLY_ADDR;
else if (selector == 0x2f8cd8b1) return MULTIPLY_ADDR;
else return address(0);
}
function facetAddresses() public view returns (address[] memory) {
address[] memory addresses = new address[](2);
addresses[0] = ADD_ADDR;
addresses[1] = MULTIPLY_ADDR;
return addresses;
}
function facetFunctionSelectors(address _facet) public view returns (bytes4[] memory) {
if (_facet == ADD_ADDR) {
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = 0x771602f7;
return selectors;
}
else if (_facet == MULTIPLY_ADDR) {
bytes4[] memory selectors = new bytes4[](2);
selectors[0] = 0x165c4a16;
selectors[1] = 0x2f8cd8b1;
return selectors;
}
// Return empty array for unknown facets
return new bytes4[](0);
}
struct Facet {
address facetAddress;
bytes4[] functionSelectors;
}
function facets() public view returns (Facet[] memory) {
address[] memory fa = facetAddresses();
Facet[] memory _facets = new Facet[](2);
for (uint256 i = 0; i < fa.length; i++) {
_facets[i].facetAddress = fa[i];
_facets[i].functionSelectors = facetFunctionSelectors(fa[i]);
}
return _facets;
}
fallback() external payable {
address facet = facetAddress(msg.sig);
require(facet != address(0), "Function does not exist");
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
receive() external payable {}
}
Ahora tenemos un proxy Diamond completamente compatible con el EIP-2535. Normalmente, las funciones de IDiamondLoupe se almacenan en un facet separado, pero por simplicidad las hemos puesto en el diamond para mantener las cosas simples por ahora.
Independientemente de dónde se almacenen las funciones de IDiamondLoupe —ya sea en el propio diamond o en otro facet—, el estándar requiere emitir su dirección, la acción que tuvo lugar (agregar un facet) y los function selectors.
Duplicación entre IDiamondLoupe y los eventos de DiamondCut
Uno de los aspectos controvertidos de este EIP es el hecho de que los function selectors se pueden determinar tanto analizando los logs pasados como llamando a las funciones view en IDiamondLoupe. Esto crea lógica duplicada para lograr lo mismo.
La razón detrás de exponer los mismos datos a través de funciones públicas es que facilita la integración con exploradores de bloques y otros sistemas externos. Además, los scripts de actualización pueden verificar atómicamente si un function selector ya existe antes de registrar uno nuevo. La intención detrás de los eventos DiamondCut es mostrar un historial de actualizaciones.
En el ERC-1967, un explorador de bloques puede consultar el storage slot e identificar inmediatamente dónde está el contrato lógico; el explorador de bloques no necesita analizar los logs emitidos por el ERC-1967, que contienen la misma información.
Haciendo el Diamond Actualizable, Implementando la Función diamondCut
El estándar EIP-2535 sugiere agregar una función diamondCut() que se muestra a continuación, mediante la cual se pueden agregar, cambiar o eliminar facets.
interface IDiamondCut is IDiamond {
/// @notice Add/replace/remove any number of functions and optionally execute
/// a function with delegatecall
/// @param _diamondCut Contains the facet addresses and function selectors
/// @param _init The address of the contract or facet to execute _calldata
/// @param _calldata A function call, including function selector and arguments
/// _calldata is executed with delegatecall on _init
function diamondCut(
FacetCut[] calldata _diamondCut,
address _init,
bytes calldata _calldata
) external;
}
El estándar no requiere que la función de actualización se llame diamondCut() o que implemente la firma anterior. Un desarrollador podría usar su propia función changeTheFacets() por ejemplo, pero esa función debe emitir el evento DiamondCut de acuerdo con el tipo de actualización de facet que realizó.
Estructuras de datos para facets y selectores
Para almacenar los function selectors y los facets, como mínimo queremos un mapping desde los function selectors hacia la dirección del facet:
mapping(bytes4 => address) facetToSelectors;
Esta estructura de datos nos permite:
- Averiguar a qué contrato de implementación hacer delegatecall basándose en el
msg.sigrecibido. - Determinar si un nuevo function selector puede ser agregado sin colisionar con un function selector preexistente. Debemos requerir
facetToSelectors[selector] == address(0). - Determinar si un selector puede ser reemplazado o eliminado. Usando la misma comprobación, podemos determinar si estamos reemplazando o eliminando un selector inexistente (esta operación debería hacer revert).
Implementando las funciones para IDiamondLoupe
Como resumen, aquí están las funciones view en IDiamondLoupe que necesitamos soportar:
facetAddress(bytes4 selector)dado un function selector, devuelve el facetfacetAddresses()devuelve todas las direcciones de los facetsfacetFunctionSelectors(address facet)dada una dirección, devuelve todos los function selectorsfacets()devuelve todas las direcciones de los facets y sus function selectors
Así es como implementamos cada una:
- La función
facetAddress(bytes4 selector)puede ser simplemente una función view para el mappingfacetToSelectors, o podríamos hacer que el mapping sea público. - Para devolver todas las direcciones en
facetAddresses(), debemos optar por:- Almacenar explícitamente todas las direcciones en una lista.
- Almacenar explícitamente todos los function selectors en una lista, iterar a través de los function selectors, y llamar a
facetAddress(bytes4 selector)en cada uno, luego construir una lista de direcciones únicas.
- Para devolver todos los function selectors de un facet a través de
facetFunctionSelectors(address facet)debemos optar por:- Crear un mapping
mapping(address facet => bytes4[])que almacene la lista de function selectors asociada con cada mapping. - Mantener un array de todos los function selectors, iterar a través de todo ese array y llamar a
facetAddress(bytes4 selector)en cada uno. Agregar la dirección a una lista si elfacetdevuelto es elfaceten el argumento defacetFunctionSelectors.
- Crear un mapping
- Dado que
facets()devuelve la misma información que iterar a través defacetAddresses()y llamar afacetFunctionSelectors(address facet)en cada dirección, omitimos una mayor discusión sobre la implementación defacets().
Hay un trade-off fundamental. Si usamos más estructuras de datos, será más barato llamar a las funciones view porque no tienen que “reconstruir” los datos que estas funciones devuelven, pero se necesitarán actualizar más estructuras de datos durante una actualización. Así que debemos tener:
- La actualización es más barata, pero llamar a las funciones view on-chain es más caro.
- Llamar a la función view es más barato, pero actualizar un facet requiere más contabilidad (mayor costo de gas).
Es extremadamente poco común llamar a cualquiera de las funciones view de IDiamondLoupe on-chain, ya que están destinadas para su consumo off-chain. Por lo tanto, se prefiere optar por menos estructuras de datos.
Variables de almacenamiento para facets y selectores — y evitando las colisiones de almacenamiento
Como se ha visto, para almacenar la información de los selectores y las direcciones, necesitamos al menos un mapping como:
mapping(bytes4 => address) facetToAddress;
en el diamond, pero entonces cualquier mapping dentro de un facet asignado al primer storage slot colisionará potencialmente con este mapping.
Las colisiones de almacenamiento en el proxy Diamond son más complicadas que en otros patrones proxy, porque las colisiones pueden ocurrir no solo en las actualizaciones del mismo contrato de implementación, sino también entre facets.
El patrón Diamond no especifica cómo debe manejarse el almacenamiento. Una forma sencilla de manejar las colisiones es usando storage namespaces. Se puede encontrar una explicación detallada del uso de namespaces en EIP-7201 storage namespaces.
Como repaso, en los storage namespaces, las variables de estado de un contrato se agrupan en un struct, y la base de este struct se almacena en un slot pseudoaleatorio, típicamente determinado por el hash de un string. Como resultado, cada contrato tiene su propio base storage slot, haciendo que las colisiones de almacenamiento sean altamente improbables.
El EIP-7201 se derivó de una solución anterior propuesta por el patrón diamond llamada “diamond storage”. El EIP-2535 también propuso otro patrón llamado “App Storage”. Sin embargo, el EIP-2535 no dicta cómo se debe manejar el almacenamiento, por lo que simplemente referimos al lector a una solución viable, que es usar EIP-7201. El lector interesado puede aprender sobre los patrones “diamond storage” y “app storage” directamente desde el EIP, ambos recomendados por el autor del EIP.
El almacenamiento mínimo para operar el diamond
Si decidimos mantener el almacenamiento mínimo requerido para operar el diamond, el namespace para el proxy debería ser un struct con los siguientes campos:
- Necesitamos una lista de selectores, es decir
bytes4[] selectors. Cada vez que agregamos un selector, escaneamos a través de esta lista para asegurarnos de que no estamos agregando un selector previamente existente. - Necesitamos al menos un mapping de selectores a direcciones. Sin embargo, sería útil mapear también los selectores a su índice en
selectors. De esa manera, cuando eliminamos un selector, podemos buscar rápidamente su índice en el array. Luego, intercambiamos esa entrada con la última entrada y hacemos un pop en la lista. Por lo tanto, en lugar de almacenarselector => addressalmacenamos un struct que contiene la dirección y la posición del selector en el array. Por consiguiente, nuestro mapping contieneselector => (address, index_in_selectors).
El código a continuación implementa los dos puntos anteriores:
selectorses simplemente la lista de selectores- El struct
FacetAddressAndSelectorPositionalmacena la facetAddress y dónde está el índice del selector enselectors
struct FacetAddressAndSelectorPosition {
address facetAddress;
uint16 selectorPosition; // index of the selector in `selectors`
}
struct DiamondStorage {
mapping(bytes4 => FacetAddressAndSelectorPosition) facetAddressAndSelectorPosition;
bytes4[] selectors;
}
Al struct se puede acceder usando el mismo patrón del EIP-7201.
LibDiamond para manejar el almacenamiento
Para mantener las cosas simples, el struct que contiene la información anterior y la función para establecer el puntero de almacenamiento hacia el struct se pueden mantener en una librería separada llamada LibDiamond. La librería proporciona una función diamondStorage() que devuelve un puntero al struct y facetAddress(bytes4 selector). Definir facetAddress en esta librería es opcional y puramente por conveniencia.
// ┌────────────────────┐
// │ │
// │ CODE FOR STORAGE │
// │ │
// └────────────────────┘
library LibDiamond {
// keccak256(abi.encode(uint256(keccak256("diamond.storage")) - 1)) & ~bytes32(uint256(0xff));
bytes32 constant DIAMOND_STORAGE_POSITION = 0xd7ce2c87e6a71bef91a0dfa43113050aa4eae7c1a7c451ae61d9077904d7cd00;
struct DiamondStorage {
mapping(bytes4 => FacetAddressAndSelectorPosition) facetAddressAndSelectorPosition;
bytes4[] selectors;
}
function diamondStorage()
internal
pure
returns (DiamondStorage storage ds) {
bytes32 position = DIAMOND_STORAGE_POSITION;
assembly {
// change the slot of the storage pointer
ds.slot := position
}
}
function facetAddress(bytes4 _functionSelector)
external
override
view
returns (address facetAddress_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
facetAddress_ = ds.facetAddressAndSelectorPosition[_functionSelector].facetAddress;
}
}
Para diamonds actualizables, es convencional mantener las funciones de IDiamondLoupe en su propio facet, y no en el propio proxy. No hay un requisito sobre cómo nombrar a este contrato, pero DiamondLoupeFacet es razonablemente descriptivo. A continuación mostramos DiamondLoupeFacet usando la librería LibDiamond para implementar la función externa facetAddress que es parte de IDiamondLoupe.
import { LibDiamond } from "./libraries/LibDiamond.sol";
// ┌─────────────────────┐
// │ │
// │ DiamondLoupeFacet │
// │ │
// └─────────────────────┘
contract DiamondLoupeFacet is IDiamondLoupe {
/// @notice Gets the facet address that supports the given selector.
/// @dev If facet is not found return address(0).
/// @param _functionSelector The function selector.
/// @return facetAddress_ The facet address.
function facetAddress(bytes4 _functionSelector)
external
override
view
returns (address facetAddress_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
facetAddress_ = ds.facetAddressAndSelectorPosition[_functionSelector].facetAddress;
}
// other functions not shown
}
Implementaciones de Referencia del Estándar Diamond
Nick Mudge, el autor del EIP-2535, mantiene tres implementaciones de referencia (diamond-1, diamond-2 y diamond-3) en el siguiente repositorio:
https://github.com/mudgen/diamond
Estas implementaciones se optimizan para los trade-offs que discutimos anteriormente: si las funciones view en IDiamondLoupe son baratas de consultar on-chain, serán caras de actualizar y viceversa.
Diamond-1 y diamond-2 usan el menor almacenamiento posible para rastrear los facets y selectores, usando únicamente una lista de function selectors y un mapping desde el function selector hacia la dirección del facet. A continuación vemos el almacenamiento para diamond-1.
Tenga en cuenta que la implementación de referencia implementa el array de function selectors como un mapping de slots de selectores uint256 => bytes32 para empaquetar 8 function selectors en un solo slot. Los mappings son un poco más eficientes en gas que los arrays porque no verifican implícitamente la longitud del array antes de hacer una búsqueda. La longitud de este “array” se almacena por separado como selectorCount.
struct DiamondStorage {
// function selector => facet address and selector position in selectors array
mapping(bytes4 => FacetAddressAndSelectorPosition) facetAddressAndSelectorPosition;
bytes4[] selectors;
mapping(bytes4 => bool) supportedInterfaces;
// owner of the contract
address contractOwner;
}
Diamond-3, por otro lado, almacena explícitamente las direcciones de facets y un mapping desde la dirección del facet hacia la lista de function selectors que almacena:
struct FacetAddressAndPosition {
address facetAddress;
uint96 functionSelectorPosition; // position in facetFunctionSelectors.functionSelectors array
}
struct FacetFunctionSelectors {
bytes4[] functionSelectors;
uint256 facetAddressPosition; // position of facetAddress in facetAddresses array
}
struct DiamondStorage {
// maps function selector to the facet address and
// the position of the selector in the facetFunctionSelectors.selectors array
mapping(bytes4 => FacetAddressAndPosition) selectorToFacetAndPosition;
// maps facet addresses to function selectors
mapping(address => FacetFunctionSelectors) facetFunctionSelectors;
// facet addresses
address[] facetAddresses;
// Used to query if a contract implements an interface.
// Used to implement ERC-165.
mapping(bytes4 => bool) supportedInterfaces;
// owner of the contract
address contractOwner;
}
La implementación de DiamondLoupe para diamond-3 es muy simple ya que es solo un envoltorio (thin wrapper) ligero sobre esas variables de almacenamiento:
contract DiamondLoupeFacet is IDiamondLoupe, IERC165 {
// Diamond Loupe Functions
////////////////////////////////////////////////////////////////////
/// These functions are expected to be called frequently by tools.
//
// struct Facet {
// address facetAddress;
// bytes4[] functionSelectors;
// }
/// @notice Gets all facets and their selectors.
/// @return facets_ Facet
function facets() external override view returns (Facet[] memory facets_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
uint256 numFacets = ds.facetAddresses.length;
facets_ = new Facet[](numFacets);
for (uint256 i; i < numFacets; i++) {
address facetAddress_ = ds.facetAddresses[i];
facets_[i].facetAddress = facetAddress_;
facets_[i].functionSelectors = ds.facetFunctionSelectors[facetAddress_].functionSelectors;
}
}
/// @notice Gets all the function selectors provided by a facet.
/// @param _facet The facet address.
/// @return facetFunctionSelectors_
function facetFunctionSelectors(address _facet) external override view returns (bytes4[] memory facetFunctionSelectors_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
facetFunctionSelectors_ = ds.facetFunctionSelectors[_facet].functionSelectors;
}
/// @notice Get all the facet addresses used by a diamond.
/// @return facetAddresses_
function facetAddresses() external override view returns (address[] memory facetAddresses_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
facetAddresses_ = ds.facetAddresses;
}
/// @notice Gets the facet that supports the given selector.
/// @dev If facet is not found return address(0).
/// @param _functionSelector The function selector.
/// @return facetAddress_ The facet address.
function facetAddress(bytes4 _functionSelector) external override view returns (address facetAddress_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
facetAddress_ = ds.selectorToFacetAndPosition[_functionSelector].facetAddress;
}
// This implements ERC-165.
function supportsInterface(bytes4 _interfaceId) external override view returns (bool) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
return ds.supportedInterfaces[_interfaceId];
}
}
Las funciones view para diamond-1 y diamond-2 son más complejas porque tienen que iterar a través de todos los function selectors para listar las direcciones de los facets, y luego solo devolver las direcciones únicas.
Desplegando un diamond
El proceso de despliegue y actualización de un diamond es el mismo que el de otros patrones de proxy actualizables:
Durante el despliegue debemos:
- Desplegar los facets (implementaciones)
- Desplegar el proxy con la lista de facets y selectores como argumentos para el constructor, e inicializar en la misma transacción
Y durante una actualización:
- Desplegar el(los) nuevo(s) facet(s)
- Llamar a diamondCut() o a la función definida por el usuario con la lista de selectores a eliminar y la lista de selectores a agregar. Es posible agregar varios facets nuevos en una sola transacción.
Inicializando variables de almacenamiento durante el despliegue y la actualización
Al desplegar un proxy, es posible que queramos establecer algunas variables de estado iniciales como si tuviéramos un constructor. Del mismo modo, podríamos querer inicializar algunas variables de almacenamiento después de una actualización, de forma similar a upgradeToAndCall en la implementación de OpenZeppelin de Transparent Upgradeable Proxy y UUPS.
Este es el uso para los dos últimos argumentos _init y _calldata en diamondCut:
function diamondCut(Facets[] facets, address _init, bytes memory _calldata)
Si _init ≠ address(0) entonces diamondCut debe hacer delegatecall a _init usando _calldata como argumento. Dado que diamondCut se ejecuta en el contexto del proxy (diamond), el contrato llamado por delegatecall (_init) puede inicializar las variables de almacenamiento en el proxy.
La lógica de inicialización es ejecutada por un contrato externo. Si queremos establecer múltiples variables de almacenamiento en una sola transacción, es más fácil hacer esto como una sola transacción usando un contrato inteligente de propósito especial.
A continuación se muestra un contrato de ejemplo:
import {LibDiamond} from "../libraries/LibDiamond.sol";
contract DiamondInit {
function init() external {
// read the ds struct from storage
// (remember, this executes in the context of the proxy)
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
// write to it
ds.owner = 0x456....;
ds.usdc = 0xa1...;
// ...
}
}
La llamada a diamondCut pasaría la dirección de DiamondInit y la codificación ABI de init().
La inicialización puede ocurrir cada vez que se produce una transacción de diamondCut; no se limita al despliegue inicial de los facets. También podemos hacer un delegatecall a un contrato externo de forma atómica al reemplazar o eliminar un function selector.
Considere requerir un valor mágico con DiamondInit
El EIP-2535 no requiere una comprobación de seguridad de que el contrato DiamondInit sea realmente un contrato, pero ciertamente es más seguro comprobar que la dirección para _init realmente tiene bytecode. Para mayor seguridad, la implementación diamond de ZkSync verifica que el delegatecall al contrato _init devuelva un valor mágico (magic value).
Detalles de implementación para el estándar diamond
La forma más segura de usar el estándar diamond es usar los contratos de referencia enlazados anteriormente, ya que han sido auditados. Si no tiene la intención de llamar a las funciones de IDiamondLoupe on-chain (lo cual suele ser el caso), entonces diamond-2 será el más eficiente en gas, ya que utiliza el menor número de variables de almacenamiento necesarias para cumplir con el ERC-2535.
Diamonds no actualizables — use búsqueda binaria en lugar de un mapping
Para diamonds no actualizables, recomendamos hardcodear la relación entre los function selectors. En lugar de usar una larga declaración if-else para encontrar la dirección del facet, use una búsqueda binaria.
Como ejemplo, considere la siguiente función que toma un function selector como argumento y devuelve una dirección (el código está tomado de Pendle Finance, que usa un Diamond Proxy):

El código hace una búsqueda binaria en el function selector y devuelve la dirección del contrato de implementación (facet) que contiene la función. Eso es todo.
Las direcciones anteriores son direcciones inmutables establecidas en el constructor, que también emite el evento DiamondCut como se requiere:

No use librerías que escriban en variables de almacenamiento a menos que usen EIP-7201
Cualquier facet que escriba o lea de un almacenamiento sin namespaces es propenso a tener una colisión de almacenamiento. Recomendamos usar EIP-7201 para gestionar todo el almacenamiento.
Llamando funciones en otro facet
Cuando una función en un facet (contrato de implementación) se ejecuta, lo hace en el contexto del diamond (el contrato proxy). Por lo tanto, para llamar a una función pública en otro facet, podemos llamar a la dirección del proxy.
Considere nuestro ejemplo inicial donde teníamos un facet Add con una función add() y un facet separado Multiply. Supongamos que quisiéramos llamar a add() desde el facet Multiply. A continuación mostramos cómo lograr eso; el facet Multiply llama a la función add() especificando la dirección del diamond proxy:
interface IAdd {
function add(uint256 x, uint256 y) external view returns (uint256);
}
contract Multiply {
function callAdd(uint256 x, uint256 y) external {
uint256 sum = IAdd(address(this)).add(x, y);
// rest of the code
}
}
Esto hará dos llamadas bajo el capó:
- Primero
callAddse llama a sí misma (en el contexto del proxy) - El proxy empareja el function selector con el facet Add
- El proxy hace un delegatecall al facet Add
Puede ser una sorpresa para algunos desarrolladores que un contrato pueda llamarse a sí mismo de esta manera, pero esto está de hecho permitido por la EVM.
Puede probar el siguiente contrato para demostrar esto:
contract SelfCall {
uint256 public x = 0;
function setToOne() external {
x = 1;
}
function selfCall() external {
SelfCall(address(this)).setToOne();
// alternatively,
// address(this).call(abi.encodeWithSignature("setToOne()"));
}
}
Sin embargo, hacer una llamada a sí mismo (self-call) es un poco un desperdicio. Para ahorrar gas, un facet puede hacer “directamente” un delegatecall al otro facet. Tras bambalinas, la lógica del facet que llama se ejecuta en el proxy, y la lógica está haciendo un delegatecall a otro facet, por lo que ese facet puede “ver” las variables de almacenamiento en el proxy. El código para lograr esto no será tan limpio porque los delegatecalls solo pueden ser una llamada de bajo nivel. Aquí hay un ejemplo tomado del EIP-2535:
// get the mapping from selector => facet address
DiamondStorage storage ds = diamondStorage(); // EIP-7201
// compute the selector
bytes4 functionSelector = bytes4(keccak256("functionToCall(uint256)"));
// get the facet address
address facet = ds.selectorToFacet[functionSelector];
// delegatecall
bytes memory myFunctionCall = abi.encodeWithSelector(functionSelector, 4);
(bool success, bytes memory result) = address(facet).delegatecall(myFunctionCall);
Sin embargo, el código anterior no es elegante y requiere unas tres o cuatro líneas adicionales para hacer una simple llamada.
Una tercera solución es crear una o varias librerías en Solidity que contengan solo funciones internas, y luego importar esas funciones internas dentro de cualquier facet que las necesite. Esto puede llevar a bytecode duplicado entre los facets, pero si los facets no exceden el límite de 24kb ni aumentan excesivamente el costo de despliegue, esto no debería ser un problema importante.
Diamond de Ejemplo
Construiremos un diamond que sirva como contrato contador. Un facet contendrá la función view del valor del contador y otro facet contendrá la lógica para incrementar el contador.
Para ilustrar la lógica de inicialización del diamond, haremos que nuestro contador comience en 8 en lugar de 0.
Para empezar, haga un fork del repositorio de hardhat de referencia diamond-2. No tenemos conocimiento de ninguna implementación en Foundry que esté auditada.
IncrementLibrary
Es útil poner el código para leer el namespace storage en una librería que los facets puedan importar. Poner la lógica para actualizar el contador dentro de la librería es una elección de diseño. Poner la lógica de incremento directamente en la librería aumentará el tamaño del bytecode para cada otro facet que llame a las funciones en esa librería. Sin embargo, también simplifica la lógica para el facet de incremento, ya que el facet de incremento no necesita saber nada sobre cómo está estructurado el namespaced storage.
Tenga en cuenta que todas las funciones deben ser internal en esta librería, ya que el compilador de Solidity espera que las librerías con funciones externas se desplieguen por separado.
pragma solidity ^0.8.0;
library LibInc {
// keccak256(abi.encode(uint256(keccak256("RareSkills.Facet.Increment")) - 1)) ^ bytes32(uint256(0xff))
bytes32 constant STORAGE_LOCATION = 0xfa04c3581a2244f8cd60ed05a316a89d13b0e00f0bfbe2b8a2155985a9d65e00;
struct IncrementStorage {
uint256 x;
}
function incStorage()
internal
pure
returns (IncrementStorage storage iStor) {
bytes32 location = STORAGE_LOCATION;
assembly {
iStor.slot := location
}
}
function x()
internal
view
returns (uint256 x) {
x = incStorage().x;
}
function increment() internal {
incStorage().x++;
}
}
Añada el archivo contracts/libraries/LibInc.sol
Inicializando la variable de almacenamiento con DiamondInit
La inicialización ocurre en un contrato separado al que el contrato de diamondCut() hace delegatecall. En la implementación de referencia, esto se encuentra en contracts/upgradeInitializers/DiamondInit.sol.
No mostramos el contrato completo por brevedad. Añada el siguiente código a DiamondInit.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// ADD THIS LINE
import {LibInc} from "../libraries/LibInc.sol";
//...
contract DiamondInit {
// You can add arguments to this function in order to pass in
// data to set your own state variables
function init() external {
// ...
// ADD THESE LINES
LibInc.IncrementStorage storage _is = LibInc.incStorage();
_is.x = 8; // initialize x to 8
}
}
Facet que solo visualiza x
Cree un nuevo archivo LibIncFacet.sol en contracts/facets/:
pragma solidity ^0.8.0;
import { LibInc } from "../libraries/LibInc.sol";
contract IncViewFacet {
function x() external view returns (uint256 x) {
x = LibInc.x();
}
}
Facet que solo incrementa x
Cree IncrementFacet.sol en contracts/facets:
import { LibInc } from "../libraries/LibInc.sol";
contract IncrementFacet {
function increment() external {
LibInc.increment();
}
}
Agregue el siguiente test
Agregue el siguiente test a test/diamondTest.js
it.only('test increment', async () => {
const incViewInterface = await ethers.getContractAt('IncViewFacet', diamondAddress)
const initialX = await incViewInterface.x()
console.log(initialX.toString())
assert.equal(initialX, 8)
const incrementInterface = await ethers.getContractAt('IncrementFacet', diamondAddress)
await incrementInterface.increment();
const afterX = await incViewInterface.x()
assert.equal(afterX, 9)
});
Actualice scripts/deploy.js para desplegar los nuevos facets
Abra scripts/deploy.js y agregue los nombres de los contratos de los facets. Hardhat es lo suficientemente inteligente como para encontrar los contratos sin especificar las rutas de los archivos. El cambio se muestra en el cuadro rojo a continuación:

Actualice el test
Actualice el hook before en el archivo diamondTest.js de la siguiente manera para desplegar los nuevos facets.
Un pequeño inciso: el compilador de Solidity es capaz de generar automáticamente los function selectors de las funciones públicas/externas. Por ejemplo, supongamos que tenemos el siguiente contrato almacenado en C.sol:
contract C {
function foo() public {}
function bar() external {}
}
Si ejecutamos solc --hashes C.sol en el contrato, obtendremos la siguiente salida:
======= C.sol:C =======
Function signatures:
febb0f7e: bar()
c2985578: foo()
Los scripts proporcionados en este repositorio extraen los function selectors por nosotros usando esta técnica, lo que nos ahorra la molestia de especificar explícitamente los function selectors nosotros mismos.
Una vez más, Hardhat es capaz de encontrar los contratos sin especificar las rutas de los archivos:

Ejecute el test con:
npx hardhat test
Tenga en cuenta que los otros tests fallarán porque no esperan los nuevos selectores. Ignoramos esto usando el modificador .only en nuestro test. El modificador .only evita que se ejecuten los otros tests unitarios y ejecuta únicamente el test unitario modificado con .only.
Si se encuentra con problemas, asegúrese de estar usando la versión 20 de Node.
Resumen
- El patrón diamond es un proxy que tiene múltiples contratos de implementación.
- El diamond sabe a qué facet hacer delegatecall basándose en el function selector del calldata entrante.
- Si el diamond es actualizable, el mapping del selector a la dirección de implementación se puede cambiar a través de diamondCut. La lógica para cambiar este mapping no está dictada por el estándar, puede ser parte del bytecode del proxy o parte de un facet.
- Todos los facets y selectores en el diamond deben ser determinables a través de los eventos emitidos y las funciones públicas en IDiamondLoupe.
- Los costos de gas para las funciones view en
IDiamondLoupese pueden reducir agregando más estructuras de datos. Sin embargo, esto aumenta el costo de actualización. En casi todos los casos, deberíamos optar por menos estructuras de datos porque las funciones deIDiamondLoupeestán destinadas para usuarios off-chain.
Nos gustaría agradecer a Nick Mudge por proporcionar comentarios sobre una versión anterior de este artículo.