React Native DApp 開發(fā)全棧實(shí)戰(zhàn)·從 0 到 1 系列(收益聚合器-合約部分)

前言

本文基于 OpenZeppelin v5 + Solidity 0.8.20+,用 200 行代碼帶你從零搭建「可編譯、可部署、可擴(kuò)容」的收益聚合器 MVP。流程只有四步:

  1. 雙 ERC20:一份生息資產(chǎn),一份收益憑證;
  2. Chainlink 喂價(jià):鏈上美元計(jì)價(jià),一秒搞定;
  3. 份額制資金池:按份額自動(dòng)分收益,無需記賬;
  4. Hardhat 全套:防重入、Owner 救援、單元測試、部署腳本,一鍵 compile / test / deploy 打通閉環(huán)。

智能合約

  • 代幣合約

// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.22;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import "hardhat/console.sol";
contract MyToken1 is ERC20, ERC20Burnable, Ownable {
    constructor(string memory name_,string memory symbol_,address initialOwner)
        ERC20(name_, symbol_)
        Ownable(initialOwner)
    {
        _mint(msg.sender, 1000 * 10 ** decimals());
    }

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}

  • 喂價(jià)合約

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";

contract MockV3Aggregator is AggregatorV3Interface {
    uint256 public constant versionvar = 4;

    uint8 public decimalsvar;
    int256 public latestAnswer;
    uint256 public latestTimestamp;
    uint256 public latestRound;
    mapping(uint256 => int256) public getAnswer;
    mapping(uint256 => uint256) public getTimestamp;
    mapping(uint256 => uint256) private getStartedAt;
    string private descriptionvar;

    constructor(
        uint8 _decimals,
        string memory _description,
        int256 _initialAnswer
    ) {
        decimalsvar = _decimals;
        descriptionvar = _description;
        updateAnswer(_initialAnswer);
    }

    function updateAnswer(int256 _answer) public {
        latestAnswer = _answer;
        latestTimestamp = block.timestamp;
        latestRound++;
        getAnswer[latestRound] = _answer;
        getTimestamp[latestRound] = block.timestamp;
        getStartedAt[latestRound] = block.timestamp;
    }

    function getRoundData(uint80 _roundId)
        external
        view
        override
        returns (
            uint80 roundId,
            int256 answer,
            uint256 startedAt,
            uint256 updatedAt,
            uint80 answeredInRound
        )
    {
        return (
            _roundId,
            getAnswer[_roundId],
            getStartedAt[_roundId],
            getTimestamp[_roundId],
            _roundId
        );
    }

    function latestRoundData()
        external
        view
        override
        returns (
            uint80 roundId,
            int256 answer,
            uint256 startedAt,
            uint256 updatedAt,
            uint80 answeredInRound
        )
    {
        return (
            uint80(latestRound),
            latestAnswer,
            getStartedAt[latestRound],
            latestTimestamp,
            uint80(latestRound)
        );
    }

    function decimals() external view override returns (uint8) {
        return decimalsvar;
    }

    function description() external view override returns (string memory) {
        return descriptionvar;
    }

    function version() external  pure override returns (uint256) {
        return versionvar;
    }
}

  • 聚合器合約(核心)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";

contract YieldAggregator is ReentrancyGuard, Ownable {
    using SafeERC20 for IERC20;

    IERC20 public immutable asset; // 存入的資產(chǎn),如 USDC
    IERC20 public immutable rewardToken; // 收益代幣,如 yUSDC
    AggregatorV3Interface public priceFeed; // Chainlink 價(jià)格預(yù)言機(jī)

    mapping(address => uint256) public shares; // 用戶份額
    uint256 public totalShares;
    uint256 public totalAssetsDeposited;

    event Deposit(address indexed user, uint256 amount, uint256 shares);
    event Withdraw(address indexed user, uint256 amount, uint256 shares);

    constructor(
        address _asset,
        address _rewardToken,
        address _priceFeed
    ) Ownable(msg.sender) {
        asset = IERC20(_asset);
        rewardToken = IERC20(_rewardToken);
        priceFeed = AggregatorV3Interface(_priceFeed);
    }

    // 獲取 ETH/USD 價(jià)格(示例)
    function getETHPrice() public view returns (uint256) {
        (, int price, , , ) = priceFeed.latestRoundData();
        require(price > 0, "Invalid price");
        return uint256(price);
    }

    // 存入資產(chǎn)
    function deposit(uint256 amount) external nonReentrant {
        require(amount > 0, "Amount must be > 0");

        uint256 sharesToMint = totalShares == 0 ? amount : (amount * totalShares) / totalAssetsDeposited;

        asset.safeTransferFrom(msg.sender, address(this), amount);
        shares[msg.sender] += sharesToMint;
        totalShares += sharesToMint;
        totalAssetsDeposited += amount;

        // 模擬策略投資(此處省略實(shí)際策略調(diào)用)
        // 例如:strategy.deposit(amount);

        emit Deposit(msg.sender, amount, sharesToMint);
    }

    // 提取資產(chǎn) + 收益
    function withdraw(uint256 sharesAmount) external nonReentrant {
        require(shares[msg.sender] >= sharesAmount, "Not enough shares");

        uint256 assetAmount = (sharesAmount * totalAssetsDeposited) / totalShares;

        shares[msg.sender] -= sharesAmount;
        totalShares -= sharesAmount;
        totalAssetsDeposited -= assetAmount;

        // 模擬策略贖回
        // 例如:strategy.withdraw(assetAmount);

        asset.safeTransfer(msg.sender, assetAmount);

        emit Withdraw(msg.sender, assetAmount, sharesAmount);
    }

    // 查詢用戶資產(chǎn)價(jià)值(USD)
    function getUserAssetValue(address user) external view returns (uint256) {
        uint256 userAssets = (shares[user] * totalAssetsDeposited) / totalShares;
        return userAssets; // 若資產(chǎn)為 USDC,可視為 1:1 USD
    }

    // 管理員救援函數(shù)
    function rescue(address token, uint256 amount) external onlyOwner {
        IERC20(token).safeTransfer(msg.sender, amount);
    }
}

編譯指令:npx hardhat compile

測試

const { expect } = require("chai");
const { ethers, deployments } = require("hardhat");

describe("YieldAggregator", function () {
  let yieldAg;          // 被測合約
  let asset;            // 存入資產(chǎn)(MyToken3)
  let reward;           // 獎(jiǎng)勵(lì)代幣(MyToken1 / USDC)
  let feed;             // MockV3Aggregator
  let owner, alice, bob;

  const INITIAL_PRICE = 2000_0000_0000;          // 8 位小數(shù),2000 USD/ETH
  const DEPOSIT_AMOUNT = ethers.parseUnits("100", 18); // 100 個(gè) asset 代幣

  beforeEach(async () => {
    [owner, alice, bob] = await ethers.getSigners();

    // 必須保證 deployments 文件夾里有對(duì)應(yīng)的腳本:
    // 01-deploy-tokens.js   02-deploy-mock.js   03-deploy-yield.js
    await deployments.fixture(["token3", "token1", "MockV3Aggregator", "YieldAggregator"]);

    const a = await deployments.get("MyToken1");          // 存入資產(chǎn)
    const b = await deployments.get("MyToken3");          // 獎(jiǎng)勵(lì)代幣(USDC)
    const c = await deployments.get("MockV3Aggregator");
    const d = await deployments.get("YieldAggregator");

    asset   = await ethers.getContractAt("MyToken1", a.address);
    reward  = await ethers.getContractAt("MyToken3", b.address);
    feed    = await ethers.getContractAt("MockV3Aggregator", c.address);
    yieldAg = await ethers.getContractAt("YieldAggregator", d.address);
  });

  /* ------------------  helper  ------------------ */
  async function mintAndApprove(user, amount) {
    await asset.mint(user.address, amount);
    await asset.connect(user).approve(yieldAg.target, amount);
  }

  /* ------------------  測試用例  ------------------ */
  it("部署后初始狀態(tài)正確", async () => {
    console.log(await yieldAg.asset())
    console.log(asset.target);
    console.log(await yieldAg.rewardToken())
    console.log(reward.target);
   console.log(await yieldAg.priceFeed())
   console.log(feed.target);
   console.log(await yieldAg.totalShares());
    console.log(await yieldAg.totalAssetsDeposited());
  });

  it("首次存入正確鑄造份額", async () => {
    await mintAndApprove(alice, DEPOSIT_AMOUNT);

    await yieldAg.connect(alice).deposit(DEPOSIT_AMOUNT)

    //   .to.emit(yieldAg, "Deposit")
    //   .withArgs(alice.address, DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); // 1:1

    console.log("首次存入后用戶份額:",await yieldAg.shares(alice.address))
    // .to.eq(DEPOSIT_AMOUNT);
    console.log("首次存入后份額總量:",await yieldAg.totalShares())
    // .to.eq(DEPOSIT_AMOUNT);
    console.log("首次存入后資產(chǎn)總量:",await yieldAg.totalAssetsDeposited())
    // .to.eq(DEPOSIT_AMOUNT);
  });

  it("二次存入按比例鑄造份額", async () => {
    await mintAndApprove(alice, DEPOSIT_AMOUNT);
    await mintAndApprove(bob,  DEPOSIT_AMOUNT);

    await yieldAg.connect(alice).deposit(DEPOSIT_AMOUNT); // 總量 100,份額 100
    await yieldAg.connect(bob).deposit(DEPOSIT_AMOUNT);   // 總量 200,應(yīng)得 100 份額

    expect(await yieldAg.shares(bob.address)).to.eq(DEPOSIT_AMOUNT);
    expect(await yieldAg.totalShares()).to.eq(DEPOSIT_AMOUNT * 2n);
  });

  it("提取后份額與資產(chǎn)減少", async () => {
    await mintAndApprove(alice, DEPOSIT_AMOUNT);
    await yieldAg.connect(alice).deposit(DEPOSIT_AMOUNT);

    const withdrawShares = DEPOSIT_AMOUNT / 2n;
    const expectAssets   = DEPOSIT_AMOUNT / 2n;

    await expect(yieldAg.connect(alice).withdraw(withdrawShares))
      .to.emit(yieldAg, "Withdraw")
      .withArgs(alice.address, expectAssets, withdrawShares);

    expect(await yieldAg.shares(alice.address)).to.eq(withdrawShares);
    expect(await yieldAg.totalShares()).to.eq(withdrawShares);
    expect(await yieldAg.totalAssetsDeposited()).to.eq(withdrawShares);
  });

  it("無法提取超過自身份額", async () => {
    await mintAndApprove(alice, DEPOSIT_AMOUNT);
    await yieldAg.connect(alice).deposit(DEPOSIT_AMOUNT);

    await expect(
      yieldAg.connect(alice).withdraw(DEPOSIT_AMOUNT + 1n)
    ).to.be.revertedWith("Not enough shares");
  });

  it("rescue 只能 owner 調(diào)用", async () => {
    const rescueAmount = ethers.parseUnits("10", 18);
    await asset.mint(yieldAg.target, rescueAmount);

    // owner 可以 rescue
    await expect(() =>
      yieldAg.connect(owner).rescue(asset.target, rescueAmount)
    ).to.changeTokenBalance(asset, owner, rescueAmount);

    // alice 不能 rescue
    await expect(
      yieldAg.connect(alice).rescue(asset.target, 1n)
    ).to.be.reverted;
  });

  it("getETHPrice 返回 Mock 價(jià)格", async () => {
    expect(await yieldAg.getETHPrice()).to.eq(INITIAL_PRICE);
  });

  it("getUserAssetValue 計(jì)算正確", async () => {
    await mintAndApprove(alice, DEPOSIT_AMOUNT);
    await yieldAg.connect(alice).deposit(DEPOSIT_AMOUNT);

    // 1:1 對(duì)應(yīng),USDC 視為 1 USD
    expect(await yieldAg.getUserAssetValue(alice.address)).to.eq(DEPOSIT_AMOUNT);
  });
});

測試指令:npx hardhat test ./test/xxx.js

部署

module.exports = async  ({getNamedAccounts,deployments})=>{
    const getNamedAccount = (await getNamedAccounts()).firstAccount;
    const secondAccount= (await getNamedAccounts()).secondAccount;
    console.log('secondAccount',secondAccount)
    const {deploy,log} = deployments;
    //資產(chǎn)
       const MyAsset=await deploy("MyToken1",{
        from:getNamedAccount,
        args: ["MyAsset","MyAsset",getNamedAccount],//參數(shù)
        log: true,
    });
    console.log('MyToken 資產(chǎn)合約地址',MyAsset.address)
    
    //獎(jiǎng)勵(lì)代幣
    const MyAward = await deploy("MyToken3",{
        from:getNamedAccount,
        args: ["MyAward","MA",getNamedAccount],//參數(shù)
        log: true,
    })
    console.log('MyAward 獎(jiǎng)勵(lì)代幣合約地址',MyAward.address)
    //執(zhí)行MockV3Aggregator部署合約
  const MockV3Aggregator=await deploy("MockV3Aggregator",{
        from:getNamedAccount,
        args: [8,"USDC/USD", 200000000000],//參數(shù)
        log: true,
    })
  console.log("MockV3Aggregator合約地址:", MockV3Aggregator.address);
    const YieldAggregator=await deploy("YieldAggregator",{
        from:getNamedAccount,
        args: [MyAsset.address,MyAward.address,MockV3Aggregator.address],//參數(shù) 資產(chǎn)地址,獎(jiǎng)勵(lì)地址,喂價(jià)
        log: true,
    })
    // await hre.run("verify:verify", {
    //     address: TokenC.address,
    //     constructorArguments: [TokenName, TokenSymbol],
    //     });
    console.log('YieldAggregator 聚合器合約地址',YieldAggregator.address)
}
module.exports.tags = ["all", "YieldAggregator"];

部署指令:npx hardhat deploy --tags YieldAggregator

總結(jié)

本文用三份合約 + 一套測試/部署腳本,交付了一臺(tái)「袖珍版 Y 機(jī)」:

  • MyToken1:可增發(fā)、可燃燒的生息資產(chǎn),也是用戶最終取回的「本金+收益」;
  • MockV3Aggregator:本地模擬 Chainlink,8 位小數(shù) USDC/USD 喂價(jià),方便離線調(diào)試;
  • YieldAggregator:核心資金池,采用「份額制」記賬,任何時(shí)刻按份額比例享有底層資產(chǎn), deposit/withdraw 均防重入,并預(yù)留策略插槽,可無縫接入外部 DeFi 挖礦。

測試用例覆蓋首次/二次存入、部分/超額提取、Owner 救援、價(jià)格獲取、用戶資產(chǎn)估值等 8 條核心路徑,全部綠燈。部署腳本一鍵完成代幣、喂價(jià)、聚合器的鏈上生成,并打印地址,方便前端直接對(duì)接。

后續(xù)可以進(jìn)行如下優(yōu)化迭代:

  1. 把 Mock 換成主網(wǎng) Oracle,接入真實(shí) USDC;
  2. 在 deposit/withdraw 里調(diào)用 Aave/Compound/Yearn 策略,讓資金真的去挖礦;
  3. 疊加 Keeper 自動(dòng)復(fù)利、多策略路由、Vault Token 可交易化,逐步進(jìn)化成商業(yè)級(jí)收益聚合器。

代碼已開源,fork 即可開干——愿你的 ETH 24 小時(shí)不打烊,收益永遠(yuǎn)在線!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容