Smart contracts #3: Contratos mais avançados

Este é o terceiro artigo de uma série titulada “Smart Contracts”, onde irei tratar de smart contracts com foco na plataforma Ethereum. Com esta série, meu objetivo é trazer conteúdo completo e de maneira compreensiva sobre o tema, que tratarei de forma teórica e prática. Um novo artigo será publicado toda terça-feira. Neste artigo, vamos utilizar o Remix para criar contratos funcionais um pouco mais avançados.

Caso você tenha interesse em aprender mais e tenha um conhecimento razoável da língua inglesa, toda quarta-feira publico também um artigo tratando de uma definição importante do mundo da tecnologia blockchain, em uma série titulada “Blockchain definition of the week”.

Artigos anteriores:

Smart Contracts #1: Introdução

Smart Contracts #2: Construindo contratos


No artigo anterior, criamos dois smart contracts e testamos suas funcionalidades. Hoje vamos fazer o mesmo, porém com contratos mais complexos. O segundo contrato mostrado no artigo de hoje será utilizado ao longo da série para construir um dApp.

Primeiro contrato

Para o primeiro contrato de hoje, vamos criar um sistema que possibilita envio de mensagens com senha. Ao fim da explicação, este contrato servirá de exemplo para indicar uma característica importante de smart contracts na rede Ethereum. Vamos lá.

pragma solidity ^0.4.24;
contract MensagemSecreta {
    string mensagem;
string escolhersenha;
string senha;
    function enviarMensagem(string _mensagem, string _escolhersenha) public {
require (bytes(_mensagem).length > 0 && bytes(_escolhersenha).length > 0 );
mensagem = _mensagem;
escolhersenha = _escolhersenha;
}
    function lerMensagem (string _senha) public view returns (string) {
require (verificarSenha(escolhersenha, _senha) == true);
return (mensagem);
}
    function verificarSenha (string storage _a, string memory _b) internal pure returns(bool) {
bytes storage a = bytes(_a);
bytes memory b = bytes(_b);

if (keccak256(a) != keccak256(b)) {
return false;
}
return true;
}
}

Algumas linhas inteiras não couberam na página do Medium, prejudicando a indentação do código.

No artigo de hoje vou dissecar apenas novos aspectos relacionados à sintaxe. Atributos que não forem tratados neste artigo foram explicados anteriormente.

  • string mensagem; string escolhersenha; string senha;

Definimos as variáveis de natureza “cadeia de caracteres”: mensagem, escolhersenha, senha.

  • function enviarMensagem (string _mensagem, string _escolhersenha)

Desta vez, definimos parâmetros para nossa função. O usuário poderá registrar texto para ambos os parâmetros. No caso, queremos que o usuário escreva uma mensagem e defina uma senha.

  • require (bytes(_mensagem).length > 0 && bytes(_escolhersenha).length > 0 );

Garante que o usuário não irá deixar a mensagem nem a escolha de senha em branco. Para isso, usamos require e definimos que os bytes da cadeia de caracteres em questão necessitam ser maiores que zero. Ou seja, o usuário pode escrever no mínimo um caractere, não menos. bytes().length verifica o número de bytes de uma cadeia de caracteres.

  • mensagem = _mensagem; escolhersenha = _escolhersenha;

Definimos que o contrato deve guardar o texto escrito no campo “mensagem” como o valor da variável mensagem e o texto do campo “escolhersenha” como o valor da variável escolhersenha.

  • function lerMensagem (string _senha)

Definimos a cadeia de caracteres “senha” como parâmetro da função lerMensagem.

  • require (verificarSenha(escolhersenha, _senha) == true);

Estabelece o requerimento de que, para que a função seja executada, o resultado da função interna verificarSenha precisa ser verdadeiro (true). A comparação é feita entre a senha escrita, definida pela cadeia de caracteres “senha” e a senha definida anteriormente, definida pela variável escolhersenha.

A função verificarSenha é chamada pela função lerMensagem e seu resultado é registrado. Se o resultado for true, a função executa, caso contrário, é retornado erro.

  • function verificarSenha (string storage _a, string memory _b) internal pure returns(bool) {}

Como um todo, essa função compara uma cadeia de caracteres previamente armazenada no contrato (storage) com uma cadeia de caracteres na memória de curto-prazo (memory). É uma função interna que não pode ser chamada por usuários, apenas por eventos/funções do contrato em si. Isto é definido pelo modificador internal .

returns(bool) define que a função usa lógica booleana, e retorna um resultado de true ou false (verdadeiro ou falso).

        bytes storage a = bytes(_a);
bytes memory b = bytes(_b);

if (keccak256(a) != keccak256(b)) {
return false;
}
return true;

Para comparar as duas strings, é computado o hash tipo keccak256 (Ethereum-SHA-3) dos bytes de cada uma, que é então comparado para verificar se ambos são ou não iguais. Se sim, a função retorna true . Se não, a função retorna false e impede a execução da função anterior. Estas funcionalidades são definidas pelo bloco de código acima.

Testando o código

Utilizando os métodos do artigo anterior, use a JavaScriptVM para testar seu contrato. O contrato deveria permitir que você defina uma mensagem e uma senha que permita a leitura da mensagem, independente do endereço do usuário. Teste a senha correta e senhas erradas, e tente ler a mensagem de diferentes endereços.

Importante — Veja abaixo antes de testar!

Caso você escreva a mensagem e senha na barra como abaixo, a escrita necessita ser feita em formato:

“mensagem”, “escolhersenha”

São necessárias aspas duplas e vírgulas para funcionamento correto.

Alternativamente, você pode também clicar na seta apontando para baixo no canto direito, que deverá exibir o seguinte:

Neste caso, não são mais necessárias aspas nem vírgulas.

Limitações

Aqueles que testaram as diversas funcionalidades do contrato devem ter percebido que a primeira limitação clara deste contrato é que ele só permite o envio de uma mensagem, para qual a senha é trocada toda vez que uma nova mensagem é escrita. Também não direcionamos o envio da mensagem para nenhum usuário. Porém, é possível criar um contrato que envia mensagens diferentes, para usuários diversos e com senhas específicas, mas não trataremos disto nesse artigo, pela limitação que descreverei abaixo.

Na verdade, a maior limitação deste contrato, e a razão pela qual quis exibí-lo, é que as informações guardadas nele não são privadas. Caso um usuário tente chamar a função lerMensagem sem a senha correta, a função não executaria, porém existem maneiras de ler toda a informação armazenada no contrato, devido à natureza do Ethereum.

É importante ressaltar este ponto, para que usuários não construam contratos que armazenem informações sensíveis, visto que as mesmas podem ser reveladas facilmente. Com isso, o contrato acima deve ser usado apenas para teste, mas não para envio de mensagens confidenciais.

Segundo contrato

Vamos agora construir e analizar o contrato que, durante a série, publicaremos na blockchain, e para o qual criaremos um dApp. O contrato é um contrato feito para investidores, que querem “HODL”, mas não conseguem manter as mãos longe do investimento. O smart contract permite que você deposite Ether, e estabeleça uma data mínima para poder retirá-lo, que garante que você consiga HODL com tranquilidade. Adicionamos também uma funcionalidade de senha.

IMPORTANTE: O possível uso deste contrato para guardar investimentos reais é responsabilidade do usuário. Este smart contract foi criado para efeito didático, e seu código não foi auditado nem testado na mainnet. Não é recomendada a utilização do mesmo para fins que não de teste, e eu não me responsabilizo por perda de Ether causada pelo seu uso. É recomendado que o usuário estude o tópico mais à fundo antes de aplicar os conhecimentos passados por esta série de artigos em aplicações de uso real.
pragma solidity^0.4.24;
contract owned {
address owner;
    constructor() public {
owner = msg.sender;
}
    modifier onlyowner() {
if (msg.sender == owner) {
_;
}
}
}
contract HODL is owned {
    uint hoje;    
string definirsenha;
string senha;
    constructor() public {
hoje = block.timestamp;
}
    function depositarInvestimento() public payable onlyowner {}
    function definirSenha(string _definirsenha) public onlyowner {
require(bytes(_definirsenha).length > 0);
definirsenha = _definirsenha;
}
    function retirarInvestimento(string _senha) public onlyowner { 
require (verificarSenha(definirsenha, _senha) == true,
"Senha incorreta. Tente novamente."
);
require (
block.timestamp >= hoje + 365 days,
"Você não queria HODL?"
);
owner.transfer((address(this)).balance);
}
    function verificarSenha (string storage _a, string memory _b) internal pure returns(bool) {
bytes storage a = bytes(_a);
bytes memory b = bytes(_b);
        if (keccak256(a) != keccak256(b)) {
return false;
}
return true;
}
}

Com tudo que já cobrimos anteriormente, este contrato não possui muitas novas funcionalidades, apenas:

  • modifier onlyowner() { }

Em contratos passados, tratamos de “modificadores” como view e pure , que limitam as funcionalidades de uma função. Com a declaração acima, criamos um novo modificador, denominado onlyowner . Você pode alterar este nome, porém onlyowner é utilizado com frequência.

O modificador onlyowner determina que só o “dono” do contrato (endereço que o criou) pode chamar a função. No artigo anterior, utilizamos require(msg.sender == owner); para obter o mesmo resultado. Ao declarar este modificador, basta adicionar onlyowner juntamente com os outros modificadores da função para não precisar escrever require(msg.sender == owner); todas as vezes.

  • uint hoje;

uint significa unsigned integer, ou “número inteiro sem sinal”. uint é o mesmo que uint256 que indica um número inteiro de até 256 bits. Garante que até números muito grandes vão poder ser computados. Podemos utilizar desde uint8 até o máximo de uint256 , com os bits sempre aumentando em 8. (uint16 , uint24 , etc).

Com uint hoje; declaramos a variável hoje, que é um número.

  • hoje = block.timestamp;

block.timestamp é utilizado para definir o presente, referente ao último bloco publicado na rede. É utilizada a timestamp do bloco para tal. No caso acima, quando o contrato for publicado, o construtor irá executar e guardar a timestamp como o valor da variável hoje, determinando quando o contrato foi publicado. Com isso, podemos definir um período de tempo necessário, desde a publicação do contrato, para que uma função seja chamada, por exemplo.

  • payable

Modificador que permite que uma função receba Ether de um usuário. É necessário que exista um modificador com esta finalidade para previnir que usuários acidentalmente enviem Ether que pode ficar preso em contratos que não permitem a retirada deste Ether. Funções sem payable irão retornar qualquer valor em Ether enviado por usuários de volta para o endereço ao ser chamada a função.

  • require(requerimento, “Mensagem aqui”);

Antes de fechar os parênteses de um requerimento, podemos adicionar uma vírgula e denotar uma mensagem entre aspas duplas. Esta mensagem será retornada caso o requerimento não seja cumprido. Por exemplo:

require(verificarSenha(definirsenha, _senha) == true, 
"Senha incorreta. Tente novamente."
);

Caso a senha esteja errada, o contrato retorna: “Senha incorreta. Tente novamente”.

  • owner.transfer((address(this)).balance);

this se refere sempre ao endereço do contrato. balance se refere ao saldo de um endereço específico. Acima, executamos uma transferência para owner do endereço this (contrato), com valor saldo total do contrato.

Serve para que o investidor consiga retirar todo o Ether que depositou no contrato.

  • require ( (block.timestamp >= hoje + 365 days)

Determina que a função só pode ser chamada após 365 dias contados a partir da publicação do contrato. A timestamp do bloco precisa ser maior ou igual a timestamp da criação, com o número de segundos referentes a 365 dias adicionado.

Os símbolos de “maior que” e “menor que” (> & <) são aplicados normalmente. Outros símbolos importantes são:
>= indica “maior ou igual”
<= indica “menor ou igual”.
== indica “igual a”
!= indica “diferente de”

Em Solidity, podemos determinar tempo em seconds (segundos), minutes (minutos), days (dias), entre outros. Não é mais recomendado o uso de years , já que nem todos os anos tem o mesmo número de dias.

Quando for testar o contrato, use segundos, para não ter que esperar muito. Para usar segundos como medida, você não precisa não escrever nada extra, ou seja:

require(block.timestamp >= hoje + 60);

No caso acima, ‘60’ automaticamente se refere à segundos.

Considerações

O contrato “HODL” permite que o usuário deposite Ether, determinando quando pode retirar este investimento do contrato. O período de tempo pelo qual o investimento é travado dentro do contrato é determinado por seu criador na escrita do smart contract.

A ideia de utilizar uma senha como segurança adicional é para que mesmo que com acesso ao seu endereço, uma pessoa mal-intencionada não consegue acesso aos fundos sem sua senha. Claro, como discutimos anteriormente, esta senha pode ser descoberta por pessoas com certo conhecimento da plataforma Ethereum, porém, como vamos utilizar o contrato em conjunto com um dApp, a senha ao menos protege o usuário daqueles que não tem experiência com blockchain. Resumindo, é necessário acesso ao seu endereço e alguma maneira de conseguir a senha para acessar seus fundos. Isto tudo, claro, após o tempo determinado.

É possível implementar uma funcionalidade de senha segura, utilizando uma função de hash e guardando o hash no storage do contrato ao invés da senha em seu estado cru. Porém não vamos tratar disto nessa série.

Vale também lembrar que para mudar a senha, basta registrar uma nova com a mesma função. A senha válida é sempre a última registrada. Por questões de segurança, no dApp não iremos incluir a função de registrar a senha, e isto terá que ser feito por meio de outra plataforma.

Testando o contrato

Para testar o contrato no Remix, lembre-se de remover ou modificar o requerimento de tempo, para poder testar a função retirarInvestimento. Caso contrário, a função sempre vai retornar erro. Experimente registrar e mudar a senha, depositar diferentes valores e testar o contrato de endereços diversos.

Up next…

No próximo artigo vamos aprender a publicar o smart contract e como utilizar diferentes ferramentas essenciais. Vamos também testar o contrato na rede Ropsten do Ethereum.

Como sempre, deixe abaixo seus comentários e sugestões.