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;
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;
}
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;
}
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');
_;
}
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;
}
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;
}
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);
}
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 otargetUser
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;
}
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;
}
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)