Herança é interfaces

A herança e as interfaces são conceitos fundamentais na programação de smart contracts em Solidity, permitindo a organização modular e reutilizável do código, a abstração de funcionalidades e a criação de contratos compatíveis. Vamos guiá-lo através dos conceitos básicos e da implementação prática de herança e interfaces em Solidity.

Orientação a Objetos em Solidity

Conceito Básico de OO:

  • Objetos: Pense nos objetos como entidades do mundo real. Por exemplo, um carro. Cada carro tem atributos (como cor, modelo, marca) e comportamentos (como acelerar, frear).

  • Classes: Uma classe é como uma planta baixa (blueprint) para criar objetos. No caso dos carros, a classe definiria o que todo carro deve ter (atributos) e o que pode fazer (métodos).

Definindo uma Classe (Contrato) em Solidity:

  • Em Solidity, um contrato pode ser visto como uma classe. Ele define um conjunto de dados (estado) e funções (comportamentos).

pragma solidity ^0.8.0;

contract Car {
    // Atributos (estado)
    string public color;
    string public model;
    uint public mileage;

    // Construtor - inicializa o objeto
    constructor(string memory _color, string memory _model, uint _mileage) {
        color = _color;
        model = _model;
        mileage = _mileage;
    }

    // Método (função)
    function drive(uint distance) public {
        mileage += distance;
    }
}

Instanciando Objetos:

  • Quando você cria um novo contrato Car, está criando uma instância dessa classe. Cada instância tem seu próprio estado.

Car myCar = new Car("Red", "Tesla Model S", 100);

Herança:

  • Assim como em outras linguagens orientadas a objetos, Solidity suporta herança, permitindo que um contrato herde as propriedades e métodos de outro contrato.

contract ElectricCar is Car {
    uint public batteryLife;

    constructor(string memory _color, string memory _model, uint _mileage, uint _batteryLife)
    Car(_color, _model, _mileage) {
        batteryLife = _batteryLife;
    }

    function chargeBattery(uint amount) public {
        batteryLife += amount;
    }
}

Encapsulamento:

  • Encapsulamento é a prática de esconder os detalhes internos e expor apenas o necessário. Em Solidity, podemos usar modificadores de visibilidade como public, private, e internal para controlar o acesso.

contract Car {
    string public color;
    string private model;
    uint internal mileage;

    constructor(string memory _color, string memory _model, uint _mileage) {
        color = _color;
        model = _model;
        mileage = _mileage;
    }

    function drive(uint distance) public {
        mileage += distance;
    }

    function getModel() public view returns (string memory) {
        return model;
    }
}

Desafio prático : Contas de Banco Imagine um banco com diferentes tipos de contas: conta corrente, conta poupança e conta investimento. Cada conta possui características e funcionalidades próprias, como taxas de juros, limites de saque e opções de investimento.

  • Contas como contratos: Cada tipo de conta pode ser visto como um contrato inteligente, com suas próprias regras e funções.

  • OO para organização: A OO permite criar classes abstratas para as características básicas (conta) e classes derivadas para cada tipo de conta (corrente, poupança, investimento), com herança e redefinição de métodos.

  • Modularização: Maior organização, reutilização de código, segurança e confiabilidade nas transações bancárias.

Herança

Herança é um princípio da programação orientada a objetos que permite que um contrato (a classe base) transfira suas propriedades e métodos para outro contrato (a classe derivada). Isso promove a reutilização de código e facilita a manutenção.

Exemplo de Herança:

// Contrato Pai: Veiculo
contract Veiculo {
    string marca;
    string modelo;

    constructor(string memory _marca, string memory _modelo) {
        marca = _marca;
        modelo = _modelo;
    }

    function getMarca() public view returns (string memory) {
        return marca;
    }

    function getModelo() public view returns (string memory) {
        return modelo;
    }
}

// Contrato Filho: Carro
contract Carro is Veiculo {
    uint256 numeroPortas;

    constructor(string memory _marca, string memory _modelo, uint256  _numeroPortas) Veiculo(_marca, _modelo) {
        numeroPortas = _numeroPortas;
    }

    function getNumeroPortas() public view returns (uint256) {
        return numeroPortas;
    }
}

Neste exemplo, o contrato Carro herda do contrato Veiculo. O contrato Carro possui as propriedades e métodos do Veiculo, como marca, modelo, getMarca e getModelo, além de adicionar sua própria propriedade numeroPortas e método getNumeroPortas.

Herança Múltipla

Solidity também suporta herança múltipla, permitindo que um contrato herde de vários contratos. Aqui está o código Solidity comentado, explicando o que cada parte faz:

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

// Definição do contrato A
contract A {
    
    // Declaração de uma variável de estado privada que armazena o endereço do dono do contrato
    address private dono;

    // Modificador de função que restringe a execução apenas ao dono do contrato
    modifier apenasDono() {
        require(msg.sender == dono, "Apenas o dono pode chamar essa funcao");
        _;
    }

    // Construtor do contrato que define o endereço do dono como o endereço que implanta o contrato
    constructor() {
        dono = msg.sender;
    }

    // Função pública que retorna uma string "Funcao A"
    function funcA() public pure returns (string memory) {
        return "Funcao A";
    }

    // Função pública restrita ao dono que retorna uma string "Funcao AX"
    function funcX() public apenasDono virtual view returns (string memory) {
        return "Funcao AX"; 
    }
}

// Definição do contrato B
contract B {

    // Função pública que retorna uma string "Funcao B"
    function funcB() public virtual pure returns (string memory) {
        return "Funcao B";
    }

    // Função pública que retorna uma string "Funcao BX"
    function funcX() public virtual view returns (string memory) {
        return "Funcao BX";
    }
}

// Definição do contrato C que herda de A e B
contract C is A, B {
    // Este contrato agora tem acesso a funcA e funcB

    // Sobrescrita da função funcX que é restrita ao dono, combinando a lógica dos contratos A e B
    function funcX() public apenasDono override(A, B) view returns (string memory) {
        // lógica adicional pode ser adicionada aqui
        return "Funcao CX";
    }

    // Sobrescrita da função funcB do contrato B
    function funcB() public override(B) pure returns (string memory) {
        return "Funcao CB";
    }
}

Contrato A

  • address private dono;: Armazena o endereço do dono do contrato.

  • modifier apenasDono(): Modificador que garante que apenas o dono pode executar a função.

  • constructor(): Define o dono do contrato como o endereço que o implantou.

  • funcA(): Função pública que retorna "Funcao A".

  • funcX(): Função pública restrita ao dono que retorna "Funcao AX".

Contrato B

  • funcB(): Função pública que retorna "Funcao B".

  • funcX(): Função pública que retorna "Funcao BX".

Contrato C

  • Herda de A e B.

  • funcX(): Sobrescreve a função funcX dos contratos A e B e é restrita ao dono.

  • funcB(): Sobrescreve a função funcB do contrato B.

Benefícios da Herança:

  • Reutilização de código: Reduz a duplicação de código, promovendo a organização e a legibilidade do código.

  • Extensão de funcionalidades: Permite a criação de contratos mais complexos e especializados, construindo sobre a base de contratos existentes.

  • Organização modular: Facilita a estruturação do código em módulos distintos e interligados, promovendo a manutenção e a atualização do código.

Interfaces

As interfaces em Solidity definem um conjunto de funções que um contrato deve implementar, sem fornecer a implementação das funções. As interfaces permitem a abstração de funcionalidades e a criação de contratos compatíveis.

Exemplo de Interface:

// Interface: VeiculoInterface
interface VeiculoInterface {
    function getMarca() external view returns (string memory);
    function getModelo() external view returns (string memory);
}

// Contrato: Carro
contract Carro is VeiculoInterface {
    string marca;
    string modelo;

    constructor(string memory _marca, string memory _modelo) {
        marca = _marca;
        modelo = _modelo;
    }

    function getMarca() public view override returns (string memory) {
        return marca;
    }

    function getModelo() public view override returns (string memory) {
        return modelo;
    }
}

Neste exemplo, a interface VeiculoInterface define as funções getMarca e getModelo. O contrato Carro implementa a interface VeiculoInterface, garantindo que ele possui as funções definidas na interface.

Benefícios das Interfaces:

  • Abstração de funcionalidades: Separa a definição da implementação, permitindo que diferentes contratos implementem as mesmas funções de maneira distinta.

  • Compatibilidade de contratos: Facilita a interação entre contratos, garantindo que todos os contratos que implementam a mesma interface possuem as mesmas funções disponíveis.

  • Desacoplamento de contratos: Reduz a dependência entre contratos, permitindo a fácil substituição de um contrato por outro que implementa a mesma interface.

Usando Herança e Interfaces Juntas

A herança e as interfaces podem ser usadas juntas para criar uma estrutura modular e flexível para seus smart contracts. Você pode usar interfaces para definir funcionalidades abstratas que diferentes contratos podem implementar, e usar herança para criar contratos que herdam funcionalidades de outros contratos e implementam interfaces.

Exemplo:

// Interface: VeiculoInterface
interface VeiculoInterface {
    function getMarca() external view returns (string memory);
    function getModelo() external view returns (string memory);
}

// Contrato Pai: Veiculo
contract Veiculo is VeiculoInterface {
    string marca;
    string modelo;

    constructor(string memory _marca, string memory _modelo) {
        marca

Especificadores de Visibilidade em Herança e Interfaces em Solidity

Os especificadores de visibilidade em Solidity controlam o acesso a atributos e funções de um contrato, definindo quem pode acessá-los e de onde. No contexto de herança e interfaces, os especificadores de visibilidade assumem importância crucial na organização modular e no controle de acesso entre diferentes contratos.

Em Solidity, existem quatro especificadores de visibilidade principais: external, public, internal e private. Vamos explorar cada um deles com exemplos práticos.

public

  • Atributos: Quando um atributo é declarado como public, Solidity automaticamente cria uma função getter para ele. Isso significa que o atributo pode ser acessado por qualquer pessoa dentro e fora do contrato.

  • Funções: Funções public podem ser chamadas de dentro do contrato, por outros contratos e externamente através de transações.

Exemplo

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

contract PublicExample {
    uint public valorPublico;

    function setValorPublico(uint _valor) public {
        valorPublico = _valor;
    }
}

Neste exemplo, valorPublico pode ser lido externamente, e setValorPublico pode ser chamado tanto internamente quanto externamente.

external

  • Funções: Funções external só podem ser chamadas de fora do contrato. Elas não podem ser chamadas internamente sem o uso da palavra-chave this.

Exemplo

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

contract ExternalExample {
    uint private valorExterno;

    function setValorExterno(uint _valor) external {
        valorExterno = _valor;
    }

    function chamarSetValorExterno(uint _valor) public {
        // Chamando uma função external de dentro do contrato
        this.setValorExterno(_valor);
    }
}

Neste exemplo, setValorExterno só pode ser chamada externamente ou internamente usando this.

internal

  • Atributos e Funções: Atributos e funções internal podem ser acessados apenas dentro do contrato em que são definidos e em contratos que herdam desse contrato.

Exemplo

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

contract InternalExample {
    uint internal valorInterno;

    function setValorInterno(uint _valor) internal {
        valorInterno = _valor;
    }
}

contract DerivedInternalExample is InternalExample {
    function atualizarValorInterno(uint _valor) public {
        setValorInterno(_valor); // Chamando a função internal do contrato pai
    }
}

Neste exemplo, valorInterno e setValorInterno são acessíveis apenas dentro de InternalExample e DerivedInternalExample.

private

  • Atributos e Funções: Atributos e funções private só podem ser acessados dentro do contrato em que são definidos. Eles não podem ser acessados por contratos derivados.

Exemplo

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

contract PrivateExample {
    uint private valorPrivado;

    function setValorPrivado(uint _valor) private {
        valorPrivado = _valor;
    }

    function atualizarValorPrivado(uint _valor) public {
        setValorPrivado(_valor); // Chamando a função private dentro do próprio contrato
    }
}

contract DerivedPrivateExample is PrivateExample {
    function tentarAcessarPrivado(uint _valor) public {
        // setValorPrivado(_valor); // Isto gerará um erro
    }
}

Neste exemplo, valorPrivado e setValorPrivado só podem ser acessados dentro de PrivateExample. Qualquer tentativa de acessá-los em DerivedPrivateExample resultará em um erro.

Visibilidade em Herança:

  • Atributos: A visibilidade de um atributo herdado é preservada no contrato filho. Por exemplo, se um atributo for private no contrato pai, ele também será private no contrato filho.

  • Funções: A visibilidade de uma função herdada pode ser modificada no contrato filho. Por exemplo, uma função public no contrato pai pode ser alterada para private no contrato filho.

Visibilidade em Interfaces:

  • As interfaces definem apenas a assinatura das funções, não sua visibilidade.

  • Ao implementar uma interface, o contrato deve escolher a visibilidade adequada para cada função, considerando a lógica do contrato e os princípios de encapsulamento.

Exemplo de Visibilidade em Herança:

// Contrato Pai: Veiculo
contract Veiculo {
    string marca; // private
    string modelo; // public

    function getMarca() public view returns (string memory) {
        return marca;
    }

    function getModelo() public view returns (string memory) {
        return modelo;
    }
}

// Contrato Filho: Carro
contract Carro is Veiculo {
    uint256 numeroPortas;

    constructor(string memory _marca, string memory _modelo, uint256 _numeroPortas) Veiculo(_marca, _modelo) {
        numeroPortas = _numeroPortas;
    }

    function getNumeroPortas() public view returns (uint256) {
        return numeroPortas;
    }
}

Neste exemplo, o contrato Veiculo possui um atributo marca privado e um atributo modelo público. No contrato Carro, o atributo marca permanece privado, enquanto o atributo modelo pode ser acessado por contratos externos.

Exemplo de Visibilidade em Interface:

// Interface: VeiculoInterface
interface VeiculoInterface {
    function getMarca() external view returns (string memory);
    function getModelo() external view returns (string memory);
}

// Contrato: Carro
contract Carro is VeiculoInterface {
    string marca;
    string modelo;

    constructor(string memory _marca, string memory _modelo) {
        marca = _marca;
        modelo = _modelo;
    }

    function getMarca() public view override returns (string memory) {
        return marca;
    }

    function getModelo() public view override returns (string memory) {
        return modelo;
    }
}

Neste exemplo, a interface VeiculoInterface define as funções getMarca e getModelo. O contrato Carro implementa a interface e escolhe a visibilidade public para ambas as funções.

Considerações Importantes:

  • Utilize os especificadores de visibilidade com cuidado para garantir o encapsulamento e a segurança dos seus smart contracts.

  • Evite expor atributos e funções desnecessariamente, limitando o acesso a apenas quem precisa deles.

  • Avalie cuidadosamente a visibilidade de funções herdadas antes de modificá-las no contrato filho.

  • Considere a utilização de interfaces para definir funcionalidades abstratas e permitir a implementação com diferentes visibilidades em diferentes contratos.

Diferenças entre internal e external:

  • Acesso: internal é interno, enquanto external é externo.

  • Chamadas: internal pode ser chamada internamente e por derivados, enquanto external só pode ser chamada externamente.

  • Encapsulamento: internal promove encapsulamento, enquanto external expõe a interface da função.

Os especificadores de visibilidade são essenciais para controlar o acesso a atributos e funções em contratos inteligentes Solidity. Utilizar esses especificadores de maneira adequada ajuda a proteger dados sensíveis e a manter uma boa organização do código. Aqui está um resumo rápido:

  • public: Acessível por qualquer pessoa dentro e fora do contrato.

  • external: Acessível apenas de fora do contrato.

  • internal: Acessível dentro do contrato e em contratos derivados.

  • private: Acessível apenas dentro do contrato onde é definido.

Herança de atributos , funções, construtor, modifier, events

A herança de contratos é uma técnica que permite a reutilização e extensão de código. Através da herança, contratos filhos podem herdar atributos (variáveis de estado), funções, construtores, modifiers e eventos dos contratos pais. Vamos detalhar como a herança funciona para cada um desses componentes:

Atributos (Variáveis de Estado)

Quando um contrato herda de outro, ele automaticamente herda todas as variáveis de estado do contrato pai. Estas variáveis podem ser acessadas e modificadas diretamente no contrato filho, a menos que sejam declaradas como private.

Exemplo:

pragma solidity ^0.8.0;

contract Parent {
    uint public value;
}

contract Child is Parent {
    function setValue(uint _value) public {
        value = _value;  // Acessa a variável de estado herdada
    }
}

Funções

As funções de um contrato pai são herdadas pelo contrato filho e podem ser chamadas diretamente ou sobrescritas (override) no contrato filho. Para sobrescrever uma função, use a palavra-chave override no contrato filho e virtual no contrato pai.

Exemplo:

pragma solidity ^0.8.0;

contract Parent {
    function sayHello() public virtual returns (string memory) {
        return "Hello from Parent";
    }
}

contract Child is Parent {
    function sayHello() public override returns (string memory) {
        return "Hello from Child";
    }
}

Construtores

Construtores não são herdados diretamente, mas o contrato filho pode chamar o construtor do contrato pai utilizando a sintaxe de construtor de contrato pai. Isso é feito passando os argumentos necessários ao construtor do contrato pai na definição do construtor do contrato filho.

Exemplo:

pragma solidity ^0.8.0;

contract Parent {
    uint public value;

    constructor(uint _value) {
        value = _value;
    }
}

contract Child is Parent {
    constructor(uint _value) Parent(_value) {
        // Pode adicionar lógica adicional aqui
    }
}

Modifiers

Modifiers também são herdados pelos contratos filhos. Assim como funções, modifiers podem ser sobrescritos usando as palavras-chave virtual no contrato pai e override no contrato filho.

Exemplo:

pragma solidity ^0.8.0;

contract Parent {
    modifier onlyOwner() virtual {
        require(msg.sender == owner, "Not the owner");
        _;
    }

    address public owner;

    constructor() {
        owner = msg.sender;
    }
}

contract Child is Parent {
    modifier onlyOwner() override {
        require(msg.sender == owner, "Not the owner");
        _;
    }

    function restrictedFunction() public onlyOwner {
        // Lógica da função restrita
    }
}

Events

Eventos definidos em um contrato pai são herdados pelo contrato filho e podem ser emitidos tanto no contrato pai quanto no contrato filho.

Exemplo:

pragma solidity ^0.8.0;

contract Parent {
    event ValueChanged(uint oldValue, uint newValue);

    uint public value;

    function setValue(uint _value) public {
        emit ValueChanged(value, _value);
        value = _value;
    }
}

contract Child is Parent {
    function updateValue(uint _value) public {
        emit ValueChanged(value, _value);  // Emitindo evento herdado
        value = _value;
    }
}

Resumo

  • Atributos (Variáveis de Estado): Herdados automaticamente, acessíveis a menos que private.

  • Funções: Herdadas automaticamente, podem ser sobrescritas usando virtual e override.

  • Construtores: Não são herdados diretamente, mas podem ser chamados no construtor do contrato filho.

  • Modifiers: Herdados automaticamente, podem ser sobrescritos.

  • Events: Herdados automaticamente, podem ser emitidos tanto pelo contrato pai quanto pelo contrato filho.

Herança x struct x interface

A escolha entre structs, herança de contratos e interfaces depende do contexto específico e dos objetivos do seu contrato inteligente. Cada uma dessas ferramentas serve a diferentes propósitos. Aqui estão algumas diretrizes para ajudar a decidir quando usar structs, herança de contratos ou interfaces:

Structs

Quando usar structs:

  1. Agrupamento de Dados: Utilize structs para agrupar múltiplos tipos de dados relacionados em uma única unidade lógica. Isso é útil quando você tem várias propriedades que descrevem uma entidade (por exemplo, um usuário, uma transação, um produto).

  2. Organização de Dados Complexos: Quando você precisa armazenar dados complexos no estado do contrato, structs ajudam a manter a organização e clareza.

  3. Simplificação de Funções: Structs podem ser usadas como parâmetros ou valores de retorno de funções para simplificar a passagem e manipulação de dados.

Exemplo de uso de struct:

pragma solidity ^0.8.0;

contract Marketplace {
    struct Product {
        uint id;
        string name;
        uint price;
        address seller;
    }

    mapping(uint => Product) public products;

    function addProduct(uint id, string memory name, uint price) public {
        products[id] = Product(id, name, price, msg.sender);
    }

    function getProduct(uint id) public view returns (Product memory) {
        return products[id];
    }
}

Herança de Contratos

Quando usar herança:

  1. Reutilização de Código: Utilize herança para reutilizar funcionalidades comuns entre diferentes contratos. Isso reduz a duplicação de código e facilita a manutenção.

  2. Modularidade: Quando você tem funcionalidades que podem ser divididas em componentes menores e reutilizáveis. Contratos base podem fornecer funcionalidades comuns compartilhadas por contratos derivados.

  3. Extensão e Personalização: Quando você precisa estender ou personalizar funcionalidades de um contrato existente sem modificar seu código original.

  4. Uso de Contratos Base: Herança é útil para incorporar contratos base que fornecem funcionalidades padrão, como contratos de gerenciamento de propriedade (Ownable), pausabilidade (Pausable) ou controle de acesso (AccessControl).

Exemplo de uso de herança:

pragma solidity ^0.8.0;

contract Ownable {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor() {
        _owner = msg.sender;
        emit OwnershipTransferred(address(0), _owner);
    }

    modifier onlyOwner() {
        require(_owner == msg.sender, "Ownable: caller is not the owner");
        _;
    }

    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        emit OwnershipTransferred(_owner, newOwner);
        _owner = newOwner;
    }
}

contract MyContract is Ownable {
    // MyContract herda as funcionalidades de Ownable
}

Interfaces

Quando usar interfaces:

  1. Desacoplamento e Abstração: Use interfaces para definir um conjunto de funções que devem ser implementadas por outros contratos, sem impor uma implementação específica. Isso promove a modularidade e permite que diferentes contratos interajam de maneira padronizada.

  2. Interoperabilidade: Quando você precisa interagir com contratos externos (por exemplo, padrões como ERC20, ERC721), interfaces permitem chamar funções desses contratos sem precisar da implementação completa.

  3. Múltiplas Implementações: Se você espera ter várias implementações diferentes para um conjunto de funções, usar interfaces define uma estrutura comum que todas as implementações devem seguir.

Exemplo de uso de interface:

pragma solidity ^0.8.0;

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    // Outras funções ERC20...
}

contract MyToken is IERC20 {
    mapping(address => uint256) private _balances;
    uint256 private _totalSupply;

    function totalSupply() external view override returns (uint256) {
        return _totalSupply;
    }

    function balanceOf(address account) external view override returns (uint256) {
        return _balances[account];
    }

    function transfer(address recipient, uint256 amount) external override returns (bool) {
        require(_balances[msg.sender] >= amount, "Insufficient balance");
        _balances[msg.sender] -= amount;
        _balances[recipient] += amount;
        return true;
    }

    // Implementação das outras funções ERC20...
}

Considerações

  • Structs são ideais para agrupar e organizar dados relacionados em uma unidade lógica, facilitando o gerenciamento e a manipulação de dados complexos.

  • Herança é útil para compartilhar lógica comum, modularizar funcionalidades e estender contratos existentes, promovendo reutilização e manutenção de código.

  • Interfaces são essenciais para definir contratos padrão e garantir interoperabilidade entre diferentes contratos, promovendo uma arquitetura desacoplada e padronizada.