SwiftHTML & CSSSolidityDesenvolvimento de JogosSolana/Rust
05.12.2024

Lição 235: Padrões Seguros de Atualização de Contratos

No mundo das blockchain e aplicativos descentralizados, os contratos inteligentes são projetados para serem imutáveis uma vez implantados. No entanto, à medida que continuamos a construir aplicativos mais complexos, surge a necessidade de atualizações devido a correções de bugs, melhorias de funcionalidades ou otimização. Esta lição aborda padrões seguros de atualização de contratos que ajudam a mitigar riscos, como a introdução de vulnerabilidades ou perda de estado durante as atualizações.

Entendendo os Padrões de Atualização

Existem vários padrões de atualização utilizados no Solidity, cada um com suas vantagens e desvantagens. Aqui, discutiremos três padrões principais de atualização: o Padrão Proxy, EIP-1967, e Padrão Diamante.

1. Padrão Proxy

O Padrão Proxy permite que você implante um contrato que delega chamadas para outro contrato (o contrato de lógica). Isso significa que a lógica pode ser atualizada sem mudar o endereço do contrato com o qual os usuários interagem.

Exemplo:

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

contrato ContratoDeLogica {
    uint public valor;

    função definirValor(uint _valor) público {
        valor = _valor;
    }
}

contrato ContratoProxy {
    endereço público contratoDeLogica;

    construtor(endereço _contratoDeLogica) {
        contratoDeLogica = _contratoDeLogica;
    }

    função fallback() externa paga {
        (bool sucesso, ) = contratoDeLogica.delegatecall(msg.data);
        require(sucesso, "Falha ao delegar chamada");
    }
}

Neste exemplo, o ContratoProxy delega chamadas ao ContratoDeLogica. Quando a lógica precisa ser atualizada, você implanta uma nova versão do ContratoDeLogica e muda de forma transparente o endereço contratoDeLogica no ContratoProxy.

2. EIP-1967

EIP-1967 fornece um padrão para armazenar o endereço de implementação do contrato de lógica. Isso é importante para prevenir colisões de armazenamento e garantir que as atualizações sejam tratadas corretamente.

Exemplo:

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

contrato ProxyAtualizável {
    bytes32 private constant _SLOT_DE_IMPLEMENTACAO = 
        keccak256("eip1967.proxy.implementation") - 1;

    construtor(endereço implementacaoInicial) {
        definirImplementacao(implementacaoInicial);
    }

    função definirImplementacao(endereço novaImplementacao) interno {
        require(Address.isContract(novaImplementacao), "Não é um contrato");
        StorageSlot.getAddressSlot(_SLOT_DE_IMPLEMENTACAO).value = novaImplementacao;
    }

    função implementacao() público view retorna (endereço) {
        return StorageSlot.getAddressSlot(_SLOT_DE_IMPLEMENTACAO).value;
    }

    função fallback() externa paga {
        endereço impl = implementacao();
        require(impl != endereço(0), "Implementação não definida");
        (bool sucesso, bytes memória dados) = impl.delegatecall(msg.data);
        require(sucesso, "Falha na delegatecall");
        assembly {
            return(add(dados, 0x20), mload(dados))
        }
    }
}

Neste exemplo, o contrato ProxyAtualizável usa um slot de armazenamento para armazenar o endereço do contrato de implementação. A função definirImplementacao atualiza o endereço da implementação enquanto garante que ele aponte para um contrato válido.

3. Padrão Diamante

O Padrão Diamante (EIP-2535) é um método avançado para gerenciar contratos inteligentes complexos. Ele permite que múltiplos contratos (facetas) sejam combinados sob um único endereço, permitindo atualizações flexíveis e designs modulares.

Exemplo:

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

contrato Diamante {
    estrutura Faceta {
        endereço endereçoFaceta;
        bytes4[] seletores;
    }

    mapeamento(bytes4 => endereço) público endereçoFaceta;
    Faceta[] público facetas;

    função adicionarFaceta(endereço _endereçoFaceta, bytes4[] memória _seletores) externo {
        para (uint256 i = 0; i < _seletores.length; i++) {
            endereçoFaceta[_seletores[i]] = _endereçoFaceta;
        }
        facetas.push(Faceta(_endereçoFaceta, _seletores));
    }

    função fallback() externa {
        endereço faceta = endereçoFaceta[msg.sig];
        require(faceta != endereço(0), "Função não encontrada");
        assembly {
            calldatacopy(0, 0, calldatasize())
            let resultado := delegatecall(gas(), faceta, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch resultado
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

Neste exemplo, o contrato Diamante pode adicionar dinamicamente facetas que contêm funcionalidades diferentes. A função fallback roteia chamadas para a faceta apropriada com base na assinatura da função.

Melhores Práticas para Atualizar Contratos

  1. Testes Extensivos: Sempre conduza testes rigorosos e auditorias para a nova lógica antes de implantar atualizações.
  2. Tempos de Espera: Implemente atrasos de tempo para atualizações para permitir que os usuários reajam e previnam mudanças maliciosas.
  3. Funções de Administrador: Utilize controle de acesso baseado em funções para restringir quem pode realizar atualizações.
  4. Migração de Estado: Gerencie cuidadosamente quaisquer mudanças de estado para evitar perda de dados.

Conclusão

Padrões de atualização no Solidity são cruciais para manter e evoluir contratos inteligentes, gerenciando os riscos associados. O Padrão Proxy, EIP-1967, e o Padrão Diamante são técnicas eficazes para alcançar atualizações seguras. Como desenvolvedor, entender esses padrões permitirá que você construa aplicações descentralizadas mais robustas, flexíveis e manuteníveis.

Video

Did you like this article? Rate it from 1 to 5:

Thank you for voting!