WEB3DEV Español

Cover image for Diseña Patrones para Contratos Inteligentes: Acción y Control
Delia Viloria T
Delia Viloria T

Posted on

Diseña Patrones para Contratos Inteligentes: Acción y Control

Image description

Prólogo

De acuerdo a los Diseños de Patrones para Contratos Inteligentes en el papel del Ecosistema Ethereum, el diseño de patrones puede dividirse en 5 categorías: Acción y Control, Autorización, Ciclo de Vida, Mantenimiento y Seguridad, para cada uno tiene su propia colección de modelos distintos.

Esta es la primera sección en una serie de 5 partes sobre cómo resolver fallas de diseños recurrentes con patrones de diseños reusables y convencionales. Mediante esto, vamos a diseccionar la Acción y Control, un grupo de patrones que proveen mecanismos para tareas operacionales típicas.

Patrón de Pago Completo (Patrón de Retirada)

Problema

Hay distintas circunstancias en las que una transferencia puede fallar. Esto es por el hecho que la implementación que envía fondos envuelve una llamada externa (a través de los métodos de transferencia, envío o llamada) los cuales, básicamente, dan el control al contrato llamado.

Primero y antes que nada, un ataque de reentrada describe el escenario donde el contrato invocado devuelve el llamado al contrato actual, antes de la primera invocación de la función que contiene la llamada, se terminó lo cual lleva a resultados no deseados (descrito en más detalle en el patrón Revisión Efectos Interacción).

Más aún, un atacante puede atrapar el contrato en un estado inservible, usando un contrato receptor que ha recibido o fallback una función que falla (es decir, usando recert() o ejecutando operaciones costosas que consumen más que el estipendio de 2300 gas, transferidos a ellos causando un error “fuera de gas” (Out of gas, OOG) o solo dejar por fuera la implementación de ambas funciones). De esta forma, cuando transferencia se invoque para entregar los fondos al contrato “envenenado”, fallará y, por lo tanto, se quedará pegado para siempre.

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

contract Auction {
address public highestBidder;
uint256 highestBid;

/// La cantidad de Ether enviado no fue mayor a
/// la cantidad mayor actual.
error NotEnoughEther();

function bid() public payable {
if (msg.value <= highestBid) revert NotEnoughEther();

if (highestBidder != address(0)) {
// Si la llamada falla causando un rollback,
// nadie más puede subastar
highestBidder.transfer(highestBid);
}
highestBidder = msg.sender;
highestBid = msg.value;
}
}
Enter fullscreen mode Exit fullscreen mode

Solución

Un acercamiento más favorable es revertir el proceso de pago (permite a los usuarios retirar sus fondos por sí mismos), el atacante sólo causa su retirada a que falle y no el resto del trabajo del contrato.

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

contract Auction {
address public highestBidder;
uint256 highestBid;

mapping(address => uint) refunds;

/// La cantidad de Ether enviado no fue mayor a
/// la cantidad actual mayor.
error NotEnoughEther();

function bid() public payable {
if (msg.value <= highestBid) revert NotEnoughEther();

if (highestBidder != 0) {
// registro de la oferta subyacente para que se refunde
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);
}
}
Enter fullscreen mode Exit fullscreen mode

Estado del Patrón de la Máquina

Problema

Los contratos usualmente actúan como un estado de máquina, lo cual quiere decir que tienen ciertas etapas en las cuales se comportan diferentemente o en las que diferentes funciones pueden ser llamadas. Cuando interactúas con este tipo de contrato, una función de llamada puede terminar la etapa actual e iniciar el cambio en una etapa consecutiva.

Solución

Los desarrolladores usan esta construcción para romper problemas complejos en estados simples y transiciones de estado. Luego son usadas para representar y controlar la ejecución del flujo de un 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, "Function Invalid At This Stage");
_;
}

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);
}
}
Enter fullscreen mode Exit fullscreen mode

Un contrato basado en el estado de la máquina para representar un depósito bloqueado, los cuales aceptan depósitos por un período de un día y los suelta luego de siete.

El Orden del Modificador es Importante. Si atStage es combinado con timedTransitions, asegúrate que lo menciones después del último, para que el nuevo estado sea tomado en cuenta.

Commit y Patrón Revelador

Problema

Todos los datos y cada transacción es públicamente visible en la blockchain por sus características, pero un escenario de aplicación requiere que las interacciones del contrato, específicamente presentando parámetros de valores y son tratados confidencialmente.

Solución

Aplica un esquema commitment para garantizar que la presentación de un valor es vinculante y oculta hasta que una fase de consolidación se acabe, el cual después el valor será revelado, y es públicamente verificable que el valor haya permanecido sin cambio

// 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; // commit ha sido usado anteriormente
}

userCommit.status = "c"; // comitted
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; // elección que no fue committed antes
} else if (bytesStatus[0] == "r") {
return false; // elección que ya fue revelada
}

if (_commit != keccak256(abi.encode(_choice, _secret))) {
return false; // hash does not match commit
}

userCommit.choice = _choice;
userCommit.secret = _secret;
userCommit.status = "r"; // revelada
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);
}
}
Enter fullscreen mode Exit fullscreen mode

Para emprender, un usuario realizará una llamada a la función commit junto al parámetro de 32 bits, el cual es actualmente el resultado de keccak256 de ambos la elección y el secreto, luego esta función marcará el estado como committed (“c”) sin exponer ningún valor real de elección o secreto, manteniéndolos sin publicar. Después, en el momento adecuado, ese usuario revelará cualquiera de ellos usando la función reveal en donde el siguiente comando será usado, combinado con el control de estado para la resistencia a la manipulación:

_commit != keccak256(abi.encode(_choice, _secret))
Enter fullscreen mode Exit fullscreen mode

Por ahora, una vez que la transición revele (“r”) cuando el estatus del commit termine, los valores elección o secretos pueden ser revisados por la función traceCommit.

Patrón Oráculo (Proveedor de Datos)

Problema

Los contratos Ethereum se ejecutan dentro de su propio ecosistema, donde se comunican el uno con el otro y con los datos externos, sólo pueden entrar al sistema a través de la interacción externa a través de la transacción (pasando los datos a un método).

De hecho, siempre hay escenarios de aplicación que requieren conocimiento contenido fuera de la blockchain, pero los contratos de Ethereum no pueden adquirir información directamente desde el mundo externo. Por el contrario, dependen del mundo externo empujando información en la red.

Solución

Una solución para este problema es utilizar oráculos con una conexión al mundo externo. El servicio oráculo actúa como portador de datos, donde la interacción entre un servicio oráculo y el contrato inteligente es asincrónica.

Primero, una transacción invoca una función (updateExchangeRate) de un contrato inteligente que contiene una instrucción de enviar un pedido a un oráculo (pasando datos de bit tipeados en “USD” y oracleResponse como callback).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;
import "./Oracle.sol";

contract OracleConsumer {
Oracle oracle = Oracle(0x123...); // contrato conocido

modifier onlyBy(address account) {
require(msg.sender == account);
_;
}
function updateExchangeRate() {
oracle.query("USD", this.oracleResponse);
}

function oracleResponse(bytes response) onlyBy(oracle) {
// usa los datos
}
}
Enter fullscreen mode Exit fullscreen mode

Luego, de acuerdo a los parámetros de tal pedido, el oráculo formará un pedido de datos basado en la estructura y lo agregará la fila para ejecutarlo después. Una vez que el pedido es calificado para realizar el oráculo, buscará un resultado y regresará, ejecutando una función callback puesta en el contrato primario.

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

contract Oracle {
struct Request {
bytes data;
function(bytes memory) external callback;
}

address knownSource = "0x123..."; // fuente conocida
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 por el mundo externo
function reply(
uint256 requestID,
bytes memory response
) public onlyBy(knownSource) {
requests[requestID].callback(response);
}
}
Enter fullscreen mode Exit fullscreen mode

Debería tomarse en cuenta que un oráculo tiene que pagar la invocación callback, por lo tanto, un oráculo usualmente requiere el pago de una tarifa de un oráculo, además el Ether es necesario para el callback.

Uno de los servicios de oráculo más famosos es Chainlink, el cual es una red oráculo descentralizada construída en Ethereum. La red debe usarse para facilitar la transferencia de datos resistentes a la manipulación desde fuentes fuera de la cadena a contratos inteligentes en la cadena.

Conclusión

He descrito el grupo de patrones Acción y Control en detalle y proveí ejemplos de código excepcionales para ilustrarlo mejor. Recomiendo que uses, al menos, uno de estos patrones en tu próximo proyecto de Solidity para probar tu entendimiento en este tema. En el siguiente post, iré al siguiente patrón de grupo: Autorización.

Sígueme En LinkedIn Para Mantenernos Conectados

https://www.linkedin.com/in/ninh-kim-927571149/

Este artículo fue escrito por Brian Kim y traducido por Delia Viloria T. Su original se puede leer aquí.

Sería genial escucharte en nuestro Discord, puedes contarnos tus ideas, comentarios, sugerencias y dejarnos saber lo que necesitas.
Si prefieres puedes escribirnos a @web3dev_eshttps://twitter.com/web3dev_es en Twitter.

Discussion (0)