练习2:自动化房地产借贷
Day 1 回顾
回顾一下,截至目前,我们已经创建了ERC-1155智能合约,并通过Chainlink Functions将其与Zillow的API连接起来,从而创建了一个细分权益化的房地产代币。在接下来的两个练习中,我们将针对这个细分权益化的现实世界资产构建应用场景:
RWA的借出 & 借入
在英式拍卖会上出售细分权益化的RWA

用例1) RWA的借出 & 借入
在这个用例中,我们将创建RwaLending.sol
,在这里Alice将会锁定一定数量的代表她房产的ERC-1155通证,以此换取USDC的贷款。她还可以借出房产某一比例的价值。因为如果大家还记得的话,如果我们发行给她20个RealEstateToken通证
,而她只借出了其中的5个,那就意味着她借出了房产价值的25%。
为了计算房产的价格,我们会依赖getPriceDetails
函数。如果你们还有印象,昨天我们已经设置了Chainlink Automation定期更新房产价格详情——以美元计价。然后,我们将直接采用以下公式在链上计算房产通证的估值:
这里的W
代表权重。
一旦Alice用USDC还清了她的债务,她就会拿回自己的ERC-1155通证。如果房产的价值低于清算阈值,任何人都可以对这个头寸进行清算,这样一来,Alice将会失去所有的ERC-1155通证。
创建RWALending.sol
在./use-cases
文件夹中创建RWALending.sol
文件。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {RealEstateToken} from "../RealEstateToken.sol";
import {IERC1155Receiver, IERC165} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
/**
* THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
* DO NOT USE THIS CODE IN PRODUCTION.
*/
contract RwaLending is IERC1155Receiver, OwnerIsCreator, ReentrancyGuard {
using SafeERC20 for IERC20;
struct LoanDetails {
uint256 erc1155AmountSupplied;
uint256 usdcAmountLoaned;
uint256 usdcLiquidationThreshold;
}
RealEstateToken internal immutable i_realEstateToken;
address internal immutable i_usdc;
AggregatorV3Interface internal s_usdcUsdAggregator;
uint32 internal s_usdcUsdFeedHeartbeat;
uint256 internal immutable i_weightListPrice;
uint256 internal immutable i_weightOriginalListPrice;
uint256 internal immutable i_weightTaxAssessedValue;
uint256 internal immutable i_ltvInitialThreshold;
uint256 internal immutable i_ltvLiquidationThreshold;
mapping(uint256 tokenId => mapping(address borrower => LoanDetails)) internal s_activeLoans;
event Borrow(
uint256 indexed tokenId, uint256 amount, uint256 indexed loanAmount, uint256 indexed liquidationThreshold
);
event BorrowRepayed(uint256 indexed tokenId, uint256 indexed amount);
event Liquidated(uint256 indexed tokenId);
error AlreadyBorrowed(address borrower, uint256 tokenId);
error OnlyRealEstateTokenSupported();
error InvalidValuation();
error SlippageToleranceExceeded();
error PriceFeedDdosed();
error InvalidRoundId();
error StalePriceFeed();
error NothingToRepay();
constructor(
address realEstateTokenAddress,
address usdc,
address usdcUsdAggregatorAddress,
uint32 usdcUsdFeedHeartbeat
) {
i_realEstateToken = RealEstateToken(realEstateTokenAddress);
i_usdc = usdc;
s_usdcUsdAggregator = AggregatorV3Interface(usdcUsdAggregatorAddress);
s_usdcUsdFeedHeartbeat = usdcUsdFeedHeartbeat;
i_weightListPrice = 50;
i_weightOriginalListPrice = 30;
i_weightTaxAssessedValue = 20;
i_ltvInitialThreshold = 60;
i_ltvLiquidationThreshold = 75;
}
function borrow(
uint256 tokenId,
uint256 amount,
bytes memory data,
uint256 minLoanAmount,
uint256 maxLiquidationThreshold
) external nonReentrant {
if (s_activeLoans[tokenId][msg.sender].usdcAmountLoaned != 0) revert AlreadyBorrowed(msg.sender, tokenId);
uint256 normalizedValuation = getValuationInUsdc(tokenId) * amount / i_realEstateToken.totalSupply(tokenId);
if (normalizedValuation == 0) revert InvalidValuation();
uint256 loanAmount = (normalizedValuation * i_ltvInitialThreshold) / 100;
if (loanAmount < minLoanAmount) revert SlippageToleranceExceeded();
uint256 liquidationThreshold = (normalizedValuation * i_ltvLiquidationThreshold) / 100;
if (liquidationThreshold > maxLiquidationThreshold) {
revert SlippageToleranceExceeded();
}
i_realEstateToken.safeTransferFrom(msg.sender, address(this), tokenId, amount, data);
s_activeLoans[tokenId][msg.sender] = LoanDetails({
erc1155AmountSupplied: amount,
usdcAmountLoaned: loanAmount,
usdcLiquidationThreshold: liquidationThreshold
});
IERC20(i_usdc).safeTransfer(msg.sender, loanAmount);
emit Borrow(tokenId, amount, loanAmount, liquidationThreshold);
}
function repay(uint256 tokenId) external nonReentrant {
LoanDetails memory loanDetails = s_activeLoans[tokenId][msg.sender];
if (loanDetails.usdcAmountLoaned == 0) revert NothingToRepay();
delete s_activeLoans[tokenId][msg.sender];
IERC20(i_usdc).safeTransferFrom(msg.sender, address(this), loanDetails.usdcAmountLoaned);
i_realEstateToken.safeTransferFrom(address(this), msg.sender, tokenId, loanDetails.erc1155AmountSupplied, "");
emit BorrowRepayed(tokenId, loanDetails.erc1155AmountSupplied);
}
function liquidate(uint256 tokenId, address borrower) external {
LoanDetails memory loanDetails = s_activeLoans[tokenId][borrower];
uint256 normalizedValuation =
getValuationInUsdc(tokenId) * loanDetails.erc1155AmountSupplied / i_realEstateToken.totalSupply(tokenId);
if (normalizedValuation == 0) revert InvalidValuation();
uint256 liquidationThreshold = (normalizedValuation * i_ltvLiquidationThreshold) / 100;
if (liquidationThreshold < loanDetails.usdcLiquidationThreshold) {
delete s_activeLoans[tokenId][borrower];
}
}
function getUsdcPriceInUsd() public view returns (uint256) {
uint80 _roundId;
int256 _price;
uint256 _updatedAt;
try s_usdcUsdAggregator.latestRoundData() returns (
uint80 roundId,
int256 price,
uint256,
/* startedAt */
uint256 updatedAt,
uint80 /* answeredInRound */
) {
_roundId = roundId;
_price = price;
_updatedAt = updatedAt;
} catch {
revert PriceFeedDdosed();
}
if (_roundId == 0) revert InvalidRoundId();
if (_updatedAt < block.timestamp - s_usdcUsdFeedHeartbeat) {
revert StalePriceFeed();
}
return uint256(_price);
}
function getValuationInUsdc(uint256 tokenId) public view returns (uint256) {
RealEstateToken.PriceDetails memory priceDetails = i_realEstateToken.getPriceDetails(tokenId);
uint256 valuation = (
i_weightListPrice * priceDetails.listPrice + i_weightOriginalListPrice * priceDetails.originalListPrice
+ i_weightTaxAssessedValue * priceDetails.taxAssessedValue
) / (i_weightListPrice + i_weightOriginalListPrice + i_weightTaxAssessedValue);
uint256 usdcPriceInUsd = getUsdcPriceInUsd();
uint256 feedDecimals = s_usdcUsdAggregator.decimals();
uint256 usdcDecimals = 6; // USDC uses 6 decimals
uint256 normalizedValuation = Math.mulDiv((valuation * usdcPriceInUsd), 10 ** usdcDecimals, 10 ** feedDecimals); // Adjust the valuation from USD (Chainlink 1e8) to USDC (1e6)
return normalizedValuation;
}
function setUsdcUsdPriceFeedDetails(address usdcUsdAggregatorAddress, uint32 usdcUsdFeedHeartbeat)
external
onlyOwner
{
s_usdcUsdAggregator = AggregatorV3Interface(usdcUsdAggregatorAddress);
s_usdcUsdFeedHeartbeat = usdcUsdFeedHeartbeat;
}
function onERC1155Received(
address, /*operator*/
address, /*from*/
uint256, /*id*/
uint256, /*value*/
bytes calldata /*data*/
) external view returns (bytes4) {
if (msg.sender != address(i_realEstateToken)) {
revert OnlyRealEstateTokenSupported();
}
return IERC1155Receiver.onERC1155Received.selector;
}
function onERC1155BatchReceived(
address, /*operator*/
address, /*from*/
uint256[] calldata, /*ids*/
uint256[] calldata, /*values*/
bytes calldata /*data*/
) external view returns (bytes4) {
if (msg.sender != address(i_realEstateToken)) {
revert OnlyRealEstateTokenSupported();
}
return IERC1155Receiver.onERC1155BatchReceived.selector;
}
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165) returns (bool) {
return interfaceId == type(IERC1155Receiver).interfaceId || interfaceId == type(IERC165).interfaceId;
}
}
让我们来分析代码的关键部分
让我们来分析该合约的一些关键部分。
IERC1155Receiver
为了让该合约能够接收ERC-1155通证,它必须实现IERC1155Receiver接口。最重要的检查发生在该接口中的onERC1155Received
和onERC1155ReceivedBatch
函数里。具体来说,我们在这里确保msg.sender是我们自己的RealEstateToken.sol
。
初始和清算阈值
为了简化代码,在这个例子中的初始和清算阈值被硬编码为60%和75%。这意味着如果Alice的房产价值100美元,她将获得60 USDC作为贷款。如果在某个时刻,她的房产价值下降到低于75美元,任何人都可以清算其头寸,而她将失去所有的ERC-1155通证。但她会保留USDC。
滑点保护
调用borrow
函数时,Alice还需要提供minLoanAmount
和maxLiquidationThreshold
的值,以保护自己免受滑点的影响。由于loanAmount
和liquidationThreshold
是在链上动态计算的,Alice需要设定自己可接受的边界值,否则交易将被回滚。
1美元不等于1 USDC
通常情况下,1美元并不等同于1 USDC。其价值可能在0.99美元到1.01美元之间波动,因此将美元值硬编码为1 USDC是极其危险的。我们必须使用USDC/USD的Chainlink Price Feed。
使用Chainlink Data Feeds - 精度位数
USDC/USD的喂价有8位小数,而USDC ERC-20通证本身有6位小数。例如,喂价值可能返回99993242,这意味着1 USDC等于0.99993242美元。现在,我们需要将这100美元转换为我们实际要发送或接收的USDC通证的确切数量。为此,我们使用getValuationInUsdc
函数。
使用Chainlink Data Feeds - 价格更新频率
每产生一个新区块就推送价格更新到区块链上是不切实际的。因此,有两种触发条件(可以在data.chain.link的"Trigger parameters" 处看到):价格偏差阈值和心跳周期。这些参数因不同的数据源而异,开发者们需要对此保持关注。基本上,DONs持续地计算新的价格并将其与最新价格进行比较。如果新价格低于或高于偏差阈值,一个新的报告会被推送到链上。此外,假设心跳周期设为1小时,如果前一小时内没有新的报告生成,不管怎样都会推动一个新报告到链上。 这对开发者而言为什么很重要?使用过期价格来进行链上行为是有风险的。因此,开发者必须:
应该使用
latestRoundData
函数而不是latestAnswer
函数。将
updatedAt
变量与他们所选的usdcUsdFeedHeartbeat
进行对比。
作为一种合理性检查,我们同样希望验证返回的roundId大于零。
部署RWALending.sol到Avalanche Fuji测试网
部署RWALending.sol
到Avalanche Fuji测试网,我们将需要向构造函数提供以下信息:
realEstateTokenAddress: 之前我们部署的
RealEstateToken.sol
地址usdc:
0x5425890298aed601595a70AB815c96711a31Bc65
usdcUsdAggregatorAddress:
0x97FE42a7E96640D932bbc0e1580c73E705A8EB73
usdcUsdFeedHeartbeat:
86400
Last updated