Las finanzas descentralizadas (DeFi) han transformado la forma en que las personas prestan y piden dinero prestado, eliminando intermediarios y utilizando contratos inteligentes para gestionar las transacciones de forma automática. Lo que antes requería bancos y papeleo ahora puede ocurrir en unos pocos clics, todo on-chain.
Linea ayuda a los desarrolladores a crear aplicaciones DeFi con mayor escalabilidad y menores costos de gas en comparación con la red principal de Ethereum, mediante el uso de rollups de conocimiento cero (zk-rollups) para agrupar y comprimir transacciones.
En esta guía, te explicaremos paso a paso cómo construir e implementar un contrato inteligente sencillo de préstamos en Linea. Aprenderás a gestionar garantías, emitir préstamos y configurar reglas de reembolso: todas las funciones esenciales de un sistema de préstamos DeFi.
Al finalizar, tendrás un contrato inteligente funcional y una comprensión sólida de cómo funcionan los préstamos DeFi por dentro. Tanto si eres nuevo en el desarrollo blockchain como si buscas perfeccionar tus habilidades, esta guía paso a paso te dará las herramientas para empezar a construir en Linea.
Requisitos previos
Debes tener conocimientos básicos de Solidity.
Debes tener Nodejs y Foundry instalados en tu PC.
Descripción general del contrato
Este contrato permite:
Depósito de garantía: Los usuarios pueden depositar garantías en el contrato, con los detalles almacenados de forma segura en el contrato inteligente.
Préstamo: Los usuarios pueden solicitar un préstamo respaldado por su garantía depositada.
Reembolso: Los usuarios pueden reembolsar sus préstamos para liberar su garantía.
Retiro de garantía: Los usuarios pueden retirar la garantía que no esté bloqueada por un préstamo activo.
Configuración del proyecto
Utilizaremos el framework Foundry para este proyecto. Con Foundry instalado, ejecuta el siguiente comando para iniciar un nuevo proyecto:
forge init lending
Abre la carpeta del proyecto en Vscode o tu editor de código favorito, y elimina los archivos scripts/counter.s.sol, src/counter.sol y test/counter.t.sol.
Instalar todas las dependencias
forge install foundry-rs/forge-std --no-commit && forge install OpenZeppelin/openzeppelin-contracts --no-commitCódigo completo del contrato inteligente de préstamos
pragma solidity ^0.8.13;
import "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "lib/openzeppelin-contracts/contracts/access/Ownable.sol";
contract LendingBorrowing is Ownable {
struct Loan {
uint256 amount;
uint256 collateral;
bool isActive;
}
IERC20 public immutable collateralToken;
IERC20 public immutable lendingToken;
uint256 public collateralFactor;
mapping(address => uint256) public collateralBalances;
mapping(address => Loan) public loans;
event CollateralDeposited(address indexed user, uint256 amount);
event CollateralWithdrawn(address indexed user, uint256 amount);
event LoanTaken(address indexed user, uint256 amount);
event LoanRepaid(address indexed user, uint256 amount);
constructor(IERC20 _collateralToken, IERC20 _lendingToken, uint256 _collateralFactor) Ownable(msg.sender) {
require(_collateralFactor <= 100, "Collateral factor must be <= 100");
collateralToken = _collateralToken;
lendingToken = _lendingToken;
collateralFactor = _collateralFactor;
}
function setCollateralFactor(uint256 _newFactor) external onlyOwner {
require(_newFactor <= 100, "Collateral factor must be <= 100");
collateralFactor = _newFactor;
}
function depositCollateral(uint256 _amount) external {
require(_amount > 0, "Amount must be greater than zero");
collateralBalances[msg.sender] += _amount;
collateralToken.transferFrom(msg.sender, address(this), _amount);
emit CollateralDeposited(msg.sender, _amount);
}
function withdrawCollateral(uint256 _amount) external {
require(_amount > 0, "Amount must be greater than zero");
require(collateralBalances[msg.sender] >= _amount, "Insufficient collateral");
uint256 maxWithdrawable = collateralBalances[msg.sender] - _loanRequiredCollateral(msg.sender);
require(_amount <= maxWithdrawable, "Cannot withdraw collateral locked for a loan");
collateralBalances[msg.sender] -= _amount;
collateralToken.transfer(msg.sender, _amount);
emit CollateralWithdrawn(msg.sender, _amount);
}
function takeLoan(uint256 _amount) external {
require(_amount > 0, "Amount must be greater than zero");
require(loans[msg.sender].isActive == false, "Existing loan must be repaid first");
uint256 maxLoan = (collateralBalances[msg.sender] * collateralFactor) / 100;
require(_amount <= maxLoan, "Loan exceeds collateral limit");
loans[msg.sender] = Loan({
amount: _amount,
collateral: collateralBalances[msg.sender],
isActive: true
});
lendingToken.transfer(msg.sender, _amount);
emit LoanTaken(msg.sender, _amount);
}
function repayLoan(uint256 _amount) external {
require(_amount > 0, "Amount must be greater than zero");
Loan storage userLoan = loans[msg.sender];
require(userLoan.isActive, "No active loan");
require(_amount <= userLoan.amount, "Repay amount exceeds loan");
lendingToken.transferFrom(msg.sender, address(this), _amount);
userLoan.amount -= _amount;
if (userLoan.amount == 0) {
userLoan.isActive = false;
}
emit LoanRepaid(msg.sender, _amount);
}
function _loanRequiredCollateral(address _user) internal view returns (uint256) {
Loan memory userLoan = loans[_user];
if (!userLoan.isActive) return 0;
return (userLoan.amount * 100) / collateralFactor;
}
function getLoanDetails(address _user) external view returns (uint256 amount, uint256 collateral, bool isActive) {
Loan memory userLoan = loans[_user];
return (userLoan.amount, userLoan.collateral, userLoan.isActive);
}
}
Descripción general del código
Struct Loan
struct Loan {
uint256 amount;
uint256 collateral;
bool isActive;
}
El struct Loan sirve como plantilla para almacenar los detalles de los préstamos individuales tomados por los usuarios. Cada campo dentro del struct representa una pieza clave de información sobre el estado y las condiciones del préstamo. A continuación se detalla cada campo:
Amount: Almacena la cantidad de tokens que el usuario ha tomado prestada. Este valor ayuda a rastrear cuánto debe el usuario, determina las obligaciones de reembolso y valida las condiciones del préstamo.
Collateral: Almacena el valor de la garantía depositada por el usuario para asegurar el préstamo. Protege los fondos del prestamista y ayuda a determinar el monto máximo que se puede tomar prestado en función del factor de garantía.
IsActive: Indica si el préstamo está actualmente activo. Este campo ayuda a rastrear el estado del préstamo y evita que los usuarios tomen un nuevo préstamo hasta que el existente esté completamente reembolsado.
Variables de estado
IERC20 public immutable collateralToken;
IERC20 public immutable lendingToken;
uint256 public collateralFactor;
El fragmento anterior define tres variables de estado críticas, cada una con un rol específico para habilitar y asegurar la funcionalidad de la plataforma de préstamos. A continuación se detalla cada variable:
CollateralToken: Representa el token que los usuarios depositan como garantía para sus préstamos. Por ejemplo, si se utiliza USDC como garantía, esta variable almacena la dirección blockchain del contrato de USDC.
LendingToken: Esta variable representa el token que la plataforma presta a los prestatarios. Si USDT es el token de préstamo, almacena la dirección blockchain del contrato de USDT.
CollateralFactor: Define el porcentaje del valor de la garantía que un usuario puede tomar prestado. Se expresa como un número entero, donde 100 representa el 100% del valor de la garantía. Por ejemplo, si el collateralFactor es 50, un usuario puede tomar prestado hasta el 50% del valor de su garantía. Esto garantiza que los préstamos estén sobrecolateralizados para mantener la seguridad del sistema de préstamos. Si un usuario deposita $1,000 en garantía y el collateralFactor es del 50%, el préstamo máximo que puede tomar es de $500.
Mappings
mapping(address => uint256) public collateralBalances;
mapping(address => Loan) public loans;
Mapping CollateralBalances: Rastrea la cantidad de garantía depositada por cada usuario. Cada usuario tiene un saldo de garantía único asociado a su dirección. Este mapping ayuda a validar si un usuario tiene suficiente garantía para tomar un préstamo o retirar fondos.
Mapping Loans: Rastrea los detalles de los préstamos activos de cada usuario vinculando su dirección a la información de su préstamo. Garantiza que un usuario no pueda tomar un nuevo préstamo mientras aún tenga uno activo.
Eventos
event CollateralDeposited(address indexed user, uint256 amount);
event CollateralWithdrawn(address indexed user, uint256 amount);
event LoanTaken(address indexed user, uint256 amount);
event LoanRepaid(address indexed user, uint256 amount);
Evento CollateralDeposited – Se emite cuando un usuario deposita garantía en el contrato. Registra la dirección del usuario y el monto depositado.
Evento CollateralWithdrawn – Se emite cuando un usuario retira garantía del contrato. Registra la dirección del usuario y el monto retirado.
Evento LoanTaken – Se emite cuando un usuario toma un préstamo. Registra la dirección del usuario y el monto prestado.
Evento LoanRepaid – Se emite cuando un usuario reembolsa un préstamo. Registra la dirección del usuario y el monto del reembolso.
Constructor
constructor(IERC20 _collateralToken, IERC20 _lendingToken, uint256 _collateralFactor) Ownable(msg.sender) {
require(_collateralFactor <= 100, "Collateral factor must be <= 100");
collateralToken = _collateralToken;
lendingToken = _lendingToken;
collateralFactor = _collateralFactor;
}
El constructor es una función especial en Solidity que se ejecuta solo una vez durante el despliegue del contrato. Su propósito es:
Establecer la configuración inicial del contrato.
Asignar valores a las variables de estado clave.
Establecer la propiedad y el control de acceso a través de Ownable.
El constructor recibe tres argumentos: _collateralToken, _lendingToken y _collateralFactor.
_collateralToken – El token utilizado como garantía para los préstamos.
_lendingToken – El token que los usuarios toman prestado del sistema.
_collateralFactor – El porcentaje de garantía que determina la capacidad máxima de endeudamiento, asegurando que no supere el 100%.
Al inicializarse, el constructor:
Asigna _collateralToken como el activo de garantía.
Establece _lendingToken como el token de préstamo.
Configura _collateralFactor para establecer la relación garantía-préstamo.
Además, Ownable(msg.sender) garantiza que el desplegador del contrato se convierta en el propietario, otorgándole privilegios administrativos, como modificar el collateralFactor cuando sea necesario.
Configuración del factor de garantía
function setCollateralFactor(uint256 _newFactor) external onlyOwner {
require(_newFactor <= 100, "Collateral factor must be <= 100");
collateralFactor = _newFactor;
}
Esta función es una característica administrativa fundamental que permite al propietario del contrato modificar el factor de garantía, que controla la relación entre la garantía y los fondos que se pueden tomar prestados. El factor de garantía se utiliza para determinar el monto máximo que un usuario puede tomar prestado contra su garantía. Al permitir actualizaciones de este factor, el contrato puede adaptarse a las condiciones cambiantes del mercado o a los perfiles de riesgo.
La función está marcada como external, lo que significa que solo puede ser llamada desde fuera del contrato. El modificador onlyOwner garantiza que solo el propietario del contrato (generalmente el desplegador) pueda llamar a esta función, proporcionando un control de acceso estricto.
La función garantiza que el nuevo factor de garantía sea un porcentaje válido entre 0 y 100 para evitar establecer valores poco realistas o inseguros. Por ejemplo, un factor superior a 100 podría permitir a los usuarios tomar prestado más de lo que cubre su garantía.
La función actualiza la variable de estado collateralFactor con el nuevo valor pasado como _newFactor.
Depósito de garantía
function depositCollateral(uint256 _amount) external {
require(_amount > 0, "Amount must be greater than zero");
collateralBalances[msg.sender] += _amount;
collateralToken.transferFrom(msg.sender, address(this), _amount);
emit CollateralDeposited(msg.sender, _amount);
}
Esta función es una característica fundamental que permite a los usuarios depositar garantías en el contrato. Es un paso obligatorio antes de que un usuario pueda solicitar un préstamo y garantiza que la garantía sea correctamente contabilizada y almacenada en el contrato.
La función valida que el usuario proporcione un monto válido mayor que cero y aumenta el saldo de garantía del usuario por el monto depositado. Utiliza transferFrom para transferir tokens del usuario al contrato, lo que requiere que el usuario apruebe que el contrato gaste sus tokens antes de llamar a esta función.
Esta función habilita lo siguiente:
Depósito de garantía: Los usuarios depositan una cantidad específica de un token predefinido (collateralToken) en el contrato. Esta garantía actúa como seguridad para cualquier préstamo que tomen.
Seguimiento de saldos de garantía: El monto depositado se añade al saldo de garantía del usuario, que se rastrea en el mapping collateralBalances.
Habilitación del préstamo: La garantía sirve como base para calcular cuánto puede tomar prestado el usuario.
Retiro de garantía
function withdrawCollateral(uint256 _amount) external {
require(_amount > 0, "Amount must be greater than zero");
require(collateralBalances[msg.sender] >= _amount, "Insufficient collateral");
uint256 maxWithdrawable = collateralBalances[msg.sender] - _loanRequiredCollateral(msg.sender);
require(_amount <= maxWithdrawable, "Cannot withdraw collateral locked for a loan");
collateralBalances[msg.sender] -= _amount;
collateralToken.transfer(msg.sender, _amount);
emit CollateralWithdrawn(msg.sender, _amount);
}
Esta función permite a los usuarios retirar una parte o la totalidad de su garantía depositada, asegurando que la garantía bloqueada como seguridad para un préstamo activo no pueda ser retirada.
La función valida que el monto de retiro sea mayor que cero y verifica que el usuario tenga suficiente garantía disponible para cubrir el retiro solicitado. Calcula la porción de garantía bloqueada para cualquier préstamo activo, disminuye el saldo de garantía del usuario por el monto de retiro y transfiere los tokens solicitados del contrato de vuelta al usuario.
En resumen, esta función habilita lo siguiente:
Retiro de garantía: Permite a los usuarios retirar su garantía disponible del contrato.
Protección del préstamo: Garantiza que los usuarios no puedan retirar la garantía necesaria para asegurar un préstamo activo.
Gestión del estado: Actualiza el saldo de garantía del usuario después de un retiro exitoso.
Tomar préstamo
function takeLoan(uint256 _amount) external {
require(_amount > 0, "Amount must be greater than zero");
require(loans[msg.sender].isActive == false, "Existing loan must be repaid first");
uint256 maxLoan = (collateralBalances[msg.sender] * collateralFactor) / 100;
require(_amount <= maxLoan, "Loan exceeds collateral limit");
loans[msg.sender] = Loan({
amount: _amount,
collateral: collateralBalances[msg.sender],
isActive: true
});
lendingToken.transfer(msg.sender, _amount);
emit LoanTaken(msg.sender, _amount);
}
Esta función permite a los usuarios tomar tokens prestados del contrato en función de su garantía depositada. El monto del préstamo está limitado por la garantía depositada del usuario y el factor de garantía del contrato, asegurando que el endeudamiento se mantenga dentro de límites seguros. Esta función juega un papel clave en la facilitación de préstamos mientras mantiene la seguridad del sistema.
La función garantiza lo siguiente:
El monto del préstamo es mayor que cero.
El usuario no tiene un préstamo activo, evitando múltiples préstamos simultáneos.
El préstamo solicitado no supera el límite máximo de endeudamiento basado en la garantía.
El mapping loans se actualiza con los detalles del préstamo del usuario.
Los tokens prestados se transfieren del contrato al usuario.
En resumen, esta función habilita lo siguiente:
Emisión de préstamos: Permite a los usuarios tomar tokens prestados respaldados por su garantía depositada.
Gestión de garantías: Garantiza que el monto del préstamo se mantenga dentro de la capacidad de endeudamiento del usuario.
Seguimiento de préstamos: Mantiene los registros de préstamos de los usuarios para evitar el sobreendeudamiento y garantizar la integridad del sistema.
Reembolso del préstamo
function repayLoan(uint256 _amount) external {
require(_amount > 0, "Amount must be greater than zero");
Loan storage userLoan = loans[msg.sender];
require(userLoan.isActive, "No active loan");
require(_amount <= userLoan.amount, "Repay amount exceeds loan");
lendingToken.transferFrom(msg.sender, address(this), _amount);
userLoan.amount -= _amount;
if (userLoan.amount == 0) {
userLoan.isActive = false;
}
emit LoanRepaid(msg.sender, _amount);
}
Esta función permite a los usuarios reembolsar sus préstamos activos, ya sea de forma parcial o total. Actualiza el estado del préstamo y garantiza que los reembolsos se procesen correctamente, ayudando a mantener la integridad del sistema.
La función realiza las siguientes verificaciones y acciones:
Garantiza que el monto del reembolso sea mayor que cero para evitar operaciones inválidas.
Recupera los detalles del préstamo del usuario y confirma que tiene un préstamo activo antes de continuar.
Garantiza que el monto del reembolso no supere el saldo restante del préstamo.
Transfiere el monto del reembolso del usuario al contrato, requiriendo aprobación previa por el monto especificado.
Reduce el saldo del préstamo por el monto del reembolso.
Si el préstamo está completamente reembolsado, la función lo marca como inactivo.
En resumen, esta función habilita lo siguiente:
Reembolso del préstamo: Permite a los prestatarios devolver el monto prestado al contrato.
Seguimiento del préstamo: Actualiza el saldo y el estado del préstamo después del reembolso.
Transparencia: Emite un evento para registrar las transacciones de reembolso on-chain.
Pruebas
Crea un archivo Token.sol en la carpeta src y añade el código siguiente al archivo.
pragma solidity ^0.8.0;
import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract Token is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol){}
function mint(address user, uint256 amount) public {
_mint(user, amount);
}
}
Crea un archivo lending.t.sol en la carpeta test y añade el código siguiente al archivo.
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import { LendingBorrowing} from "../src/Lending.sol";
import { Token } from "../src/Token.sol";
contract LendingTest is Test {
LendingBorrowing public lending;
Token public collateral;
Token public lendingToken;
address public user1 = makeAddr("user1");
address public user2 = makeAddr("user2");
address public user3 = makeAddr("user3");
function setUp() public {
collateral = new Token("collateralToken", "ct");
lendingToken = new Token("lendingToken", "lt");
lending = new LendingBorrowing(collateral, lendingToken, 80);
collateral.mint(user1, 100 ether);
lendingToken.mint(address(lending), 1000 ether);
}
function test_DepositCollateral() public {
vm.startPrank(user1);
collateral.approve(address(lending), 100 ether);
lending.depositCollateral(50 ether);
assert(collateral.balanceOf(user1) == 50 ether);
assert(collateral.balanceOf(address(lending)) == 50 ether);
}
function test_TakeLoan() public {
vm.startPrank(user1);
collateral.approve(address(lending), 100 ether);
lending.depositCollateral(50 ether);
vm.expectRevert();
lending.takeLoan(100 ether);
vm.expectRevert();
lending.takeLoan(49 ether);
lending.takeLoan(40 ether);
assert(lendingToken.balanceOf(user1) == 40 ether);
}
function test_RepayLoan() public {
vm.startPrank(user1);
collateral.approve(address(lending), 100 ether);
lending.depositCollateral(50 ether);
lending.takeLoan(40 ether);
lendingToken.approve(address(lending), 40 ether);
lending.repayLoan(40 ether);
assert(lendingToken.balanceOf(user1) == 0 ether);
}
function test_WithdrawCollateral() public {
vm.startPrank(user1);
collateral.approve(address(lending), 100 ether);
lending.depositCollateral(50 ether);
lending.takeLoan(40 ether);
lendingToken.approve(address(lending), 40 ether);
lending.repayLoan(40 ether);
lending.withdrawCollateral(50 ether);
assert(lendingToken.balanceOf(user1) == 0 ether);
assert(collateral.balanceOf(user1) == 100 ether);
}
}
Configuración de las pruebas
Esta función se llama antes de cada función de prueba y se utiliza para inicializar el estado.
Despliega los contratos de tokens: Se despliegan tanto los contratos de collateral como de lendingToken.
Despliega el contrato LendingBorrowing: El contrato LendingBorrowing se inicializa con el token de garantía, el lendingToken y una relación garantía-préstamo del 80% (lo que significa que un usuario solo puede tomar prestado hasta el 80% del valor de su garantía).
Acuña tokens:- Se acuñan 100 tokens de garantía para el Usuario 1.
Se acuñan 1000 tokens de préstamo para el contrato de préstamos.
Prueba de depósito de garantía
Esta función prueba que un usuario puede depositar garantía en el contrato de préstamos. El Usuario 1 aprueba que el contrato de préstamos gaste hasta 100 de sus tokens de garantía y deposita 50 tokens de garantía en el contrato. La función verifica que:
El saldo del Usuario 1 se reduce en 50 tokens.
El saldo de garantía del contrato de préstamos aumenta en 50 tokens.
Prueba de tomar préstamo
Esta función prueba que un usuario puede tomar un préstamo basado en la garantía que ha depositado. El Usuario 1 deposita 50 tokens de garantía y aprueba que el contrato de préstamos los gaste.
La prueba garantiza que tomar un préstamo superior al monto permitido o por debajo del mínimo requerido (basado en la garantía) revierta.
El Usuario 1 toma exitosamente un préstamo de 40 tokens lendingToken.
La prueba verifica que el Usuario 1 recibe 40 tokens lendingToken después de tomar el préstamo.
Prueba de reembolso del préstamo
Esta función prueba que un usuario puede reembolsar su préstamo correctamente.
El Usuario 1 deposita 50 tokens de garantía y toma un préstamo de 40 tokens lendingToken.
El Usuario 1 aprueba que el contrato de préstamos gaste 40 tokens lendingToken y reembolsa el préstamo.
La prueba verifica que el saldo de lendingToken del Usuario 1 es cero después del reembolso del préstamo.
Prueba de retiro de garantía
Esta función prueba que un usuario puede retirar su garantía después de reembolsar el préstamo.
El Usuario 1 deposita 50 tokens de garantía, toma un préstamo de 40 tokens lendingToken y reembolsa el préstamo.
Después de reembolsar el préstamo, el Usuario 1 retira su garantía.
La prueba verifica que:- El Usuario 1 recibe sus 50 tokens de garantía de vuelta.
El saldo del Usuario 1 es 100 (ya que inicialmente acuñó 100 tokens).
El saldo de lendingToken del Usuario 1 es cero después del reembolso del préstamo.
Ejecutar pruebas
forge test
Despliegue y verificación en Linea
Añade las variables necesarias al archivo .env, que debe estar en el nivel raíz.
LINEA_RPC_URL=https://linea-mainnet.infura.io/v3/
PRIVATE_KEY=*************************************************
Añade los detalles siguientes a foundry.toml
[etherscan]
linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
Crea un Makefile y añade los detalles siguientes
deploy:
forge create src/Lending.sol: LendingBorrowing --rpc-url $(LINEA_RPC_URL) --private-key $(PRIVATE_KEY)
verify:; forge verify-contract --rpc-url $(LINEA_RPC_URL) --chain linea <contract address> src/Lending.sol:LendingBorrowing --constructor-args arg1 arg2
Despliegue
make deploy
Verificación
make verify
Conclusión
Construir un contrato inteligente sencillo de préstamos sienta las bases para comprender DeFi y el desarrollo blockchain. Esta guía cubrió:
Depósito de garantía – Los usuarios bloquean activos como seguridad para los préstamos.
Tomar préstamos – Los prestatarios acceden a fondos basados en su garantía.
Reembolso de préstamos – Los usuarios devuelven los fondos prestados para saldar su deuda.
Retiro de garantía – Los usuarios recuperan la garantía después del reembolso total.
Al integrar estos elementos, hemos creado un sistema de préstamos funcional y automatizado. A medida que DeFi crece, estos conceptos sirven como bloques de construcción para aplicaciones financieras más avanzadas.
Para más tutoriales prácticos y análisis en profundidad sobre el desarrollo blockchain, visita el canal de YouTube de MetaMask Developer y mantente al día con nuestras últimas novedades siguiéndonos en Twitter.