Aller au contenu principal

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 :

Alt text

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.
Alt text

Composants clés de l’architecture Diamond

  1. 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.
  2. Facettes :
    • Contrats indépendants (ex. DiamondCutFacet.sol, DiamondLoupeFacet.sol) contenant des fonctions spécifiques.
    • Par exemple, DiamondCutFacet gère les upgrades, tandis que DiamondLoupeFacet fournit l’introspection (interroger les adresses de facettes et leurs sélecteurs).
  3. Interfaces :
    • Des interfaces standards comme IDiamondCut et IDiamondLoupe assurent la cohérence.
    • Les facettes implémentent ces interfaces pour fournir la fonctionnalité attendue.
  4. 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.

Alt text

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 :

  1. Déploie un nouveau contrat Diamond avec le propriétaire spécifié.
  2. Crée un tableau FacetCut pour enregistrer les facettes (ex. DiamondCutFacet, DiamondLoupeFacet).
  3. Appelle diamondCut pour initialiser le diamond avec les facettes.
  4. É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 !