练习2:自动化房地产借贷

Day 1 回顾

回顾一下,截至目前,我们已经创建了ERC-1155智能合约,并通过Chainlink Functions将其与Zillow的API连接起来,从而创建了一个细分权益化的房地产代币。在接下来的两个练习中,我们将针对这个细分权益化的现实世界资产构建应用场景:

  1. RWA的借出 & 借入

  2. 在英式拍卖会上出售细分权益化的RWA

用例1) RWA的借出 & 借入

在这个用例中,我们将创建RwaLending.sol,在这里Alice将会锁定一定数量的代表她房产的ERC-1155通证,以此换取USDC的贷款。她还可以借出房产某一比例的价值。因为如果大家还记得的话,如果我们发行给她20个RealEstateToken通证,而她只借出了其中的5个,那就意味着她借出了房产价值的25%。

为了计算房产的价格,我们会依赖getPriceDetails函数。如果你们还有印象,昨天我们已经设置了Chainlink Automation定期更新房产价格详情——以美元计价。然后,我们将直接采用以下公式在链上计算房产通证的估值:

Valuation=W1×ListPrice+W2×OriginalListPrice+W3×TaxAssessedValueW1+W2+W3Valuation = \frac{W_1 \times ListPrice + W_2 \times OriginalListPrice + W_3 \times TaxAssessedValue}{W_1 + W_2 + W_3}

这里的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接口。最重要的检查发生在该接口中的onERC1155ReceivedonERC1155ReceivedBatch函数里。具体来说,我们在这里确保msg.sender是我们自己的RealEstateToken.sol

初始和清算阈值

为了简化代码,在这个例子中的初始和清算阈值被硬编码为60%和75%。这意味着如果Alice的房产价值100美元,她将获得60 USDC作为贷款。如果在某个时刻,她的房产价值下降到低于75美元,任何人都可以清算其头寸,而她将失去所有的ERC-1155通证。但她会保留USDC。

滑点保护

调用borrow函数时,Alice还需要提供minLoanAmountmaxLiquidationThreshold的值,以保护自己免受滑点的影响。由于loanAmountliquidationThreshold是在链上动态计算的,Alice需要设定自己可接受的边界值,否则交易将被回滚。

1美元不等于1 USDC

通常情况下,1美元并不等同于1 USDC。其价值可能在0.99美元到1.01美元之间波动,因此将美元值硬编码为1 USDC是极其危险的。我们必须使用USDC/USD的Chainlink Price Feed。

USDC/USD的喂价有8位小数,而USDC ERC-20通证本身有6位小数。例如,喂价值可能返回99993242,这意味着1 USDC等于0.99993242美元。现在,我们需要将这100美元转换为我们实际要发送或接收的USDC通证的确切数量。为此,我们使用getValuationInUsdc函数。

每产生一个新区块就推送价格更新到区块链上是不切实际的。因此,有两种触发条件(可以在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