Prefácio
De acordo com o artigo Design Patterns for Smart Contracts in Ethereum Ecosystem (Padrões de Design para Contratos Inteligentes no Ecossistema Ethereum), os padrões de design podem ser divididos em 5 categorias principais, nomeadamente Ação e Controle, Autorização, Ciclo de Vida, Manutenção e Segurança, pois cada um tem sua própria coleção de modelos diferentes.
Esta é a primeira seção de uma série de 5 partes sobre como resolver falhas de design recorrentes por meio de padrões de design convencionais e reutilizáveis. Por meio disso, dissecaremos, em Ação e Controle, um conjunto de padrões que fornecem mecanismos para tarefas operacionais típicas.
Padrão de Pagamento Pull (Padrão de Retirada)
Problema
Existem diversas circunstâncias sob as quais uma transferência pode falhar. Isso se deve ao fato de que a implementação para enviar fundos envolve uma chamada externa (através dos métodos transfer, send ou call), que basicamente transfere o controle para o contrato chamado.
Em primeiro lugar, um ataque de reentrância descreve o cenário em que o contrato chamado chama de volta o contrato atual, antes que a primeira invocação da função que contém a chamada seja concluída, o que leva a resultados indesejados (descritos com mais detalhes no padrão Verificações-Efeitos-Interação).
Além disso, um atacante pode colocar o contrato em um estado inutilizável, usando um contrato receptor que tenha uma função de receive ou fallback que falhe (por exemplo, usando revert() ou executando operações caras que consomem mais do que o estipêndio de 2300 gás transferido para eles, causando um erro “out of gas” (OOG) ou simplesmente omitindo a implementação de ambas as funções). Dessa forma, sempre que uma função transfer for chamada para entregar fundos ao contrato “envenenado”, ela falhará e, portanto, ficará presa para sempre.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;
contract Auction {
address public highestBidder;
uint256 highestBid;
/// A quantia de Ether enviado não foi maior que
/// a quantia atualmente mais alta.
error NotEnoughEther();
function bid() public payable {
if (msg.value <= highestBid) revert NotEnoughEther();
if (highestBidder != address(0)) {
// se a chamada falhar, causando uma reversão,
// ninguém mais pode fazer ofertas
highestBidder.transfer(highestBid);
}
highestBidder = msg.sender;
highestBid = msg.value;
}
}
Solução
Uma abordagem mais favorável é reverter o processo de pagamento (permitir que os próprios usuários retirem seus fundos). O atacante só pode causar falha no seu próprio saque e não no restante do funcionamento do contrato.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;
contract Auction {
address public highestBidder;
uint256 highestBid;
mapping(address => uint) refunds;
/// A quantia de Ether enviada não foi maior que
/// a quantia atualmente mais alta.
error NotEnoughEther();
function bid() public payable {
if (msg.value <= highestBid) revert NotEnoughEther();
if (highestBidder != 0) {
// registra a proposta subjacente a ser reembolsada
refunds[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
function withdrawRefund() public {
uint256 refund = refunds[msg.sender];
refunds[msg.sender] = 0;
msg.sender.transfer(refund);
}
}
Padrão de Máquina de Estado
Problema
Os contratos muitas vezes funcionam como uma máquina de estado, o que significa que têm certos estágios nas quais se comportam de maneira diferente ou nas quais diferentes funções podem ser chamadas. Ao interagir com um contrato desse tipo, uma chamada de função pode encerrar o estágio atual e iniciar uma mudança para um estágio consecutivo.
Solução
Os desenvolvedores usam essa construção para dividir problemas complexos em estados simples e transições de estado. Eles são então usados para representar e controlar o fluxo de execução de um programa.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;
contract DepositLock {
enum Stages {
AcceptingDeposits,
FreezingDeposits,
ReleasingDeposits
}
Stages public stage = Stages.AcceptingDeposits;
uint256 public creationTime = block.timestamp;
mapping (address => uint256) balances;
modifier atStage(Stages _stage) {
require(stage == _stage, "Função inválida neste estágio");
_;
}
modifier timedTransitions() {
if (stage == Stages.AcceptingDeposits && block.timestamp >= creationTime + 1 days)
nextStage();
if (stage == Stages.FreezingDeposits && block.timestamp >= creationTime + 8 days)
nextStage();
_;
}
function nextStage() internal {
stage = Stages(uint8(stage) + 1);
}
function deposit() public payable
timedTransitions
atStage(Stages.AcceptingDeposits)
{
balances[msg.sender] += msg.value;
}
function withdraw() public
timedTransitions
atStage(Stages.ReleasingDeposits)
{
uint amount = balances[msg.sender];
balances[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
}
Contrato baseado em máquina de estado para representar um bloqueio de depósito, que aceita depósitos pelo período de um dia e os libera após sete dias.
A ordem do modificador é importante. Se atStage for combinado com timedTransitions, certifique-se de mencioná-lo após o último, para que o novo estágio seja levado em consideração.
Padrão de Commit (Comprometimento) e Reveal (Revelação)
Problema
Todos os dados e todas as transações são publicamente visíveis na blockchain devido às suas características, mas um cenário de aplicação exige que as interações do contrato, especificamente os valores dos parâmetros enviados, sejam tratadas de forma confidencial.
Solução
Aplicar um esquema de comprometimento para garantir que a submissão de um valor seja vinculativa e oculta até que uma fase de consolidação termine, após a qual o valor seja revelado e seja publicamente verificável que o valor permaneceu inalterado.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;
contract CommitReveal {
struct Commit {
string choice;
string secret;
string status;
}
mapping(address => mapping(bytes32 => Commit)) public userCommits;
event LogCommit(bytes32, address);
event LogReveal(bytes32, address, string, string);
function commit(bytes32 _commit) public returns (bool success) {
Commit storage userCommit = userCommits[msg.sender][_commit];
if(bytes(userCommit.status).length != 0) {
return false; // o commit foi usado anteriormente
}
userCommit.status = "c"; // comprometido
emit LogCommit(_commit, msg.sender);
return true;
}
function reveal(
string memory _choice,
string memory _secret,
bytes32 _commit
) public returns (bool success) {
Commit storage userCommit = userCommits[msg.sender][_commit];
bytes memory bytesStatus = bytes(userCommit.status);
if(bytesStatus.length == 0) {
return false; // escolha não comprometida anteriormente
} else if (bytesStatus[0] == "r") {
return false; // escolha já revelada
}
if (_commit != keccak256(abi.encode(_choice, _secret))) {
return false; // o hash não corresponde ao commit
}
userCommit.choice = _choice;
userCommit.secret = _secret;
userCommit.status = "r"; // revelado
emit LogReveal(_commit, msg.sender, _choice, _secret);
return true;
}
function traceCommit(
address _address,
bytes32 _commit
) public view returns (
string memory choice,
string memory secret,
string memory status
) {
Commit memory userCommit = userCommits[_address][_commit];
require(bytes(userCommit.status)[0] == "r");
return (userCommit.choice, userCommit.secret, userCommit.status);
}
}
Para prosseguir, um usuário fará uma chamada para a função commit junto com o parâmetro de 32 bytes que, na verdade, é o resultado de keccak256 tanto da escolha como do segredo. Depois essa função marcará o estado como comprometido (“c”) sem expor qualquer valor real da escolha ou do segredo, mantendo-os não publicados. Posteriormente, no momento adequado, esse usuário irá revelar ambos os valores usando a função reveal, na qual o seguinte comando será usado, combinado com a verificação do estado para resistência à falsificação:
_commit != keccak256(abi.encode(_choice, _secret))
A partir de agora, uma vez concluída a transição para a revelação (“r”) do estado do comprometimento, os valores da escolha ou do segredo podem ser verificados pela função traceCommit.
Padrão de Oráculo (Provedor de Dados)
Problema
Os contratos Ethereum são executados dentro de seu próprio ecossistema, onde se comunicam entre si, e os dados externos só podem entrar no sistema por meio de uma interação externa através de uma transação (passando dados para um método).
De fato, existem sempre cenários de aplicação que exigem conhecimentos contidos fora da blockchain, mas os contratos Ethereum não podem adquirir informações diretamente do mundo exterior. Pelo contrário, dependem do mundo exterior para enviar informações para a rede.
Solução
Uma solução para este problema é utilizar oráculos com uma conexão com o mundo exterior. O serviço de oráculo (oracle) atua como um portador de dados, em que a interação entre um serviço de oráculo e um contrato inteligente é assíncrona.
Primeiro, uma transação invoca uma função (updateExchangeRate) de um contrato inteligente que contém uma instrução para enviar uma solicitação a um oráculo (passando dados do tipo bytes de “USD” e oracleResponse como retorno de chamada - callback).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;
import "./Oracle.sol";
contract OracleConsumer {
Oracle oracle = Oracle(0x123...); // known contract
modifier onlyBy(address account) {
require(msg.sender == account);
_;
}
function updateExchangeRate() {
oracle.query("USD", this.oracleResponse);
}
function oracleResponse(bytes response) onlyBy(oracle) {
// use os dados
}
}
Em seguida, de acordo com os parâmetros de tal solicitação, o oráculo formará um dado baseado na struct Request e adicionará à fila para execução posterior. Assim que a solicitação estiver qualificada para execução, o oráculo irá buscar um resultado e retorná-lo executando uma função de retorno de chamada colocada no contrato principal.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;
contract Oracle {
struct Request {
bytes data;
function(bytes memory) external callback;
}
address knownSource = "0x123..."; // fonte conhecida
Request[] requests;
event NewRequest(uint);
modifier onlyBy(address account) {
require(msg.sender == account); _;
}
function query(
bytes memory data,
function(bytes memory) external callback
) public {
requests.push(Request(data, callback));
emit NewRequest(requests.length - 1);
}
// invocado pelo mundo exterior
function reply(
uint256 requestID,
bytes memory response
) public onlyBy(knownSource) {
requests[requestID].callback(response);
}
}
Deve-se notar que um oráculo tem que pagar pela invocação da chamada de retorno. Portanto, um oráculo geralmente exige o pagamento de uma taxa de oráculo mais o Ether necessário para a chamada de retorno.
Um dos serviços de oráculo mais conhecidos é a Chainlink, que é uma rede de oráculo blockchain descentralizada construída na Ethereum. A rede destina-se a ser usada para facilitar a transferência de dados à prova de falsificação de fontes fora da cadeia para contratos inteligentes na cadeia.
Conclusão
Descrevi detalhadamente o grupo do padrão Ação e Controle e forneci um código de exemplo para melhor ilustração. Recomendo que você use pelo menos um desses padrões em seu próximo projeto em Solidity para testar sua compreensão deste tópico. Na próxima postagem, passaremos para o próximo grupo de padrões, Autorização.
Siga-me no Linkedin para Ficar Conectado
https://www.linkedin.com/in/ninh-kim-927571149/
Este artigo foi escrito por BrianKim e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.
Top comments (0)