WEB3DEV

Cover image for Criando um Contrato de Troca de Casas Usando Solidity e Hardhat - Parte 1
Panegali
Panegali

Posted on

Criando um Contrato de Troca de Casas Usando Solidity e Hardhat - Parte 1

Nesta primeira parte desta série, vamos aprender como criar um contrato inteligente que permite a dois usuários trocarem suas casas. Você pode ler o código completo do contrato aqui.

Como o contrato funciona

As próximas seções descrevem como o contrato funciona sem entrar nas peculiaridades de cada método, mas mostrando uma visão geral do fluxo do contrato.

Inicializar o contrato

O proprietário do contrato (quem o implanta) chama a função initialize. Essa função define o status do contrato como INITIALIZED (inicializado) e armazena os dados da casa para que outros usuários possam consultá-los e fazer ofertas de troca.

Apenas o proprietário do contrato pode chamar esta função.

Adicionar uma nova oferta

Um usuário pode chamar a função addOffer para enviar uma oferta de troca. Após ser chamada, um novo evento é emitido contendo a oferta. O proprietário não pode chamar essa função, já que ele não troca a própria casa.

Aceitar uma oferta

Se o proprietário do contrato ler uma oferta e quiser aceitá-la, ele pode chamar o método acceptOffer. Este método alterará o status do contrato para ACCEPTED (aceito) e o contrato não aceitará mais ofertas. Apenas o proprietário do contrato pode chamar esta função.

Confirmar a Troca

Após o proprietário do contrato aceitar uma oferta, o targetUser (usuário-alvo) pode confirmar a troca chamando a função confirmSwap. Esta função possui algumas condições:

  • Se a oferta especificar que o proprietário deve pagar uma certa quantia ao usuário-alvo, o proprietário terá que depositar ether suficiente para que o contrato possa transferir os fundos necessários ao usuário-alvo. Para fazer isso, o proprietário pode chamar a função deposit, que é payable (pagável).
  • O mesmo se aplica ao usuário-alvo. Se a oferta especificar que o usuário-alvo deve pagar uma certa quantia ao proprietário, o usuário-alvo também terá que depositar ether suficiente.

Esta função conclui a troca de propriedade de cada casa.

O Código do Contrato

Agora que vimos uma visão geral de como o contrato funciona, vamos nos aprofundar no código.

Eventos e Variáveis de Armazenamento

event NewOffer(Offer offer);
event BalanceUpdated(address, uint256);

address payable owner;
address payable targetUser;
Swap swap;
Statuses status;
mapping (address => uint256) balances;
Enter fullscreen mode Exit fullscreen mode

Como podemos ver, estas primeiras linhas declaram dois eventos e cinco variáveis. Vamos descrevê-los um por um:

  • event NewOffer: a função addOffer emite este evento após um usuário enviar uma oferta. Uma variável do tipo Offer é passada para o evento (veremos o tipo Offer na próxima seção).
  • event BalanceUpdated(address, uint256): a função deposit emite este evento quando o proprietário ou o usuário-alvo depositam fundos no endereço do contrato. Isso é necessário quando a troca requer a transferência de um pagamento extra para o originador ou usuário-alvo. BalanceUpdated recebe o endereço que atualiza seu saldo e o valor.
  • address payable owner: esta variável armazena o endereço do proprietário. Aquele que implanta o contrato e deseja trocar sua casa. Este endereço precisa ser pagável para que o contrato possa transferir fundos para ele, se necessário.
  • address payable targetUser: esta variável armazena o endereço do usuário que fez a oferta aceita. Este endereço também precisa ser pagável para que o contrato possa transferir fundos para ele, se necessário.
  • Swap swap: esta variável armazena as informações da troca (veremos a struct Swap na próxima seção).
  • Statuses status: esta variável armazena o status da troca. Veremos os possíveis status na próxima seção, onde veremos o enum Statuses.
  • mapping (address => uint256) balances: este mapeamento armazena saldos tanto do proprietário quanto do usuário-alvo. É necessário transferir fundos se a troca exigir.

Structs e Enums:

enum Statuses {
    PENDING,
    INITIALIZED,
    ACCEPTED,
    FINISHED
}

struct Swap {
    House origin;
    House target;
    uint256 amountPayOriginToTarget;
    uint256 amountPayTargetToOrigin;
}

struct House {
    string houseType;
    uint value;
    string link;
    address propietary;
}

struct Offer {
    House house;
    address targetUser;
    uint256 amountPayOriginToTarget;
    uint256 amountPayTargetToOrigin;
}
Enter fullscreen mode Exit fullscreen mode

O contrato declara algumas estruturas que incluem informações comuns e um enum. Vamos ver cada uma delas:

  • enum Statuses: armazena os possíveis estados do contrato.
  • struct House: inclui informações sobre uma casa. Esta struct contém as seguintes chaves:
  • houseType: tipo de casa, pode ser duplex, apartamento etc.
  • uint value: o valor da casa. Pode ser o valor em moeda fiduciária ou criptomoeda. Não importa, já que não é usado como valor para transferência.
  • string link: o site onde os usuários podem buscar mais informações sobre a casa.
  • address propietary: o endereço do usuário que fez a oferta ou o endereço do proprietário, se a struct armazenar as informações da casa do proprietário.

  • struct Offer: esta struct armazena uma oferta feita por um usuário que deseja fazer uma troca. Esta struct contém as seguintes chaves:

  • House house: a casa que o usuário oferece para a troca.

  • address targetUser: o endereço do usuário que envia a oferta.

  • uint256 extraPayOriginToTarget: um valor maior que 0 indica que o proprietário deve pagar uma quantia extra ao usuário alvo.

  • uint256 extraPayTargetToOrigin: um valor maior que 0 indica que o usuário alvo deve pagar uma quantia extra ao proprietário.

  • struct Swap: esta estrutura contém os dados da troca:

  • House origin: armazena a casa do proprietário.

  • House target: armazena a casa alvo (a casa da oferta aceita pelo proprietário).

  • uint256 amountPayOriginToTarget: este valor é copiado da oferta aceita.

  • uint256 amountPayTargetToOrigin: este valor também é copiado da oferta aceita.

O constructor (construtor)

constructor() {
    owner  = payable(msg.sender);
    status = Statuses.PENDING;
}
Enter fullscreen mode Exit fullscreen mode

O construtor é realmente simples. Ele define o endereço do remetente (quem implanta o contrato) como o proprietário e define o status como PENDING (pendente). Observe que convertemos o endereço do remetente em uma função pagável para que o contrato possa transferir fundos para ele.

Modificadores

modifier hasToBeInitialized {
    require(keccak256(abi.encodePacked(status)) == keccak256(abi.encodePacked(Statuses.INITIALIZED)), 'Uma oferta já foi aceita ou o contrato não foi inicializado');
    _;
}

modifier isOwner {
   require(msg.sender == owner, 'Proprietário do contrato necessário');
   _;
}
Enter fullscreen mode Exit fullscreen mode

O contrato cria dois modificadores que serão usados posteriormente:

  • modificador hasToBeInitialized: exige que o contrato seja inicializado, ou seja, a variável status precisa ser igual ao status INITIALIZED (inicializado).
  • modificador isOwner: exige que o remetente seja o proprietário do contrato.

Inicialize o contrato

function initialize(House memory house ) external isOwner {
    house.propietary = owner;
    swap.origin = house;
    status = Statuses.INITIALIZED;
}
Enter fullscreen mode Exit fullscreen mode

A função initialize armazena a casa que o proprietário deseja trocar e marca o status como INITIALIZED. Esta função utiliza o modificador isOwner, garantindo que apenas o proprietário do contrato possa invocá-la.

Adicione ofertas e confirme uma delas.

function addOffer(House memory house, uint256 amountPayOriginToTarget, uint256 amountPayTargetToOrigin) external hasToBeInitialized {
   Offer memory offer = Offer(house, msg.sender, amountPayOriginToTarget, amountPayTargetToOrigin );
   emit NewOffer(offer);
}

function acceptOffer(address payable _targetUser, House memory house, uint256 amountPayOriginToTarget, uint256 amountPayTargetToOrigin) external hasToBeInitialized isOwner 
{
    targetUser = _targetUser;
    House memory targetHouse = house;

    swap.target = targetHouse;
    swap.amountPayOriginToTarget = amountPayOriginToTarget;
    swap.amountPayTargetToOrigin = amountPayTargetToOrigin;
    status = Statuses.ACCEPTED;
}
Enter fullscreen mode Exit fullscreen mode

A função addOffer emite um evento NewOffer (Nova oferta) com os dados da oferta enviados por um usuário interessado em trocar. Ela utiliza a struct Offer para agrupar os dados da oferta. Essa função requer que o contrato esteja inicializado (usa o modificador hasToBeInitialized).

A função acceptOffer aceita uma oferta. Ela define o targetUser como o endereço recebido como primeiro parâmetro. Também define os dados da oferta aceita para a variável swap. Por fim, ela define o status do contrato como ACCEPTED.

Deposit (Depósito).

function deposit() external payable {
    if(swap.amountPayOriginToTarget > 0){
       require(msg.sender == owner,  'A origem deve depositar fundos suficientes');
    }

    if(swap.amountPayTargetToOrigin > 0){
       require(msg.sender == targetUser,  'O alvo deve depositar fundos suficientes');
    }

    balances[msg.sender] = msg.value;
    emit BalanceUpdated(msg.sender, msg.value);
}
Enter fullscreen mode Exit fullscreen mode

A função deposit é uma função pagável. Tanto o proprietário quanto o targetUser podem chamá-la para depositar fundos no contrato, permitindo que o contrato posteriormente transfira esses fundos. Como mencionado no início do artigo, o contrato possui uma variável de mapeamento para armazenar os saldos. Esta função atribui o saldo ao endereço correspondente e emite um evento BalanceUpdated. Antes de fazer isso, ela verifica as seguintes condições:

  • Se amountPayOriginToTarget é maior que 0, então requer que o proprietário seja o remetente.
  • Se amountPayTargetToOrigin é maior que 0, então requer que o targetUser seja o remetente.

Realiza a troca (swap).

function performSwap() external
{
    require(targetUser == msg.sender, 'Somente o usuário alvo pode confirmar a troca');
    require(keccak256(abi.encodePacked(status)) == keccak256(abi.encodePacked(Statuses.ACCEPTED)), 'Uma oferta ainda não foi aceita');

    if(swap.amountPayOriginToTarget > 0) {
        bool success = sendTransfer(owner, swap.amountPayOriginToTarget);
        require(success, "Falha na transferência para o destino.");
    }

    if(swap.amountPayTargetToOrigin > 0) {
        bool success = sendTransfer(targetUser, swap.amountPayTargetToOrigin);
        require(success, "Falha na transferência para o proprietário.");
    }

    swap.origin.propietary = targetUser;
    swap.target.propietary = owner;
    status = Statuses.FINISHED;
}

function sendTransfer(address payable addr, uint256 amount ) private returns (bool){
    uint256 etherBalance = balances[addr] / 1 ether;
    require(etherBalance >= amount, 'O depósito não foi enviado ou é inferior ao exigido' );
    (bool success, ) = addr.call{value: amount}("");
    return success;
}
Enter fullscreen mode Exit fullscreen mode

A função performSwap transfere fundos do origin para o target se swap.amountPayOriginToTarget for maior que 0, ou do target para o origin se swap.amountPayTargetToOrigin for maior que 0.

A função privada sendTransfer recebe o endereço pagável para enviar os fundos e a quantia a ser enviada. Como os saldos são armazenados em wei, a função primeiro converte para ether e compara com a quantia a ser transferida. Se não houver saldo suficiente, a função irá reverter a operação.

Após enviar a transferência, performSwap define targetUser como o proprietário do origin e o proprietário como o proprietário do target. Por fim, ele define o status do contrato como FINISHED (finalizado).

Como podemos ver nas duas primeiras linhas, esta função requer que o contrato tenha um status ACCEPTED e que seja o targetUser quem a invoque.

Obter informações sobre a troca e o status.

function getStatus() public view returns (Statuses) {
    return status;
}

function info() public view hasToBeInitialized returns (Swap memory) {
    return swap;
}
Enter fullscreen mode Exit fullscreen mode

A função getStatus é uma função de visualização (não modifica o status do contrato) que simplesmente retorna o status atual.

A função info retorna os dados da troca quando o status do contrato é INITIALIZED. Isso permite que os usuários consultem as informações da troca e façam ofertas.

Conclusão

Esta primeira parte do artigo mostrou como criar um contrato em Solidity para gerenciar uma troca de casas. Na próxima parte, aprenderemos como compilar e testar nosso contrato usando as utilidades do Hardhat.


Artigo escrito por Nacho Colomina Torregrosa. Traduzido por Marcelo Panegali.

Latest comments (0)