Mientras avanzamos en el fascinante mundo del desarrollo en blockchain, es hora de profundizar en cómo Solidity nos permite construir aplicaciones descentralizadas que pueden revolucionar la forma en que interactuamos con las finanzas. A continuación, te guiaremos paso a paso a través del proceso de creación de un contrato inteligente de préstamos desde cero, desgranando cada elemento para que puedas comprender y aplicar estos conocimientos en tus futuros proyectos.
Crear, desde 0, un contrato inteligente para operaciones de préstamo
Parte 1: Estableciendo los Cimientos del Contrato de Préstamos
Iniciaremos un proyecto de Dapp (aplicación descentralizada) diseñado para permitir a un usuario inyectar fondos en un Contrato Inteligente, posibilitando así que otros soliciten esos fondos en forma de préstamos y los devuelvan con intereses tras un periodo de tiempo determinado.
Este sistema opera completamente en un entorno descentralizado, marcando un contraste claro con los métodos de préstamo tradicionales donde las interacciones se hacen generalmente a través de instituciones financieras establecidas.
Al obtener el préstamo, el solicitante se compromete a devolver una cantidad superior a la recibida, generando así beneficios para el prestamista.
Iniciemos por establecer el archivo base del contrato inteligente. Dentro del directorio contracts/
, crea un archivo titulado Lending.sol
. Observarás que, por convención, los nombres de los contratos se escriben con la primera letra en mayúscula, reflejando así el nombre del contrato dentro del código.
Abre el archivo y establece su estructura inicial:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
contract Lending {}
Comenzamos con una línea que define el identificador de licencia para proteger tu obra en la blockchain. Seguidamente, especificamos la versión de Solidity a utilizar, en este caso, la 0.8.0
. Si bien todas las versiones tienen capacidades similares, los detalles de sintaxis pueden variar.
Podrías desarrollar todos tus contratos bajo esta versión y seguirían siendo funcionales en décadas, tal es la naturaleza perdurable de la blockchain.
Ahora, definamos el cuerpo del contrato, llamado Lending
. Aquí es donde residirá toda la lógica y estructura de tu contrato.
Es momento de integrar variables de estado, las cuales son accesibles globalmente dentro del contrato y quedan grabadas de forma permanente en la blockchain.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
contract Lending {
uint256 public fondosDisponibles;
address public depositante;
uint256 public tasaInteres = 10;
uint256 public tiempoParaReembolso;
uint256 public reembolsado;
uint256 public inicioPrestamo;
uint256 public prestado;
bool public prestamoActivo;
}
Estas variables definen la estructura y reglas de tu contrato. Por ejemplo, tasaInteres
se inicializa en 10, indicando que el prestatario deberá pagar un 10% adicional al devolver los fondos.
Vamos a explorar en detalle cómo definir el constructor de un contrato inteligente y qué significa cada parte de este proceso crucial. El constructor es una función única que se ejecuta automáticamente en el momento en que creas y despliegas el contrato en la red de Ethereum.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
contract Lending {
uint256 public fondosDisponibles;
address public depositante;
uint256 public interes = 10;
uint256 public tiempoParaReembolso;
uint256 public reembolsado;
uint256 public inicioPrestamo;
uint256 public prestado;
bool public prestamoActivo;
constructor(uint256 _tiempoParaReembolso, uint256 _interes) payable {
tiempoParaReembolso = _tiempoParaReembolso;
interes = _interes;
depositante = msg.sender;
depositar();
}
function depositar() public payable {
require(msg.sender == depositante, 'Debes ser el depositante');
fondosDisponibles = fondosDisponibles + msg.value;
}
}
El constructor no se inicia con la palabra clave function
porque es un tipo especial de función. En nuestro contrato Lending
, establece los parámetros iniciales como el tiempo para reembolso y la tasa de interés. Además, msg.sender
registra quién está desplegando el contrato, asumiendo que es el depositante inicial.
La función
se añade para que este depositante pueda agregar más fondos al contrato. Aquí es donde depositar
()require()
juega un papel crucial. Esta función verifica una condición y, si no se cumple, detiene la ejecución y revierte todas las transacciones. En este caso, garantiza que solo el depositante inicial pueda añadir fondos, protegiendo el contrato de acciones no autorizadas.
En este contexto, require(msg.sender == depositante, "Mensaje de error")
comprueba si la dirección que intenta ejecutar la función
es la misma que la dirección almacenada en la variable depositante. Esta verificación es crucial para la seguridad del contrato, ya que asegura que solo el depositante original puede añadir más fondos al contrato. Si alguien más intenta ejecutar la función depositar
()depositar()
, la transacción será revertida y se mostrará el mensaje de error proporcionado.
Cada vez que se ejecuta con éxito la función depositar()
marcada como payable
, el contrato incrementa los fondosDisponibles
por la cantidad exacta de Ether enviado. Esto es posible gracias a la variable especial msg.value
, que registra automáticamente cuánto Ether se ha transferido junto con la transacción. Es fundamental entender cómo funciona este proceso:
- Función Payable: La palabra clave payable en la definición de depositar() indica que esta función está diseñada para recibir Ether. No se trata solo de una etiqueta; es una instrucción para Ethereum que permite que la función maneje transacciones de Ether.
- Enviar Ether: Cuando un usuario quiere añadir Ether al contrato a través de depositar(), lo hace realizando una transacción a la dirección del contrato. En esta transacción, además de llamar a la función, el usuario especifica cuánto Ether quiere enviar. Este monto no se pasa como un parámetro en la función, sino que se incluye en los detalles de la transacción misma.
- msg.value: Dentro de depositar(), la variable msg.value se establece automáticamente al monto de Ether que acompaña la transacción. Esto ocurre independientemente de los parámetros de la función. Por ejemplo, si un usuario envía 1 Ether al contrato al llamar a depositar(), msg.value será igual a la cantidad enviada (1 Ether, expresado en wei).
Es esencial entender que en Ethereum, el Ether se transfiere como parte de la transacción y no como un argumento de la función. La función depositar()
, al ser payable
, está equipada para acceder y manipular esta cantidad de Ether enviada gracias a msg.value
.
Finalmente, mientras construyes tu contrato, es esencial considerar cómo se manejarán y extraerán estos fondos, asegurando que siempre haya un camino claro para recuperarlos si es necesario.
Este proceso es solo el comienzo de tu viaje como desarrollador de Ethereum. Pronto, añadiremos más funciones y profundizaremos en cómo interactuar con las variables y manejar transacciones, llevando tu contrato al siguiente nivel.
¡Continúa aprendiendo y experimentando!
Parte 2: Implementando la Función para Solicitar Préstamos
Ahora que tienes una mayor comprensión sobre cómo iniciar un contrato inteligente desde cero, vamos a adentrarnos en la parte más emocionante: la creación de la función para solicitar préstamos. Primero, organiza la estructura de la función de esta manera:
function solicitarPrestamo(uint256 _cantidad) public {}
Colócala justo después de la función de depositar()
. La función solicitarPrestamo
, cualquier usuario puede ejecutarla e indicar la cantidad de ETH que desea obtener.
Pasemos a la redacción del cuerpo de la función. Es vital comenzar incorporando validaciones con require
al principio para asegurar que la función se ejecute correctamente:
function solicitarPrestamo(uint256 _cantidad) public {
require(_cantidad > 0, 'Debe solicitar una cantidad');
require(fondosDisponibles >= _cantidad, 'Fondos insuficientes');
require(block.timestamp > inicioPrestamo + tiempoParaReembolso, 'El préstamo ha vencido y debe ser devuelto primero');
}
El mensaje dentro de cada declaración require
es el error que verá el usuario si no se cumple la condición.
Primero verificamos que la cantidad solicitada sea mayor que cero. Luego, aseguramos que haya fondos disponibles suficientes. Y finalmente, comprobamos que no haya un préstamo anterior vencido.
Ahora, añadamos el resto del código para esta función:
function solicitarPrestamo(uint256 _cantidad) public {
require(_cantidad > 0, 'Debe solicitar una cantidad positiva');
require(fondosDisponibles >= _cantidad, 'Fondos insuficientes para el préstamo');
require(block.timestamp > inicioPrestamo + tiempoParaReembolso, 'El préstamo ha vencido y debe ser devuelto primero');
if (prestado == 0) {
inicioPrestamo = block.timestamp;
}
fondosDisponibles -= _cantidad;
prestado += _cantidad;
prestamoActivo = true;
payable(msg.sender).transfer(_cantidad);
}
Utilizamos la condición if prestado == 0
para verificar si todavía no se ha efectuado ningún préstamo. Si se confirma que no hay préstamos activos, procedemos a marcar el inicio asignando a inicioPrestamo
el valor de block.timestamp
.
Luego, disminuimos los fondos disponibles y actualizamos la cantidad prestada y el estado del préstamo.
Finalmente, transferimos los fondos al solicitante. Cada dirección de tipo payable
, como msg.sender
, tiene una función transfer()
asociada. Convertimos msg.sender
en una dirección payable
para poder realizar transferencias y luego enviamos la cantidad solicitada por el prestatario.
Eso es esencialmente todo por ahora. En la siguiente sección, abordaremos la función crucial para devolver el préstamo.
¡Continúa con este emocionante viaje de aprendizaje!
Parte 3: Implementando la Función de Devolución de Préstamos
Tras haber aprendido cómo iniciar y configurar un contrato inteligente desde cero, es hora de sumergirnos en uno de los aspectos más interesantes: el desarrollo de una función que permita a los usuarios devolver sus préstamos. Esta función, esencial para la integridad del sistema de préstamos, garantiza que los usuarios puedan cumplir con sus obligaciones y, de ser necesario, solicitar préstamos en el futuro.
Es importante recordar que este contrato es una introducción básica y no está exento de posibles mejoras. Existe la posibilidad de que algunos usuarios tomen préstamos y no los devuelvan. Es tu responsabilidad, como desarrollador, implementar medidas en el código que permitan prestar solo a usuarios confiables o utilizar colaterales, una práctica común en préstamos completamente descentralizados. Aunque no abordaremos colaterales en este curso, te animo a intentarlo como ejercicio.
Comencemos con la creación de la función de devolución, ‘devolverPrestamo‘, iniciando con las validaciones necesarias:
function devolverPrestamo() public payable {
require(prestamoActivo, 'Debe haber un préstamo activo');
}
Esta función, marcada como payable
, permite a los usuarios devolver el Ether que han tomado prestado, más los intereses correspondientes. Primero aseguramos que haya un préstamo activo; de lo contrario, no tendría sentido intentar devolverlo.
function devolverPrestamo() public payable {
require(prestamoActivo, 'Debe haber un préstamo activo');
uint256 montoAReembolsar = prestado + (prestado * interes / 100);
uint256 montoRestante = montoAReembolsar - reembolsado;
uint256 excedente = 0;
}
En este fragmento, definimos:
montoAReembolsar
: la cantidad total que el usuario debe devolver, incluyendo el principal más los intereses.montoRestante
: lo que aún debe pagar después de cualquier cantidad ya reembolsada.excedente
: una variable que usaremos más adelante para manejar cualquier Ether enviado de más.
Es crucial recordar que en Solidity y Ethereum, no se pueden manejar números decimales directamente, ya que podrían llevar a errores de precisión. Siempre debes manejar las operaciones matemáticas cuidadosamente para evitar resultados imprevistos, siguiendo la regla de multiplicar antes de dividir.
function devolverPrestamo() public payable {
require(prestamoActivo, 'Debe haber un préstamo activo');
uint256 montoAReembolsar = prestado + (prestado * interes / 100);
uint256 montoRestante = montoAReembolsar - reembolsado;
uint256 excedente = 0;
if (msg.value > montoRestante) {
excedente = msg.value - montoRestante;
prestamoActivo = false;
} else if (msg.value == montoRestante) {
prestamoActivo = false;
} else {
reembolsado = reembolsado + msg.value;
}
payable(depositante).transfer(msg.value - excedente);
if (excedente > 0) {
payable(msg.sender).transfer(excedente);
}
}
Aquí, calculamos si el usuario ha enviado más Ether del necesario y, en tal caso, devolvemos el exceso. Si el usuario envía la cantidad exacta o más, consideramos que el préstamo está completamente pagado y cerramos el préstamo.
Finalmente, transferimos los fondos devueltos al depositante, quien proporcionó el préstamo, asegurando que reciba tanto el capital como los intereses. Si hay un excedente, se lo devolvemos al prestatario.
function devolverPrestamo() public payable {
require(prestamoActivo, 'Debe haber un préstamo activo');
uint256 montoAReembolsar = prestado + (prestado * interes / 100);
uint256 montoRestante = montoAReembolsar - reembolsado;
uint256 excedente = 0;
if (msg.value > montoRestante) {
excedente = msg.value - montoRestante;
prestamoActivo = false;
} else if (msg.value == montoRestante) {
prestamoActivo = false;
} else {
reembolsado = reembolsado + msg.value;
}
payable(depositante).transfer(msg.value - excedente);
if (excedente > 0) {
payable(msg.sender).transfer(excedente);
}
if (!prestamoActivo) {
prestado = 0;
fondosDisponibles = 0;
reembolsado = 0;
inicioPrestamo = 0;
}
}
Si el préstamo se ha cerrado y está completamente reembolsado, reiniciamos todas las variables de estado para que el contrato esté listo para el próximo préstamo.
¡Eso es todo! El contrato de préstamos está listo para ser utilizado y desplegado. A continuación, abordaremos cómo crear una interfaz de usuario que permita a las personas interactuar fácilmente con el contrato, pero antes, es esencial aprender a probar tu contrato para asegurarte de que funciona correctamente y no contiene errores.
¡Sigue adelante en esta apasionante travesía del conocimiento!
Experimentar con pruebas y depuración de tus contratos inteligentes en Ethereum
Probar tus contratos inteligentes es crucial. No solo te aseguras de que funcionen como esperas, sino que también proteges los fondos de terceros.
A diferencia de las aplicaciones tradicionales, donde las pruebas buscan principalmente la funcionalidad, en Ethereum, el asunto es mucho más delicado. Los contratos desplegados permanecerán allí indefinidamente, lo que significa que debes esforzarte al máximo para garantizar que el código funcione correctamente mucho tiempo después de haberlo creado.
Para empezar, abre tu carpeta test/
que Hardhat creó para ti al principio y crea un archivo llamado Lending.js
. Luego, agrega el siguiente código de programación en JavaScript:
const { esperado } = require('chai')
const { ethers } = require('hardhat')
let prestamo = null
describe('Lending', function () {})
Estamos importando chai
y hardhat
. Chai es simplemente una herramienta para facilitar las pruebas. Luego, definimos una variable llamada prestamo
que contendrá una instancia del contrato que acabamos de escribir.
Iniciamos las pruebas con la función describe()
(función nativa de las librerías de pruebas como Mocha y Chai para JavaScript y no debe ser traducida ni modificada), que es donde vivirán todas nuestras pruebas.
Ahora, vamos a añadir la función para desplegar un contrato de prueba:
describe('Lending', function () {
beforeEach(async () => {
const Lending = await ethers.getContractFactory('Lending')
prestamo = await Lending.deploy(30 * 24 * 60 * 60, 10) // 30 días en segundos
})
})
La función beforeEach
(nativa al igual que describe()) contiene un fragmento de código que se ejecuta justo antes de cada prueba. En este caso, estamos accediendo al contrato Lending
y desplegándolo en una cadena de bloques de prueba creada por Hardhat. Es una cadena de bloques local que existe en tu computadora.
Ahora, escribamos una prueba:
describe('Lending', function () {
beforeEach(async () => {
const Lending = await ethers.getContractFactory('Lending')
prestamo = await Lending.deploy(30 * 24 * 60 * 60, 10) // 30 días en segundos
})
it('Debe depositar exitosamente', async () => {
const unEther = ethers.BigNumber.from('1000000000000000000')
await prestamo.depositar({ value: unEther })
expect(await prestamo.fondosDisponibles()).to.eq(unEther)
})
})
La prueba comienza con la función it()
(función nativa) y empieza con un mensaje indicando lo que estás probando. En este caso, estoy probando la funcionalidad de depósito.
Primero, creo una variable para contener 1 ether. Debes saber que 1 ether se representa como 1 con 18 ceros, ya que no hay decimales en Solidity.
Por lo tanto, la unidad más baja 1
se llama wei
y 1 ether es 100000000000000000 wei
, así que la gente tiene suficiente espacio en ese número para hacer cálculos.
La variable unEther
es un BigNumber
. Es un tipo especial creado por la biblioteca ethers para que podamos lidiar con grandes números sin perder precisión.
El motivo es el hecho de que JavaScript tiene dificultades para manejar números grandes; aproxima y pierde decimales cuando el número es demasiado grande.
Después de establecer la variable, ejecutamos la función depositar()
y le pasamos 1 eth, ya que la función es payable
, lo que significa que puede recibir ether.
Luego, simplemente verificamos que la función se haya ejecutado correctamente comprobando si los fondosDisponibles
en el contrato inteligente tienen un valor de 1 ether, que es exactamente lo que le enviamos.
Para ejecutar la prueba, ve a tu terminal o línea de comandos y escribe:
npx hardhat test
Se ejecutará la prueba y mostrará que ha pasado. Si no se completa con éxito, significa que tu contrato tiene algún error que necesitas corregir para que funcione. Eso es exactamente lo que buscas para poder mejorar tu código.
Añadamos otra prueba:
it('Debe tomar prestado fondos exitosamente', async () => {
const [, prestatario] = await ethers.getSigners()
const unEther = ethers.BigNumber.from('1000000000000000000')
await prestamo.depositar({ value: unEther })
expect(await prestamo.fondosDisponibles()).to.eq(unEther)
const balance1 = await ethers.provider.getBalance(prestatario.address)
lending = prestamo.connect(prestatario)
const tx = await prestamo.solicitarPrestamo(unEther.div(2))
const recibo = await tx.wait()
const gasUsado = recibo.gasUsed.mul(recibo.effectiveGasPrice)
const balance2 = await ethers.provider.getBalance(prestatario.address)
expect(await prestamo.fondosDisponibles()).to.eq(unEther.div(2))
expect(balance2.add(gasUsado)).to.eq(balance1.add(unEther.div(2)))
})
Hay mucho que desglosar, así que permítannos explicarlo paso a paso:
- Vamos a obtener la dirección del prestatario. Lo hacemos con el método
getSigners()
. Al usar un array, simplemente solicitamos la segunda cuenta. No pienses demasiado en ello. - Luego, repetimos la prueba anterior con la funcionalidad de depósito. No tengas miedo de repetir código en las pruebas y escribir de manera “impropia” porque el código no se usará fuera de probar tu contrato.
- Ahora comienza la verdadera prueba, obtenemos el balance de la cuenta del prestatario. Lo hacemos con el método
getBalance
del proveedor de ethers. Lo usaremos más adelante. - Para que el prestatario interactúe con el contrato y ejecute las funciones, necesitamos hacerle saber a la aplicación que queremos eso con la función
lending.connect()
, que recibe todo el objeto del prestatario, es decir, sin acceder al parámetroaddress
. Simplemente almacenamos esa conexión en la misma variablelending
porque queremos interactuar con el contrato inteligente como el prestatario. - Ahora ejecutamos la función
solicitarPrestamo
del contrato inteligente de lending. Mientras al mismo tiempo almacenamos la transacción en una variable llamadatx
. En este caso, estamos tomando prestada la mitad de un ether. - Luego, utilizamos
wait()
para esperar que la transacción sea procesada por la cadena de bloques para obtener elrecibo
, que es simplemente un montón de datos sobre la transacción de la cadena de bloques. - Verificar el gas utilizado. La forma en que obtenemos el gas consumido por la transacción es multiplicando el
gasUsed
por eleffectiveGasBalance
. No tienes que entenderlo profundamente, solo saber que así es como obtenemos el valor del gas utilizado. Lo necesitamos para más adelante. - Luego, verificamos el balance del prestatario nuevamente para ver cómo cambió después de ejecutar la función
solicitarPrestamo()
del contrato inteligente, para ver si realmente obtuvimos el ether o no. - Finalmente, verificamos la variable
fondosDisponibles
y vemos si es igual a la mitad de un ether. Si es así, eso significa que la funciónsolicitarPrestamo()
se ejecutó correctamente. También verificamos el balance final del prestatario, sumamos el gas consumido y lo comparamos con el balance anterior más la mitad de un ether. Así podemos ver el balance aumentando. Eso fue mucho. Pero oye, estoy seguro de que aprendiste bastante.
Ahora, agreguemos una prueba final para asegurarnos de que la función devolverPrestamo()
de tu contrato funcione como se espera.
Aquí está la prueba completa:
it('Debe devolver un préstamo parcialmente', async () => {
const [prestador, prestatario] = await ethers.getSigners()
const unEther = ethers.BigNumber.from('1000000000000000000')
await prestamo.depositar({ value: unEther })
expect(await prestamo.fondosDisponibles()).to.eq(unEther)
const balance1 = await ethers.provider.getBalance(prestatario.address)
lending = prestamo.connect(prestatario)
const tx = await prestamo.solicitarPrestamo(unEther.div(2))
const recibo = await tx.wait()
const gasUsado = recibo.gasUsed.mul(recibo.effectiveGasPrice)
const balance2 = await ethers.provider.getBalance(prestatario.address)
expect(await prestamo.fondosDisponibles()).to.eq(unEther.div(2))
expect(balance2.add(gasUsado)).to.eq(balance1.add(unEther.div(2)))
const balance3 = await ethers.provider.getBalance(prestador.address)
await prestamo.devolverPrestamo({ value: unEther.div(10) })
const balance4 = await ethers.provider.getBalance(prestador.address)
expect(balance4).to.eq(balance3.add(unEther.div(10)))
})
Como puedes ver, esta vez estamos obteniendo la dirección tanto del prestador como del prestatario porque necesitaremos ambas para simular todo el proceso de depósito, préstamo y devolución.
La mayor parte del código es igual que la prueba anterior, lo único que cambia es esta porción:
const balance3 = await ethers.provider.getBalance(prestador.address)
await prestamo.devolverPrestamo({ value: unEther.div(10) })
const balance4 = await ethers.provider.getBalance(prestador.address)
expect(balance4).to.eq(balance3.add(unEther.div(10)))
Donde básicamente verificamos primero el balance del prestador, luego ejecutamos la función devolverPrestamo()
para devolver una parte del préstamo, el 10% de 1 ether. Luego, verificamos que se haya devuelto correctamente con la última verificación expect()
.
¡Eso es todo! Ahora que ya sabes cómo probar contratos inteligentes, un proceso necesario aunque extenso para cualquier contrato público, te invito a seguir leyendo.
A continuación, nos adentraremos en la creación del sitio web que interactúa con el contrato inteligente, una de las etapas más fascinantes de todo este proceso.
👉 Dar vida al aspecto visual de tu aplicación de préstamos descentralizada.
💰📊 Gracias por llegar hasta aquí. Tu dedicación y curiosidad son el combustible que impulsa la innovación en el espacio Crypto. Juntos, estamos forjando el camino hacia un futuro financiero más abierto y accesible.
Piensa en GRANDE, piensa en CRIPTO. 😉🦊