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 selector de función 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 de los contratos inteligentes a 24kb. Si el desarrollador necesita desplegar hasta 48kb de bytecode, una solución viable es usar el patrón fallback-extension. 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 diamante (diamond) y los “contratos de implementación” se llaman “facetas” (facets). Tener dos términos que se refieren a lo mismo ha generado confusión, así que queremos dejar este punto claro ahora:
- diamond = contrato proxy
- facet = contrato de implementación
Un diamante (el proxy) puede ser actualizado cambiando uno o más de los contratos de implementación (facetas). Alternativamente, un diamante puede ser no actualizable (inmutable) si no admite un mecanismo para cambiar las facetas (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 tratar 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 tomaremos partido en el debate ni emitiremos un juicio sobre su complejidad aquí).
La especificación en sí es bastante pequeña. Requiere cuatro funciones públicas de tipo view; pero si el diamante es actualizable, se requiere una quinta función que modifique 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 lo pequeña que es la especificación, implementar esas cuatro (o cinco) funciones es considerablemente más complejo que en otros patrones de proxy.
Sin embargo, con los conocimientos previos 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 Libro de Patrones Proxy. Si no has leído esos capítulos o no estás familiarizado con esos temas, aprender el patrón diamond será una tarea difícil, así que asegúrate de tener las bases necesarias.
A continuación, se presentan algunos problemas que el patrón debe resolver:
- Cuando el diamante recibe una transacción, ¿cómo sabe a qué contrato de implementación llamar?
- Si una faceta (contrato de implementación) se actualiza, ¿cómo sabe el contrato proxy (el diamante) qué función admite la nueva faceta (contrato de implementación) y, potencialmente, qué funciones ya no son compatibles?
- ¿Dónde debería estar la lógica de actualización: en el bytecode del proxy o en una faceta?
- Dado que cada contrato de implementación no conoce directamente a las otras implementaciones, ¿cómo se pueden evitar las colisiones de almacenamiento?
- ¿Cómo puede un actor externo saber qué funciones son compatibles con 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. Ten en cuenta que el ERC-2535 no exige que un diamond proxy sea actualizable. El proxy puede tener contratos de implementación hardcodeados y seguir siendo un diamante válido.
Para simplificar las cosas al principio, comenzaremos mostrando el diamante inmutable.
Diamante inmutable
Un diamante inmutable, también llamado diamante estático o diamante de corte único 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 diamante actualizable se vuelva inmutable eliminando la funcionalidad de actualización, pero hablaremos de los diamantes actualizables más adelante).
El patrón diamond como contrato proxy
El código para la porción del proxy del diamond proxy debería resultar familiar si se compara con el OpenZeppelin Proxy utilizado por UUPS y el 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 selector de función) usando msg.sig, y luego usa facetAddress() para determinar la dirección de la faceta que implementa la función (ten en cuenta que facetAddress() podría ser parte de la lógica del proxy o vivir dentro de otra faceta y ser llamada mediante delegatecall, esto se discutirá más adelante). Luego, el proxy hace un delegatecall a esa dirección con el mismo calldata que recibió.

El EIP-2535 no especifica cómo mapear los selectores de función a las direcciones de los contratos de implementación. Para un diamante estático, una solución razonable es hardcodear la relación. Este será el enfoque que tomaremos en la siguiente sección.
Para el diamante actualizable, hardcodear los selectores claramente no es una opción, y debemos depender de mapeos, como veremos en las secciones correspondientes.
Bifurcación condicional basada en el selector de función
A continuación, mostramos un contrato proxy con dos contratos de implementación (facetas):
- 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(), las cuales hacen lo que sugieren sus nombres.
El siguiente código muestra un solo proxy que utiliza dos contratos de implementación. La función facetAddress() en Diamond toma msg.sig y devuelve la dirección de la faceta que implementa la función con esa firma, si existe. El siguiente código aún no cumple con el estándar diamond, pero evolucionaremos el código para que lo cumpla.
// 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, revertimos con la cadena "Function does not exist", pero un error personalizado sería más eficiente en gas.
Por simplicidad, nuestro diamante despliega las dos facetas en el constructor, pero esto no se hace en la práctica. En la práctica, las facetas se despliegan por separado, y el proxy es “informado” de ellas más tarde de alguna manera, como a través de los argumentos del constructor o de una función separada.
Algunos diamantes implementan las facetas como bibliotecas con funciones externas en lugar de contratos, pero el EIP-2535 no requiere que las facetas sean bibliotecas o contratos.
Por simplicidad, utilizamos una serie de declaraciones else-if para emparejar el selector de función con la dirección de la faceta, pero esto no es eficiente en gas si hay muchas opciones. Para un diamante estático, ordenar los selectores de antemano 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 faceta. 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 prevista, o al menos a una función que no revierta. Ten en cuenta que un atacante puede desencadenar esta función tanto sin enviar calldata como enviando 0x00000000 como calldata, por lo que la lógica debe manejar ambas situaciones de manera elegante.
Para que nuestro contrato cumpla con el EIP-2535, debemos implementar cuatro funciones públicas obligatorias de tipo view, cada una de las cuales se discute a continuación.
Cuatro funciones públicas View obligatorias
1/4 facetAddress()
Nota 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 todas las facetas utilizadas por el diamante; en otras palabras, una lista de direcciones. La firma es la siguiente:
function facetAddresses() public view returns (address[] memory addresses);
Dado que las facetas en nuestro diamante no pueden cambiar, simplemente hardcodeamos la lista de direcciones de facetas:
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 obligatorio en la lista de direcciones (direcciones de facetas) devuelta por facetAddresses().
3/4 facetFunctionSelectors()
Dada una dirección de faceta como argumento, facetFunctionSelectors() devuelve los selectores de todas las funciones públicas de la faceta. 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 la dirección de una faceta y una lista de los selectores de función de esa faceta.
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:
- primero, llamando a
facetAddresses()para obtener todas las direcciones de las facetas- iterando sobre cada dirección de faceta y colocando esa dirección en el campo
facetAddressdel structFacet- y llamando a
facetFunctionSelectors()en la dirección y colocando la lista de selectores de función en el campofunctionSelectorsdel struct.
- y llamando a
- iterando sobre cada dirección de faceta y colocando 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 diamantes actualizables, hardcodear obviamente no funcionará). En nuestro diamante, implementaremos facets() usando el método del bucle, como se muestra a continuación. Desplázate 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
En conjunto, estas cuatro funciones están definidas en una interfaz llamada IDiamondLoupe. Todos los diamantes deben implementar IDiamondLoupe. Se puede recordar que una “lupa” (loupe) es un pequeño cristal de aumento para mirar (“ver”) diamantes, y cada una de estas funciones son funciones 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 con el estándar: registrar (log) los cambios realizados en el diamante. Este es el tema que discutiremos a continuación.
DiamondCut — registrando los selectores de las facetas
El diamante de nuestro ejemplo en curso no es actualizable. En la práctica, los diamantes son actualizables y pueden cambiar sus facetas y los selectores de función asociados a ellas.
Cualquier cambio en una faceta debe registrarse; incluso para diamantes no actualizables (estáticos), los cambios (la adición de facetas y selectores de funciones) ocurren en el despliegue y, por lo tanto, deben ser registrados.
La teoría detrás del registro de los selectores de facetas es que debe haber dos formas de determinar los selectores de función soportados por el diamante:
- Usando las funciones públicas descritas anteriormente, o
- Analizando los logs.
Incluso las facetas que están hardcodeadas como las nuestras deben registrarse, por lo que se deben emitir eventos. En nuestro caso, la emisión debe ocurrir durante el despliegue. Dichos logs son requeridos por el estándar.
Cuando agregamos una faceta (o hacemos cualquier alteración, como reemplazar o eliminar), esa acción se llama diamond cut (corte de diamante).
Un corte de diamante (diamond cut) no significa eliminar facetas, como podría insinuar la palabra “corte”. Corresponde a alterar el diamante de alguna manera. Cualquier cambio en una faceta requiere la emisión de un evento DiamondCut, el cual se define en el siguiente bloque de código. (Uno puede recordar esta nomenclatura para “corte” 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 una función es agregada, reemplazada o eliminada de alguna faceta. 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 facetas a la vez.
A diferencia de otros patrones de proxy como los Transparent Upgradeable Proxies o los UUPS Proxies, los diamantes no tienen un mecanismo para actualizarse cambiando la dirección de la faceta. Una faceta (contrato de implementación) se elimina cuando se eliminan todos los selectores de función asociados a ella. Las facetas se agregan implícitamente cuando se agrega un selector de función con una nueva dirección de implementación.
Los parámetros _init y _calldata cumplen el mismo propósito que los OpenZeppelin Initializers; discutiremos esto con más detalle 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 diamante 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 EIP-2535. Normalmente, las funciones de IDiamondLoupe se almacenan en una faceta separada, pero por simplicidad las hemos puesto en el diamante para mantener las cosas simples por ahora.
Independientemente de dónde se almacenen las funciones de IDiamondLoupe (ya sea en el propio diamante o en otra faceta), el estándar requiere emitir su dirección, la acción que tuvo lugar (agregar una faceta) y los selectores de función.
Duplicación entre IDiamondLoupe y los eventos de DiamondCut
Uno de los aspectos controvertidos de este EIP es el hecho de que los selectores de función pueden determinarse tanto analizando los logs pasados como llamando a las funciones view en IDiamondLoupe. Esto crea lógica duplicada para lograr lo mismo.
La justificació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 selector de función 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 slot de almacenamiento e identificar de inmediato 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 diamante 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 facetas.
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 exige 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 faceta que haya realizado.
Estructuras de datos para facetas y selectores
Para almacenar los selectores de función y las facetas, como mínimo queremos un mapeo desde los selectores de función a la dirección de la faceta:
mapping(bytes4 => address) facetToSelectors;
Esta estructura de datos nos permite:
- Averiguar a qué contrato de implementación hacer el delegatecall basándose en el
msg.sigrecibido. - Determinar si se puede agregar un nuevo selector de función sin chocar con un selector de función 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 revertirse).
Implementando las funciones para IDiamondLoupe
Como repaso, aquí están las funciones view en IDiamondLoupe que debemos soportar:
facetAddress(bytes4 selector)facetAddresses()facetFunctionSelectors(address facet)facets()
A continuación se explica cómo implementamos cada una:
- La función
facetAddress(bytes4 selector)puede ser simplemente una función view para el mapeofacetToSelectors, o podríamos hacer que el mapeo 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 selectores de función en una lista, iterar sobre los selectores de función y llamar a
facetAddress(bytes4 selector)en cada uno de ellos, y luego construir una lista de direcciones únicas.
- Para devolver todos los selectores de función de una faceta a través de
facetFunctionSelectors(address facet)debemos optar por:- Crear un mapeo
mapping(address facet => bytes4[])que almacene la lista de selectores de función asociados con cada dirección. - Mantener un array de todos los selectores de función, iterar sobre todo ese array y llamar a
facetAddress(bytes4 selector)en cada uno de ellos. Agregar la dirección a una lista si lafacetdevuelta es lafaceten el argumento defacetFunctionSelectors.
- Crear un mapeo
- 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().
Existe un compromiso (trade-off) fundamental. Si utilizamos más estructuras de datos, será más barato llamar a las funciones view porque no tienen que ‘reconstruir’ los datos que devuelven, pero será necesario actualizar más estructuras de datos durante una actualización. Así que debemos elegir entre:
- La actualización es más barata, pero llamar a las funciones view en la cadena (on-chain) es más costoso.
- Llamar a la función view es más barato, pero actualizar una faceta requiere más contabilidad (mayor costo de gas).
Es extremadamente inusual llamar a cualquiera de las funciones view de IDiamondLoupe on-chain, ya que están destinadas para consumo fuera de la cadena (off-chain). Por lo tanto, se prefiere optar por menos estructuras de datos.
Variables de almacenamiento para facetas y selectores — y cómo evitar colisiones de almacenamiento
Como hemos visto, para almacenar la información de los selectores y direcciones, necesitamos al menos un mapeo como:
mapping(bytes4 => address) facetToAddress;
en el diamante, pero entonces cualquier mapeo dentro de una faceta asignado al primer slot de almacenamiento chocará potencialmente con este mapeo.
Las colisiones de almacenamiento en el proxy Diamond son más complicadas que en otros patrones de proxy, porque las colisiones pueden ocurrir no solo en actualizaciones del mismo contrato de implementación, sino también entre facetas.
El patrón Diamond no especifica cómo debe gestionarse el almacenamiento. Una forma sencilla de manejar las colisiones es usar namespaces (espacios de nombres) de almacenamiento. Una explicación detallada sobre el uso de namespaces se puede encontrar en los namespaces de almacenamiento del EIP-7201.
A modo de repaso, en los namespaces de almacenamiento, 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 slot base de almacenamiento, lo que hace que las colisiones de almacenamiento sean altamente improbables.
El EIP-7201 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 debe gestionarse el almacenamiento, así que simplemente referimos al lector a una solución viable, que es usar el 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 diamante
Si elegimos mantener el almacenamiento mínimo necesario para operar el diamante, 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, examinamos esta lista para asegurarnos de que no estamos agregando un selector que ya existe. - Necesitamos al menos un mapeo de selectores a direcciones. Sin embargo, también sería útil mapear 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 eliminamos la última de la lista (pop). Así que en lugar de almacenarselector => addressalmacenamos un struct que contiene la dirección y la posición del selector en el array. Por lo tanto, nuestro mapeo contieneselector => (address, index_in_selectors).
El siguiente código implementa los dos puntos anteriores:
selectorses simplemente la lista de selectores- El struct
FacetAddressAndSelectorPositionalmacena lafacetAddressy 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 utilizando el mismo patrón del EIP-7201.
LibDiamond para gestionar el almacenamiento
Para simplificar las cosas, el struct que contiene la información anterior y la función para configurar el puntero de almacenamiento hacia el struct se pueden mantener en una biblioteca separada llamada LibDiamond. La biblioteca proporciona una función diamondStorage() que devuelve un puntero al struct y facetAddress(bytes4 selector). Definir facetAddress en esta biblioteca 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 los diamantes actualizables, es convencional mantener las funciones de IDiamondLoupe en su propia faceta, y no en el propio proxy. No hay ningún requisito sobre cómo nombrar a este contrato, pero DiamondLoupeFacet es razonablemente descriptivo. A continuación mostramos DiamondLoupeFacet utilizando la biblioteca LibDiamond para implementar la función externa facetAddress que forma 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 optimizan los compromisos 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 las facetas y selectores, utilizando solo una lista de selectores de función y un mapeo del selector de función a la dirección de la faceta. A continuación vemos el almacenamiento para diamond-1.
Ten en cuenta que la implementación de referencia almacena el array de selectores de función como un mapeo de uint256 => bytes32 slots de selectores para empaquetar 8 selectores de función en un solo slot. Los mapeos son ligeramente más eficientes en gas que los arrays porque no comprueban implícitamente la longitud del array antes de hacer una búsqueda. La longitud de este “array” se almacena de forma separada 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;
}
Por otro lado, Diamond-3 almacena explícitamente las direcciones de las facetas y un mapeo de la dirección de la faceta a la lista de selectores de función 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 simplemente un wrapper delgado 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 selectores de función para listar las direcciones de las facetas, y luego devolver solo las direcciones únicas.
Desplegando un diamante
El proceso de despliegue y actualización de un diamante es el mismo que en otros patrones de proxy actualizables:
Durante el despliegue debemos:
- Desplegar las facetas (implementaciones).
- Desplegar el proxy con la lista de facetas y selectores como argumentos en el constructor, e inicializarlo en la misma transacción.
Y durante una actualización:
- Desplegar la(s) nueva(s) faceta(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 varias facetas nuevas en una sola transacción.
Inicializando variables de almacenamiento durante el despliegue y 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, es posible que queramos inicializar algunas variables de almacenamiento después de una actualización, similar a upgradeToAndCall en la implementación de OpenZeppelin del Transparent Upgradeable Proxy y UUPS.
Este es el uso de 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 un delegatecall a _init usando _calldata como argumento. Dado que diamondCut se ejecuta en el contexto del proxy (diamante), el contrato llamado mediante 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 hacerlo como una sola transacción mediante el uso de 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 siempre que suceda una transacción de diamondCut; no se limita al despliegue inicial de las facetas. También podemos hacer un delegatecall a un contrato externo de forma atómica al reemplazar o eliminar un selector de función.
Considera 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 sin duda es más seguro verificar que la dirección para _init realmente tenga bytecode. Para mayor seguridad, la implementación de diamante 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 utilizar el estándar diamond es emplear los contratos de referencia enlazados anteriormente, ya que estos han sido auditados. Si no tienes la intención de llamar a las funciones IDiamondLoupe on-chain (lo cual es el caso habitual), entonces diamond-2 será el más eficiente en gas, ya que utiliza la menor cantidad de variables de almacenamiento necesarias para cumplir con el ERC-2535.
Diamantes no actualizables — usar búsqueda binaria en lugar de un mapeo
Para los diamantes no actualizables, recomendamos hardcodear la relación entre los selectores de función. En lugar de usar una larga declaración if-else para encontrar la dirección de la faceta, utiliza una búsqueda binaria.
Como ejemplo, considera la siguiente función que toma un selector de función como argumento y devuelve una dirección (el código fue tomado de Pendle Finance, el cual utiliza un Diamond Proxy):

El código realiza una búsqueda binaria en el selector de función y devuelve la dirección del contrato de implementación (faceta) 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 según se requiera:

No utilices bibliotecas que escriban en variables de almacenamiento a menos que utilicen el EIP-7201
Cualquier faceta que escriba o lea de un almacenamiento sin namespace es propensa a tener una colisión de almacenamiento. Recomendamos utilizar el EIP-7201 para gestionar todo el almacenamiento.
Llamando funciones en otra faceta
Cuando una función en una faceta (contrato de implementación) se ejecuta, lo hace en el contexto del diamante (el contrato proxy). Por lo tanto, para llamar a una función pública en otra faceta, podemos llamar a la dirección del proxy.
Considera nuestro ejemplo inicial donde teníamos una faceta Add con una función add() y una faceta separada Multiply. Supongamos que quisiéramos llamar a add() desde la faceta Multiply. A continuación mostramos cómo lograr eso; la faceta 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 realizará dos llamadas en segundo plano:
- Primero
callAddse llama a sí misma (en el contexto del proxy). - El proxy empareja el selector de función con la faceta Add.
- El proxy realiza un delegatecall a la faceta Add.
Puede resultar una sorpresa para algunos desarrolladores que un contrato pueda llamarse a sí mismo de esta manera, pero de hecho esto está permitido por la EVM.
Puedes 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 auto-llamada (self-call) es un poco un desperdicio. Para ahorrar gas, una faceta puede hacer un delegatecall “directamente” a la otra faceta. Detrás de escena, la lógica de la faceta que llama se ejecuta en el proxy, y la lógica está haciendo un delegatecall a otra faceta, por lo que esa faceta 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 llamadas 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 de tres a cuatro líneas adicionales para realizar una simple llamada.
Una tercera solución es crear bibliotecas (libraries) en Solidity que contengan solo funciones internas, y luego importar esas funciones internas dentro de cualquier faceta que las necesite. Esto puede llevar a bytecode duplicado entre facetas, pero si las facetas no exceden el límite de 24kb ni aumentan excesivamente el costo de despliegue, no debería ser un problema mayor.
Diamante de ejemplo
Construiremos un diamante que servirá como un contrato contador. Una faceta contendrá la función view del valor del contador y otra faceta contendrá la lógica para incrementar el contador.
Para ilustrar la lógica de inicialización del diamante, haremos que nuestro contador comience en 8 en lugar de 0.
Para comenzar, haz un fork del repositorio hardhat de referencia diamond-2. No tenemos constancia de ninguna implementación en Foundry que haya sido auditada.
IncrementLibrary
Es útil poner el código para leer el almacenamiento con namespace en una biblioteca que las facetas puedan importar. Poner la lógica para actualizar el contador dentro de la biblioteca es una elección de diseño. Poner la lógica de incremento directamente en la biblioteca aumentará el tamaño del bytecode para todas las demás facetas que llamen a las funciones de esa biblioteca. Sin embargo, también simplifica la lógica para la faceta de incremento, ya que la faceta de incremento no necesita saber nada sobre cómo está estructurado el almacenamiento basado en namespaces.
Ten en cuenta que todas las funciones deben ser internas en esta biblioteca, ya que el compilador de Solidity espera que las bibliotecas 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++;
}
}
Agrega 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() llama usando delegatecall. En la implementación de referencia, esto se encuentra en contracts/upgradeInitializers/DiamondInit.sol.
No mostramos el contrato entero por brevedad. Agrega 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
}
}
Faceta que solo visualiza x
Crea 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();
}
}
Faceta que solo incrementa x
Crea IncrementFacet.sol en contracts/facets:
import { LibInc } from "../libraries/LibInc.sol";
contract IncrementFacet {
function increment() external {
LibInc.increment();
}
}
Agrega la siguiente prueba
Agrega la siguiente prueba 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)
});
Actualiza scripts/deploy.js para desplegar las nuevas facetas
Abre scripts/deploy.js y agrega los nombres de los contratos de las facetas. Hardhat es lo suficientemente inteligente como para encontrar los contratos sin especificar las rutas de los archivos. El cambio se muestra en el recuadro rojo a continuación:

Actualiza la prueba
Actualiza el hook before en el archivo diamondTest.js de la siguiente manera para desplegar las nuevas facetas.
Una breve aclaración: el compilador de Solidity es capaz de generar los selectores de función de las funciones públicas/externas automáticamente. 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 selectores de función por nosotros utilizando esta técnica, lo que nos ahorra la molestia de especificar explícitamente los selectores de función por nuestra cuenta.
De nuevo, Hardhat es capaz de encontrar los contratos sin especificar las rutas de los archivos:

Ejecuta la prueba con
npx hardhat test
Ten en cuenta que las demás pruebas fallarán porque no están esperando los nuevos selectores. Ignoramos esto usando el modificador .only en nuestra prueba. El modificador .only evita que se ejecuten las demás pruebas unitarias y solo ejecuta la prueba unitaria modificada con .only.
Si tienes problemas, asegúrate de estar utilizando la versión 20 de Node.
Resumen
- El patrón diamond es un proxy que tiene múltiples contratos de implementación.
- El diamante sabe a qué faceta hacer delegatecall basándose en el selector de función del calldata entrante.
- Si el diamante es actualizable, el mapeo de selector a dirección de implementación se puede cambiar a través de diamondCut. La lógica para cambiar este mapeo no está dictada por el estándar, puede ser parte del bytecode del proxy o parte de una faceta.
- Todas las facetas y selectores en el diamante deben poder determinarse 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 a usuarios offchain.
Nos gustaría agradecer a Nick Mudge por aportar comentarios a una versión anterior de este artículo.