Finanças descentralizadas (DeFi) transformaram a forma como as pessoas emprestam e tomam dinheiro emprestado, eliminando intermediários e usando smart contracts para processar transações automaticamente. O que antes exigia bancos e burocracia agora pode acontecer em poucos cliques, tudo on-chain.
A Linea ajuda desenvolvedores a criar aplicações DeFi com maior escalabilidade e custos de gas mais baixos em comparação com a mainnet do Ethereum, utilizando zero-knowledge rollups (zk-rollups) para agrupar e comprimir transações.
Neste guia, vamos percorrer o processo de criação e implantação de um smart contract simples de empréstimo e tomada de crédito na Linea. Você aprenderá como gerenciar colateral, emitir empréstimos e configurar regras de pagamento — todos os recursos essenciais de um sistema de empréstimos DeFi.
Ao final, você terá um smart contract funcional e uma compreensão sólida de como os empréstimos DeFi funcionam por baixo dos panos. Seja você novo no desenvolvimento blockchain ou querendo aprimorar suas habilidades, este guia passo a passo fornecerá as ferramentas para começar a construir na Linea.
Pré-requisito
Você deve ter um entendimento básico de Solidity.
Você deve ter o Nodejs e o Foundry instalados no seu computador.
Visão geral do contrato
Este contrato permite:
Depósito de Colateral: Usuários podem depositar colateral no contrato, com os detalhes armazenados com segurança no smart contract.
Tomada de Empréstimo: Usuários podem tomar um empréstimo com base no colateral depositado.
Pagamento: Usuários podem pagar seus empréstimos para liberar seu colateral.
Retirada de Colateral: Usuários podem retirar colateral que não esteja bloqueado por um empréstimo ativo.
Configuração do projeto
Usaremos o framework Foundry para este projeto. Com o Foundry instalado, execute o comando abaixo para iniciar um novo projeto:
forge init lending
Abra a pasta do projeto no Vscode ou no seu editor de código favorito e exclua os arquivos scripts/counter.s.sol, src/counter.sol e test/counter.t.sol.
Instalar todas as dependências
forge install foundry-rs/forge-std --no-commit && forge install OpenZeppelin/openzeppelin-contracts --no-commitCódigo completo do smart contract de empréstimo e tomada de crédito
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);
}
}
Visão geral do código
Struct Loan
struct Loan {
uint256 amount;
uint256 collateral;
bool isActive;
}
A struct Loan serve como um modelo para armazenar os detalhes dos empréstimos individuais tomados pelos usuários. Cada campo dentro da struct representa uma informação-chave sobre o status e as condições do empréstimo. Veja abaixo um detalhamento dos seus campos:
Amount: Armazena a quantidade de tokens que o usuário tomou emprestado. Esse valor ajuda a rastrear o quanto o usuário deve, determina as obrigações de pagamento e valida as condições do empréstimo.
Collateral: Armazena o valor do colateral depositado pelo usuário para garantir o empréstimo. Protege os fundos do credor e ajuda a determinar o valor máximo que pode ser tomado emprestado com base no fator de colateral.
IsActive: Indica se o empréstimo está atualmente ativo. Este campo ajuda a rastrear o status do empréstimo e impede que os usuários tomem um novo empréstimo até que o existente seja totalmente pago.
Variáveis de estado
IERC20 public immutable collateralToken;
IERC20 public immutable lendingToken;
uint256 public collateralFactor;
O trecho acima define três variáveis de estado críticas, cada uma desempenhando um papel específico para habilitar e proteger a funcionalidade da plataforma de empréstimos. Veja abaixo um detalhamento dessas variáveis:
CollateralToken: Representa o token que os usuários depositam como colateral para seus empréstimos. Por exemplo, se o USDC for usado como colateral, esta variável armazena o endereço blockchain do contrato USDC.
LendingToken: Esta variável representa o token que a plataforma empresta aos tomadores. Se o USDT for o token de empréstimo, ela armazena o endereço blockchain do contrato USDT.
CollateralFactor: Define a porcentagem do valor do colateral que um usuário pode tomar emprestado. É expresso como um número inteiro, onde 100 representa 100% do valor do colateral. Por exemplo, se o collateralFactor for 50, um usuário pode tomar emprestado até 50% do valor do seu colateral. Isso garante que os empréstimos sejam super-colateralizados para manter a segurança do sistema de empréstimos. Se um usuário depositar $1.000 em colateral e o collateralFactor for 50%, o empréstimo máximo que ele pode tomar é de $500.
Mappings
mapping(address => uint256) public collateralBalances;
mapping(address => Loan) public loans;
Mapping CollateralBalances: Rastreia a quantidade de colateral depositada por cada usuário. Cada usuário tem um saldo de colateral único associado ao seu endereço. Este mapping ajuda a validar se um usuário tem colateral suficiente para tomar um empréstimo ou retirar fundos.
Mapping Loans: Rastreia os detalhes de empréstimos ativos para cada usuário, vinculando seu endereço às informações do empréstimo. Garante que um usuário não possa tomar um novo empréstimo enquanto ainda tiver um ativo.
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 – Emitido quando um usuário deposita colateral no contrato. Registra o endereço do usuário e o valor depositado.
Evento CollateralWithdrawn – Emitido quando um usuário retira colateral do contrato. Registra o endereço do usuário e o valor retirado.
Evento LoanTaken – Emitido quando um usuário toma um empréstimo. Registra o endereço do usuário e o valor tomado emprestado.
Evento LoanRepaid – Emitido quando um usuário paga um empréstimo. Registra o endereço do usuário e o valor do pagamento.
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;
}
O constructor é uma função especial em Solidity que é executada apenas uma vez durante a implantação do contrato. Seu propósito é:
Configurar a configuração inicial do contrato.
Atribuir valores às variáveis de estado principais.
Estabelecer propriedade e controle de acesso por meio do Ownable.
O constructor recebe três argumentos: _collateralToken, _lendingToken e _collateralFactor.
_collateralToken – O token usado como colateral para empréstimos.
_lendingToken – O token que os usuários tomam emprestado do sistema.
_collateralFactor – A porcentagem do colateral que determina a capacidade máxima de tomada de empréstimo, garantindo que não ultrapasse 100%.
Após a inicialização, o constructor:
Atribui _collateralToken como o ativo de colateral.
Define _lendingToken como o token de empréstimo.
Configura _collateralFactor para estabelecer a proporção colateral-empréstimo.
Além disso, Ownable(msg.sender) garante que o implantador do contrato se torne o proprietário, concedendo-lhe privilégios administrativos, como modificar o collateralFactor quando necessário.
Configurando o fator de colateral
function setCollateralFactor(uint256 _newFactor) external onlyOwner {
require(_newFactor <= 100, "Collateral factor must be <= 100");
collateralFactor = _newFactor;
}
Esta função é um recurso administrativo essencial que permite ao proprietário do contrato modificar o fator de colateral, que controla a proporção entre colateral e fundos disponíveis para empréstimo. O fator de colateral é usado para determinar o valor máximo que um usuário pode tomar emprestado com base no seu colateral. Ao permitir atualizações nesse fator, o contrato pode se adaptar a mudanças nas condições de mercado ou perfis de risco.
A função é marcada como external, o que significa que só pode ser chamada de fora do contrato. O modificador onlyOwner garante que apenas o proprietário do contrato (geralmente o implantador) possa chamar esta função, fornecendo controle de acesso rigoroso.
A função garante que o novo fator de colateral seja uma porcentagem válida entre 0 e 100 para evitar a definição de valores irreais ou inseguros. Por exemplo, um fator acima de 100 poderia permitir que os usuários tomassem emprestado mais do que seu colateral cobre.
A função atualiza a variável de estado collateralFactor com o novo valor passado como _newFactor.
Depositar colateral
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 função é um recurso crítico que permite aos usuários depositar colateral no contrato. É uma etapa obrigatória antes que um usuário possa tomar um empréstimo e garante que o colateral seja devidamente contabilizado e armazenado no contrato.
A função valida que o usuário fornece um valor válido maior que zero e aumenta o saldo de colateral do usuário pelo valor depositado. Ela usa transferFrom para transferir tokens do usuário para o contrato, o que exige que o usuário aprove o contrato para gastar seus tokens antes de chamar esta função.
Esta função permite o seguinte:
Depósito de colateral: Os usuários depositam uma quantidade especificada de um token predefinido (collateralToken) no contrato. Este colateral serve como garantia para quaisquer empréstimos que eles tomem.
Rastreamento de saldos de colateral: O valor depositado é adicionado ao saldo de colateral do usuário, que é rastreado no mapping collateralBalances.
Habilitação de tomada de empréstimo: O colateral serve como base para calcular quanto o usuário pode tomar emprestado.
Retirar colateral
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 função permite que os usuários retirem uma parte ou todo o seu colateral depositado, garantindo que o colateral bloqueado como garantia para um empréstimo ativo não possa ser retirado.
A função valida que o valor de retirada é maior que zero e verifica se o usuário tem colateral disponível suficiente para cobrir a retirada solicitada. Ela calcula a parte do colateral bloqueada para qualquer empréstimo ativo, diminui o saldo de colateral do usuário pelo valor de retirada e transfere os tokens solicitados do contrato de volta para o usuário.
Em resumo, esta função permite o seguinte:
Retirada de colateral: Permite que os usuários retirem seu colateral disponível do contrato.
Proteção do empréstimo: Garante que os usuários não possam retirar o colateral necessário para garantir um empréstimo ativo.
Gerenciamento de estado: Atualiza o saldo de colateral do usuário após uma retirada bem-sucedida.
Tomar empréstimo
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 função permite que os usuários tomem tokens emprestados do contrato com base no colateral depositado. O valor do empréstimo é limitado pelo colateral depositado pelo usuário e pelo fator de colateral do contrato, garantindo que a tomada de empréstimo permaneça dentro de limites seguros. Esta função desempenha um papel fundamental na facilitação de empréstimos enquanto mantém a segurança do sistema.
A função garante o seguinte:
O valor do empréstimo é maior que zero.
O usuário não tem um empréstimo ativo, evitando múltiplos empréstimos simultâneos.
O empréstimo solicitado não excede o limite máximo de tomada de empréstimo com base no colateral.
O mapping loans é atualizado com os detalhes do empréstimo do usuário.
Os tokens tomados emprestados são transferidos do contrato para o usuário.
Em resumo, esta função permite o seguinte:
Emissão de empréstimo: Permite que os usuários tomem tokens emprestados com base no colateral depositado.
Gerenciamento de colateral: Garante que o valor do empréstimo permaneça dentro da capacidade de tomada de empréstimo do usuário.
Rastreamento de empréstimos: Mantém registros de empréstimos dos usuários para evitar super-endividamento e garantir a integridade do sistema.
Pagar empréstimo
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 função permite que os usuários paguem seus empréstimos ativos, parcial ou totalmente. Ela atualiza o status do empréstimo e garante que os pagamentos sejam processados corretamente, ajudando a manter a integridade do sistema.
A função realiza as seguintes verificações e ações:
Garante que o valor do pagamento seja maior que zero para evitar operações inválidas.
Recupera os detalhes do empréstimo do usuário e confirma que ele tem um empréstimo ativo antes de prosseguir.
Garante que o valor do pagamento não exceda o saldo restante do empréstimo.
Transfere o valor do pagamento do usuário para o contrato, exigindo aprovação prévia para o valor especificado.
Reduz o saldo do empréstimo pelo valor do pagamento.
Se o empréstimo for totalmente pago, a função o marca como inativo.
Em resumo, esta função permite o seguinte:
Pagamento do Empréstimo: Permite que os tomadores devolvam o valor tomado emprestado ao contrato.
Rastreamento do Empréstimo: Atualiza o saldo e o status do empréstimo após o pagamento.
Transparência: Emite um evento para registrar as transações de pagamento on-chain.
Testes
Crie um arquivo Token.sol na pasta src e adicione o código abaixo ao arquivo.
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);
}
}
Crie um arquivo lending.t.sol na pasta test e adicione o código abaixo ao arquivo.
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);
}
}
Configuração dos testes
Esta função é chamada antes de cada função de teste e é usada para inicializar o estado.
Implanta os contratos de token: Tanto os contratos de colateral quanto de lendingToken são implantados.
Implanta o contrato LendingBorrowing: O contrato LendingBorrowing é inicializado com o token de colateral, lendingToken e uma proporção colateral-empréstimo de 80% (o que significa que um usuário só pode tomar emprestado até 80% do valor do seu colateral).
Cunha tokens:- 100 tokens de colateral são cunhados para o Usuário 1.
1000 tokens de empréstimo são cunhados para o contrato de empréstimo.
Teste de depósito de colateral
Esta função testa que um usuário pode depositar colateral no contrato de empréstimo. O Usuário 1 aprova o contrato de empréstimo para gastar até 100 de seus tokens de colateral e deposita 50 tokens de colateral no contrato. A função verifica que:
O saldo do Usuário 1 é reduzido em 50 tokens.
O saldo de colateral do contrato de empréstimo aumenta em 50 tokens.
Teste de tomada de empréstimo
Esta função testa que um usuário pode tomar um empréstimo com base no colateral que depositou. O Usuário 1 deposita 50 tokens de colateral e aprova o contrato de empréstimo para gastá-los.
O teste garante que tomar um empréstimo maior que o valor permitido ou abaixo do mínimo exigido (com base no colateral) reverta.
O Usuário 1 toma com sucesso um empréstimo de 40 tokens lendingToken.
O teste verifica que o Usuário 1 recebe 40 tokens lendingToken após tomar o empréstimo.
Teste de pagamento de empréstimo
Esta função testa que um usuário pode pagar seu empréstimo corretamente.
O Usuário 1 deposita 50 tokens de colateral e toma um empréstimo de 40 tokens lendingToken.
O Usuário 1 aprova o contrato de empréstimo para gastar 40 tokens lendingToken e paga o empréstimo.
O teste verifica que o saldo de lendingToken do Usuário 1 é zero após o pagamento do empréstimo.
Teste de retirada de colateral
Esta função testa que um usuário pode retirar seu colateral após pagar o empréstimo.
O Usuário 1 deposita 50 tokens de colateral, toma um empréstimo de 40 tokens lendingToken e paga o empréstimo.
Após o pagamento do empréstimo, o Usuário 1 retira seu colateral.
O teste verifica que:- O Usuário 1 recebe de volta seus 50 tokens de colateral.
O saldo do Usuário 1 é 100 (já que ele inicialmente cunhou 100 tokens).
O saldo de lendingToken do Usuário 1 é zero após o pagamento do empréstimo.
Executar testes
forge test
Implantação e verificação na Linea
Adicione as variáveis necessárias ao .env; o arquivo .env deve estar no nível raiz.
LINEA_RPC_URL=https://linea-mainnet.infura.io/v3/
PRIVATE_KEY=*************************************************
Adicione os detalhes abaixo ao foundry.toml
[etherscan]
linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
Crie um Makefile e adicione os detalhes abaixo
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
Implantação
make deploy
Verificação
make verify
Conclusão
Construir um smart contract simples de empréstimo e tomada de crédito fornece uma base para entender DeFi e desenvolvimento blockchain. Este guia abordou:
Depositar colateral – Usuários bloqueiam ativos como garantia para empréstimos.
Tomar empréstimos – Tomadores acessam fundos com base no seu colateral.
Pagar empréstimos – Usuários devolvem os fundos tomados emprestados para quitar sua dívida.
Retirar colateral – Usuários recuperam o colateral após o pagamento total.
Ao integrar esses elementos, criamos um sistema de empréstimos funcional e automatizado. À medida que o DeFi cresce, esses conceitos servem como blocos de construção para aplicações financeiras mais avançadas.
Para mais tutoriais práticos e análises aprofundadas sobre desenvolvimento blockchain, confira o canal do YouTube do MetaMask Developer e fique atualizado com nossas últimas novidades nos seguindo no Twitter.