Jardins & Diamonds
L'architecture des Diamonds & Gardens
Chez BLOK Capital, notre architecture principale pour les smart contracts repose sur le pattern proxy Diamond. Dans cette section, nous explorons l’architecture des diamonds, une approche de déploiement basée sur une factory inspirée du dépôt Diamond de Nick Mudge, et comment ces concepts créent un « jardin » qui alimente l’architecture BLOK.
Flux du contrat proxy :
Qu’est-ce que le standard Diamond ?
Le Diamond Standard, proposé par Nick Mudge dans l’EIP-2535, répond au problème de création de contrats modulaires et upgradables sans rencontrer les limites de taille ou de gas. Les contrats traditionnels sont monolithiques, avec toute la logique empaquetée dans une seule adresse, ce qui rend les mises à jour difficiles et risquées. Les diamonds, en revanche, ressemblent à des jardins : ils consistent en un contrat Diamond central (par ex. le sol) qui délègue des fonctionnalités à plusieurs facettes (par ex. les plantes), chacune contenant une logique spécifique.
Un diamond est un contrat proxy qui utilise une architecture unique pour :
- Modulariser la logique : découper les fonctionnalités en facettes réutilisables de petite taille.
- Permettre les mises à jour : ajouter, remplacer ou supprimer des facettes sans redéployer l’ensemble du contrat.
- Contourner les limites de taille : ne stocker que les sélecteurs de fonctions dans le diamond et déléguer l’exécution aux facettes.
Cette modularité rend les diamonds idéaux pour des dApps complexes (DAO, DeFi, NFT) qui doivent évoluer au fil du temps, à l’image d’un jardin que l’on peut replanter ou agrandir.
L’architecture d’un Diamond
Imaginez le diamond comme un hub central (le contrat Diamond.sol
) qui redirige les appels de fonctions vers des facettes spécialisées. Chaque facette est un contrat séparé contenant un sous-ensemble de la fonctionnalité du système (propriété, transferts de tokens, gouvernance, etc.). Le diamond maintient une table de correspondance des sélecteurs de fonctions (identifiants uniques pour les fonctions) vers les adresses des facettes, ce qui lui permet de déléguer les appels efficacement.
Composants clés de l’architecture Diamond
- Contrat Diamond (
Diamond.sol
) :- Sert de point d’entrée pour toutes les interactions.
- Stocke une mapping des sélecteurs de fonctions vers les adresses des facettes.
- Implémente la fonction
fallback
pour router les appels vers la facette appropriée. - Utilise
diamondCut
pour ajouter, remplacer ou supprimer des facettes, permettant les upgrades.
- Facettes :
- Contrats indépendants (ex.
DiamondCutFacet.sol
,DiamondLoupeFacet.sol
) contenant des fonctions spécifiques. - Par exemple,
DiamondCutFacet
gère les upgrades, tandis queDiamondLoupeFacet
fournit l’introspection (interroger les adresses de facettes et leurs sélecteurs).
- Contrats indépendants (ex.
- Interfaces :
- Des interfaces standards comme
IDiamondCut
etIDiamondLoupe
assurent la cohérence. - Les facettes implémentent ces interfaces pour fournir la fonctionnalité attendue.
- Des interfaces standards comme
- Sélecteurs de fonctions :
- Un hash de 4 octets (ex.
keccak256("functionName()")
) qui identifie une fonction. - Le diamond mappe les sélecteurs aux adresses de facettes afin que l’appel soit traité par le bon contrat.
- Un hash de 4 octets (ex.
Voici un exemple simplifié de la fonction fallback
dans Diamond.sol
, qui délègue les appels aux facettes :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Diamond {
mapping(bytes4 => address) public facets;
fallback() external payable {
address facet = facets[msg.sig];
require(facet != address(0), "Function does not exist");
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())
let result := delegatecall(gas(), facet, ptr, calldatasize(), 0, 0)
let size := returndatasize()
returndatacopy(ptr, 0, size)
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
}
Ce code illustre comment le diamond redirige les appels entrants (msg.sig
) vers la facette appropriée, ce qui rend la solution modulaire.
Le Jardin : systèmes modulaires et extensibles
Pourquoi appeler cela un « jardin » ? Dans un jardin, chaque plante (facette) a une fonction : certaines apportent la structure (comme DiamondCutFacet
), d’autres offrent l’introspection (comme DiamondLoupeFacet
), et d’autres portent les fruits (facettes personnalisées pour la logique de votre dApp). Le Diamond Standard permet de cultiver un jardin de smart contracts qui :
- Pousse avec le temps : ajouter de nouvelles facettes au fur et à mesure des besoins.
- Tailler et replanter : remplacer ou retirer des facettes obsolètes sans perturber le jardin.
- Monter en charge efficacement : garder le contrat central léger en déportant la logique vers les facettes.
Cette modularité est particulièrement puissante pour des dApps comme les DAO, protocoles DeFi ou marketplaces NFT, où de nouvelles fonctionnalités (gouvernance, staking, etc.) peuvent être ajoutées sans redéployer l’ensemble du système.
Déploiement basé sur une factory avec DiamondFactory.sol
Déployer des diamonds manuellement peut être complexe, surtout pour des systèmes nécessitant plusieurs instances (ex. une marketplace avec de nombreux diamonds par utilisateur). Le dépôt diamond-foundry
propose DiamondFactory.sol
, une factory qui automatise le déploiement des diamonds et l’initialisation des facettes — comme un jardinier qui plante de nouveaux parterres.
Comment fonctionne DiamondFactory.sol
La factory déploie un nouveau contrat Diamond
et l’initialise avec les facettes spécifiées. Version simplifiée de DiamondFactory.sol
:
// SPDX-License-License: MIT
pragma solidity ^0.8.0;
import {IDiamondCut} from "../interfaces/IDiamondCut.sol";
import {Diamond} from "../Diamond.sol";
contract DiamondFactory {
event DiamondDeployed(address diamond, address owner);
function deployDiamond(
address _owner,
address[] memory _facets,
bytes[] memory _initData
) external returns (address diamond) {
diamond = address(new Diamond(_owner, address(this)));
IDiamondCut.FacetCut[] memory cuts = new IDiamondCut.FacetCut[](_facets.length);
for (uint256 i = 0; i < _facets.length; i++) {
cuts[i] = IDiamondCut.FacetCut({
facetAddress: _facets[i],
action: IDiamondCut.FacetCutAction.Add,
functionSelectors: getSelectors(_facets[i])
});
}
IDiamondCut(diamond).diamondCut(cuts, address(0), "");
emit DiamondDeployed(diamond, _owner);
return diamond;
}
function getSelectors(address facet) internal pure returns (bytes4[] memory selectors) {
// Simplified: Hardcode selectors for known facets
selectors = new bytes4;
selectors[0] = bytes4(keccak256("facets()"));
selectors[1] = bytes4(keccak256("facetFunctionSelectors(address)"));
selectors[2] = bytes4(keccak256("facetAddresses()"));
selectors[3] = bytes4(keccak256("facetAddress(bytes4)"));
return selectors;
}
}
La factory :
- Déploie un nouveau contrat
Diamond
avec le propriétaire spécifié. - Crée un tableau
FacetCut
pour enregistrer les facettes (ex.DiamondCutFacet
,DiamondLoupeFacet
). - Appelle
diamondCut
pour initialiser le diamond avec les facettes. - Émet un événement pour tracer les déploiements.
Ce pattern de factory permet des déploiements à grande échelle, vous autorisant à planter plusieurs diamonds dans votre jardin avec peu d’effort.
Exemple de déploiement
Voici comment déployer un diamond en utilisant un script Foundry :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Script} from "forge-std/Script.sol";
import {DiamondFactory} from "../src/factory/DiamondFactory.sol";
import {DiamondCutFacet} from "../src/facets/DiamondCutFacet.sol";
import {DiamondLoupeFacet} from "../src/facets/DiamondLoupeFacet.sol";
contract DeployDiamond is Script {
function run() external {
vm.startBroadcast();
DiamondCutFacet cutFacet = new DiamondCutFacet();
DiamondLoupeFacet loupeFacet = new DiamondLoupeFacet();
DiamondFactory factory = new DiamondFactory();
address;
facets[0] = address(cutFacet);
facets[1] = address(loupeFacet);
bytes;
initData[0] = "";
initData[1] = "";
address diamond = factory.deployDiamond(msg.sender, facets, initData);
console.log("Diamond deployed at:", diamond);
vm.stopBroadcast();
}
}
Exécutez ce script avec :
forge script script/DeployDiamond.s.sol --fork-url http://localhost:8545 --broadcast
Tester le jardin
Pour que votre jardin de diamonds prospère, il faut des tests robustes. Les tests en Solidity avec Foundry rendent cela simple. Exemple de suite de tests vérifiant le déploiement via la factory et le fonctionnement du diamond :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Test} from "forge-std/Test.sol";
import {DiamondFactory} from "../src/factory/DiamondFactory.sol";
import {DiamondCutFacet} from "../src/facets/DiamondCutFacet.sol";
import {DiamondLoupeFacet} from "../src/facets/DiamondLoupeFacet.sol";
import {IDiamondLoupe} from "../src/interfaces/IDiamondLoupe.sol";
contract DiamondFactoryTest is Test {
DiamondFactory factory;
DiamondCutFacet cutFacet;
DiamondLoupeFacet loupeFacet;
address diamond;
function setUp() public {
cutFacet = new DiamondCutFacet();
loupeFacet = new DiamondLoupeFacet();
factory = new DiamondFactory();
address;
facets[0] = address(cutFacet);
facets[1] = address(loupeFacet);
bytes;
initData[0] = "";
initData[1] = "";
diamond = factory.deployDiamond(address(this), facets, initData);
}
function testDiamondDeployment() public {
assertTrue(diamond != address(0), "Diamond not deployed");
}
function testLoupeFunctions() public {
IDiamondLoupe loupe = IDiamondLoupe(diamond);
address[] memory facets = loupe.facetAddresses();
assertEq(facets.length, 2, "Incorrect number of facets");
assertEq(facets[0], address(cutFacet), "DiamondCutFacet not registered");
assertEq(facets[1], address(loupeFacet), "DiamondLoupeFacet not registered");
}
}
Exécutez les tests avec :
forge test
Avantages du Diamond Garden
Le Diamond Standard, associé au déploiement via factory, offre :
- Modularité : ajouter des fonctionnalités (facettes) sans redéployer le cœur du contrat.
- Scalabilité : déployer plusieurs diamonds pour différents utilisateurs ou cas d’usage.
- Upgradabilité : mettre à jour la fonctionnalité sans casser les contrats existants.
- Efficacité en gas : garder le contrat diamond léger en déportant la logique vers les facettes.
Conclusion
Le Diamond Standard est un outil puissant pour construire des systèmes de smart contracts modulaires et upgradables, analogue à la culture d’un jardin où chaque plante (facette) contribue à un écosystème prospère. En utilisant DiamondFactory.sol
, vous pouvez automatiser le déploiement des diamonds et faciliter la montée en charge de votre jardin à travers plusieurs instances. Que vous développiez un protocole DeFi, une DAO ou une marketplace NFT, le Diamond Standard offre la flexibilité nécessaire pour croître et s’adapter.
Bonne culture du jardin !