DEV Community

Cover image for EIP-2535: Diamond, a arquitetura de Smart Contracts modulares
Carlos Delfino
Carlos Delfino

Posted on

EIP-2535: Diamond, a arquitetura de Smart Contracts modulares

Quando começamos a desenvolver Smart Contracts mais complexos, uma limitação aparece rapidamente: contratos grandes demais ficam difíceis de manter, testar, auditar e evoluir. Além disso, na Ethereum existe um limite prático de tamanho para o bytecode de um contrato implantado.

O EIP-2535, também conhecido como Diamond Standard, propõe uma arquitetura para resolver esse problema usando um contrato central chamado Diamond e vários contratos auxiliares chamados Facets. A proposta oficial descreve Diamonds como sistemas modulares de Smart Contracts que podem ser estendidos ou atualizados após o deploy e que não possuem, na prática, a mesma limitação de tamanho de um contrato monolítico comum.

A ideia central

O Diamond funciona como um Proxy Central.

Ele recebe as chamadas externas dos usuários e encaminha cada função para o contrato correto. Esse encaminhamento é feito usando delegatecall, ou seja, a lógica executa no contrato Facet, mas usando o armazenamento do Diamond.

De forma simplificada:

Cada Facet contém um conjunto de funções. O Diamond guarda um mapeamento dizendo qual função pertence a qual Facet.

Por que o nome Diamond?

A metáfora é interessante: um diamante possui várias faces. Cada face representa uma parte visível do mesmo objeto.

No EIP-2535:

  • O Diamond é o contrato principal.
  • As Facets são as “faces” funcionais.
  • O usuário interage com um único endereço.
  • Internamente, cada função pode estar em um contrato diferente.

Isso permite que um projeto tenha apenas um endereço principal, mas com uma arquitetura dividida em módulos.

O problema dos contratos monolíticos

Imagine um contrato que mistura:

contract MegaContrato {
    // lógica de token
    // lógica de governança
    // lógica de staking
    // lógica de permissões
    // lógica de marketplace
    // lógica de upgrade
}
Enter fullscreen mode Exit fullscreen mode

Com o tempo, esse contrato fica difícil de entender. Qualquer alteração pode afetar partes não relacionadas. Além disso, o deploy pode esbarrar no limite de tamanho do contrato.

Com o Diamond, podemos dividir:

TokenFacet
GovernanceFacet
StakingFacet
MarketplaceFacet
AdminFacet
Enter fullscreen mode Exit fullscreen mode

A aplicação continua parecendo um único contrato para o usuário, mas internamente ela é modular.

Como o Diamond sabe para onde enviar cada chamada?

Cada função em Solidity possui um function selector, que são os 4 primeiros bytes do hash da assinatura da função.

Por exemplo:

balanceOf(address)
transfer(address,uint256)
mint(address,uint256)
Enter fullscreen mode Exit fullscreen mode

Cada uma dessas assinaturas gera um seletor diferente.

O Diamond mantém uma tabela semelhante a esta:

selector de transfer()  -> endereço da TokenFacet
selector de vote()      -> endereço da GovernanceFacet
selector de stake()     -> endereço da StakingFacet
Enter fullscreen mode Exit fullscreen mode

Quando alguém chama uma função no Diamond, o fallback do contrato verifica o seletor da função e descobre qual Facet deve executar aquela lógica.

Exemplo simplificado de um Diamond

Abaixo está um exemplo conceitual. Ele não é uma implementação completa de produção, mas ajuda a entender o mecanismo.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SimpleDiamond {
    mapping(bytes4 => address) public facets;

    address public owner;

    constructor() {
        owner = msg.sender;
    }

    function setFacet(bytes4 _selector, address _facet) external {
        require(msg.sender == owner, "Apenas o owner");
        facets[_selector] = _facet;
    }

    fallback() external payable {
        address facet = facets[msg.sig];

        require(facet != address(0), "Funcao nao encontrada");

        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())
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Esse contrato faz três coisas principais:

  1. Guarda qual seletor de função aponta para qual Facet.
  2. Recebe chamadas no fallback.
  3. Encaminha a execução para a Facet correta usando delegatecall.

Exemplo de uma Facet

Agora imagine uma Facet simples responsável por armazenar e retornar um número:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract NumberFacet {
    uint256 public number;

    function setNumber(uint256 _number) external {
        number = _number;
    }

    function getNumber() external view returns (uint256) {
        return number;
    }
}
Enter fullscreen mode Exit fullscreen mode

À primeira vista, parece um contrato comum. Mas quando ele é chamado via Diamond, o armazenamento usado não é o da Facet. É o armazenamento do Diamond.

Esse detalhe é essencial.

Delegatecall: o coração da arquitetura

O delegatecall permite executar o código de outro contrato preservando o contexto do contrato chamador.

Em outras palavras:

Código vem da Facet.
Storage vem do Diamond.
msg.sender continua sendo o usuário original.
Enter fullscreen mode Exit fullscreen mode

Isso é poderoso, mas também perigoso se o layout de armazenamento for mal planejado.

Por isso, implementações reais de Diamond normalmente usam padrões de armazenamento específicos, como o Diamond Storage, onde cada módulo armazena seus dados em uma posição bem definida da storage.

Exemplo de Diamond Storage

Uma forma comum de evitar conflitos de storage é usar uma biblioteca com uma posição fixa:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

library LibAppStorage {
    bytes32 constant STORAGE_POSITION =
        keccak256("diamond.standard.example.storage");

    struct AppStorage {
        uint256 number;
        address admin;
    }

    function appStorage()
        internal
        pure
        returns (AppStorage storage s)
    {
        bytes32 position = STORAGE_POSITION;

        assembly {
            s.slot := position
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora a Facet pode usar essa storage compartilhada:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./LibAppStorage.sol";

contract NumberFacet {
    function setNumber(uint256 _number) external {
        LibAppStorage.AppStorage storage s = LibAppStorage.appStorage();
        s.number = _number;
    }

    function getNumber() external view returns (uint256) {
        LibAppStorage.AppStorage storage s = LibAppStorage.appStorage();
        return s.number;
    }
}
Enter fullscreen mode Exit fullscreen mode

Esse padrão evita que uma Facet sobrescreva dados de outra por acidente.

diamondCut: adicionando, trocando e removendo funções

O EIP-2535 define uma função importante chamada diamondCut. Ela é usada para adicionar, substituir ou remover funções de um Diamond. A especificação oficial indica que o diamondCut gerencia os seletores de funções e os endereços das Facets dentro do storage do Diamond. (Ethereum Improvement Proposals)

Conceitualmente, seria algo assim:

enum FacetCutAction {
    Add,
    Replace,
    Remove
}

struct FacetCut {
    address facetAddress;
    FacetCutAction action;
    bytes4[] functionSelectors;
}
Enter fullscreen mode Exit fullscreen mode

Exemplo conceitual de uso:

diamondCut([
    FacetCut({
        facetAddress: address(tokenFacet),
        action: FacetCutAction.Add,
        functionSelectors: selectors
    })
]);
Enter fullscreen mode Exit fullscreen mode

Na prática, isso permite que o projeto evolua sem precisar trocar o endereço principal do contrato.

Loupe: inspecionando o Diamond

Outro conceito importante do EIP-2535 é o conjunto de funções chamado Diamond Loupe.

“Loupe” é uma lupa usada para examinar diamantes. No padrão, essas funções permitem consultar quais Facets e funções existem no Diamond.

Exemplos de perguntas que o Loupe responde:

Quais Facets existem neste Diamond?
Quais funções pertencem a uma Facet?
Qual Facet executa determinado seletor?
Enter fullscreen mode Exit fullscreen mode

Isso melhora a transparência, porque usuários, exploradores de bloco e ferramentas de auditoria podem inspecionar a composição do Diamond.

Comparação com proxies tradicionais

Em muitos proxies tradicionais, como Transparent Proxy ou UUPS, normalmente existe:

Proxy -> Implementação
Enter fullscreen mode Exit fullscreen mode

Já no Diamond temos:

Diamond -> Facet A
Diamond -> Facet B
Diamond -> Facet C
Diamond -> Facet D
Enter fullscreen mode Exit fullscreen mode

A RareSkills resume bem essa diferença: o Diamond Pattern usa múltiplos contratos de implementação ao mesmo tempo, enquanto padrões como Transparent Proxy e UUPS geralmente apontam para uma implementação principal por vez. (rareskills.io)

Vantagens do EIP-2535

A primeira grande vantagem é a modularidade. Cada Facet pode representar uma parte específica do sistema.

A segunda é a escalabilidade do código. Como a lógica fica distribuída em várias Facets, o sistema consegue crescer sem concentrar tudo em um único contrato.

A terceira é a atualização seletiva. Em vez de substituir uma implementação inteira, é possível trocar apenas uma Facet ou até funções específicas.

A quarta é a organização arquitetural. Projetos grandes podem separar melhor as responsabilidades, facilitando testes, auditoria e manutenção.

Riscos e cuidados

Apesar das vantagens, o Diamond não deve ser usado sem planejamento.

O primeiro cuidado é com storage collision. Como várias Facets usam o storage do Diamond, é necessário organizar cuidadosamente onde cada dado será salvo.

O segundo cuidado é com controle de upgrade. A função diamondCut é extremamente poderosa. Se for mal protegida, alguém pode substituir funções críticas do contrato.

O terceiro cuidado é com auditoria. Um Diamond pode ser mais difícil de auditar do que um contrato simples, porque a lógica está distribuída em várias Facets.

O quarto cuidado é com complexidade desnecessária. Para contratos pequenos, um Diamond pode ser exagerado.

Quando vale a pena usar Diamond?

O EIP-2535 faz mais sentido quando o projeto possui:

  • Muitos módulos funcionais.
  • Necessidade de upgrade.
  • Contratos grandes.
  • Arquitetura de longo prazo.
  • Separação clara de responsabilidades.
  • Um único endereço principal para o usuário interagir.

Exemplos de uso:

DAOs complexas
Marketplaces
Jogos on-chain
Protocolos DeFi
Sistemas de identidade
Plataformas com múltiplos módulos
Agentes de IA com identidade e permissões on-chain
Enter fullscreen mode Exit fullscreen mode

Uma analogia simples

Imagine que o Diamond é um quadro de distribuição elétrica.

O usuário chega em um único painel, mas internamente cada disjuntor leva para um circuito diferente:

Circuito de iluminação
Circuito das tomadas
Circuito do ar-condicionado
Circuito da bomba
Circuito de segurança
Enter fullscreen mode Exit fullscreen mode

No Smart Contract Diamond:

Facet de token
Facet de staking
Facet de governança
Facet de permissões
Facet de marketplace
Enter fullscreen mode Exit fullscreen mode

Tudo parece um único sistema, mas por dentro está dividido em módulos especializados.

Conclusão

O EIP-2535 Diamond Standard é uma arquitetura poderosa para Smart Contracts modulares, atualizáveis e escaláveis. Ele transforma o contrato principal em um Proxy Central, chamado Diamond, que encaminha chamadas para múltiplas Facets.

Seu maior benefício é permitir que sistemas on-chain cresçam de maneira organizada. Em vez de construir um contrato gigantesco, o desenvolvedor pode dividir a lógica em partes menores, mais testáveis e mais fáceis de evoluir.

Por outro lado, essa liberdade vem com responsabilidade. Um Diamond exige bom planejamento de storage, controle rigoroso de permissões e auditoria cuidadosa.

Para projetos simples, ele pode ser complexo demais. Mas para sistemas grandes, como DAOs, protocolos DeFi, marketplaces, plataformas Web3 e infraestruturas para agentes autônomos, o EIP-2535 oferece uma das arquiteturas mais interessantes e flexíveis do ecossistema Ethereum.

Top comments (0)