构建 DeFi 收益聚合器:Solidity 0.8.24 + Chainlink 预言机开发、测试与部署指南

5 阅读11分钟

前言

本文将系统梳理去中心化收益聚合器的核心理论知识,涵盖其定义、核心功能、解决的行业痛点、落地应用现状及优劣势分析;同时结合技术实操,详细阐述基于Hardhat V3框架,搭配OpenZeppelin V5安全库、Chainlink预言机,采用Solidity 0.8.24语言,从开发、测试到部署落地的完整流程,形成理论与实操兼备的全面指南。

去中心化收益聚合器知识梳理

概述

去中心化收益聚合器是 DeFi 领域的自动化资产配置协议,核心是通过智能合约聚合多协议收益机会、自动执行策略并复利,解决手动管理低效、成本高、风险集中等痛点,已形成多链落地格局,但也面临智能合约安全、策略风险与监管等挑战

一、是什么

去中心化收益聚合器(DeFi Yield Aggregator)是基于区块链的自动化协议,通过智能合约聚合借贷、DEX 流动性池、质押、收益 farming 等多来源收益机会,将用户资产存入 “金库(Vault)”,用算法实时扫描并动态配置资金至最优路径,自动完成收益获取、复投与再平衡,用户以存入资产兑换金库份额,份额净值随策略盈利增长,实现 “一键委托、自动增值” 的被动投资体验。


二、能做什么(核心功能)

  1. 资产聚合与策略执行:用户存入稳定币、ETH、LP 代币等,协议按预设策略(如借贷、流动性挖矿、质押)自动部署至 Aave、Compound、Uniswap、Curve 等底层协议。
  2. 自动复利与复投:自动领取奖励(协议代币、交易手续费、质押收益),兑换为原资产或 LP 代币再投入,放大复利效应,替代手动操作的繁琐与高成本。
  3. 实时优化与再平衡:算法持续监控各协议收益率、风险参数与 gas 成本,动态迁移资金至更高收益机会,兼顾收益与风险控制。
  4. 交易成本优化:批量处理用户交易,摊薄 gas 费用,降低单笔操作成本,尤其适合以太坊等 gas 费较高的网络。
  5. 风险分散与对冲:通过跨协议、跨资产配置降低单一协议或资产的风险敞口,部分协议支持自动对冲与清算风险规避。
  6. 一站式资产管理:单界面完成存款、赎回、收益查看与策略切换,无需逐个对接底层协议,降低使用门槛。

三、解决了什么问题

问题解决方案价值
手动管理低效自动化策略执行与复利节省时间,避免错过高收益窗口
gas 成本高企批量交易与路径优化降低单笔操作成本,提升净收益
信息不对称实时扫描多协议收益率快速匹配最优收益机会
风险集中跨协议分散配置减少单一协议故障或漏洞影响
操作门槛高简化界面与一键操作降低 DeFi 准入门槛,适配新手
复利执行困难自动复投奖励最大化长期收益,提升资金效率

四、落地现状(主流协议与生态)

  1. Yearn.finance(yEarn):DeFi 收益聚合器标杆,首创金库模式,支持多链部署,提供多种自动化策略,代币 YFI 用于治理与收益分红。
  2. Beefy Finance:多链收益聚合器,专注高收益策略,支持自动复利与跨链资产转移,覆盖以太坊、Polygon、BSC 等多网络。
  3. Harvest Finance:提供单资产与 LP 代币策略,自动复投外部奖励并分发 FARM 代币,已部署 70+ 金库,跨以太坊、Polygon、BSC 等。
  4. Pendle Finance:创新收益代币化,将生息资产拆分为本金代币(PT)与收益代币(YT),支持收益交易与固定利率产品,拓展收益策略维度。
  5. 生态趋势:策略复杂化(多层嵌套、跨协议组合)、专业化分工(如专注 Curve 生态)、多链扩展、收益代币化成为重要方向。

五、优势与劣势

优势
  • 收益最大化:算法实时优化资金配置,捕捉市场最优收益率,自动复利提升长期回报。
  • 效率提升:节省手动操作时间,降低 gas 成本,提升资金周转率。
  • 风险分散:跨协议、跨资产配置降低单一风险,部分协议提供额外风控机制。
  • 易用性强:一站式界面降低 DeFi 门槛,适配不同经验用户。
劣势
  • 智能合约风险:协议代码漏洞、权限管理问题可能导致资金损失(如历史上的 Harvest 漏洞事件)。
  • 策略风险:底层协议故障、收益率骤降或代币价格波动可能影响策略表现。
  • 费用成本:部分协议收取管理费(通常 2% - 5%)或绩效费(收益的 10% - 20%),侵蚀净收益。
  • 透明度与治理:部分策略细节不透明,治理代币集中度可能影响决策公正性。
  • 监管不确定性:DeFi 整体监管模糊,可能面临合规风险与政策变动影响。

六、总结与展望

去中心化收益聚合器通过自动化与智能化解决了 DeFi 手动管理的核心痛点,显著提升了资产效率与用户体验,已成为 DeFi 基础设施的重要组成部分。未来将向更复杂的策略组合、更强的风险控制、更高的跨链兼容性与更友好的用户界面演进,同时需持续应对安全、监管与市场波动等挑战,推动 DeFi 从 “高门槛” 向 “普惠化” 发展。

智能合约开发、测试、部署

特别说明:部署脚本必须满足「代币先、聚合器后」的硬顺序

智能合约

  • 多地址授权代币
// SPDX-License-Identifier: MIT
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 {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";

contract MyMultipleToken is ERC20, ERC20Burnable, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    constructor(
        string memory name_,
        string memory symbol_,
        address[] memory initialMinters   // 👈 部署时一次性给多地址授权
    ) ERC20(name_, symbol_) {
        // 部署者拥有 DEFAULT_ADMIN_ROLE(可继续授权/撤销)
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);

        // 把 MINTER_ROLE 给所有传入地址
        for (uint256 i = 0; i < initialMinters.length; ++i) {
            _grantRole(MINTER_ROLE, initialMinters[i]);
        }

        // 给部署者自己先发 1000 个
        _mint(msg.sender, 1000 * 10 ** decimals());
    }

    // 任何拥有 MINTER_ROLE 的人都能铸币
    function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }
}
  • 喂价合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract MockV3Aggregator2 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.24;

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; // 存入的资产,如 USDC
    IERC20 public immutable rewardToken; // 收益代币,如 yUSDC
    AggregatorV3Interface public priceFeed; // Chainlink 价格预言机

    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 价格(示例)
    function getETHPrice() public view returns (uint256) {
        (, int price, , , ) = priceFeed.latestRoundData();
        require(price > 0, "Invalid price");
        return uint256(price);
    }

    // 存入资产
    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;

        // 模拟策略投资(此处省略实际策略调用)
        // 例如:strategy.deposit(amount);

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

    // 提取资产 + 收益
    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);
    }

    // 查询用户资产价值(USD)
    function getUserAssetValue(address user) external view returns (uint256) {
        uint256 userAssets = (shares[user] * totalAssetsDeposited) / totalShares;
        return userAssets; // 若资产为 USDC,可视为 1:1 USD
    }

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

测试脚本

测试场景说明

  1. 首次存入正确铸造份额
  2. 二次存入按比例铸造份额
  3. 提取后份额与资产减少
  4. 无法提取超过自身份额
  5. rescue 只能 owner 调用
  6. getETHPrice 返回 Mock 价格
  7. getUserAssetValue 计算正确
import assert from "node:assert/strict";
import { describe, it, beforeEach } from "node:test";
import { parseEther, formatEther, parseUnits } from 'viem';
import { network } from "hardhat";

describe("TokenFaucet 测试", function () {
    let publicClient: any;
    let owner: any, user1: any, user2: any;
    let asset: any;
    let reward: any;
    let mockV3Aggregator: any;
    let YieldAggregator: any;
    let deployerAddress: any;
    let testClient: any; // 增加 testClient
    const INITIAL_PRICE = 2000_0000_0000n; // 2000 USD (8位小数)
    const DEPOSIT_AMOUNT = parseUnits("100", 18);
    // const FAUCET_AMOUNT = parseEther("100"); // 合约默认单次领取 100 
    // const INITIAL_FAUCET_FUND = parseEther("1000"); // 预存 1000 个代币到水龙头
     async function mintAndApprove(user: any, amount: any) {
        // 显式指定 account 为 user,因为 user 有 MINTER_ROLE
        const hashMint = await asset.write.mint([user.account.address, amount], { 
            account: user.account 
        });
        await publicClient.waitForTransactionReceipt({ hash: hashMint });

        const hashApprove = await asset.write.approve([YieldAggregator.address, amount], { 
            account: user.account 
        });
        await publicClient.waitForTransactionReceipt({ hash: hashApprove });
    }

    beforeEach(async function () {
        // 连接 Hardhat 节点
        const { viem } = await network.connect();
        publicClient = await viem.getPublicClient();
        [owner, user1, user2] = await viem.getWalletClients();
        deployerAddress = owner.account.address;
        testClient = await viem.getTestClient(); // 获取测试客户端
        // 1. 部署代币合约 
        asset = await viem.deployContract("MyMultipleToken", [
            "Asset",
            "ASSET",
            [user1.account.address, user2.account.address]
        ]);
        console.log("Asset 地址:", asset.address);
        reward = await viem.deployContract("MyMultipleToken", [
            "Reward",
            "REWARD",
            [user1.account.address, user2.account.address]
        ]);
        console.log("Reward 地址:", reward.address);
        mockV3Aggregator = await viem.deployContract("MockV3Aggregator2", [
            8,"ETH/USD", INITIAL_PRICE
        ]);
        console.log("MockV3Aggregator 地址:", mockV3Aggregator.address);
        YieldAggregator = await viem.deployContract("YieldAggregator", [
            asset.address,
            reward.address,
            mockV3Aggregator.address
        ]);
        console.log("YieldAggregator 地址:", YieldAggregator.address);
    });

    it("首次存入正确铸造份额", async () => {

        await mintAndApprove(user1, DEPOSIT_AMOUNT);
        await YieldAggregator.write.deposit([DEPOSIT_AMOUNT], { account: user1.account });

        const aliceShares = await YieldAggregator.read.shares([user1.account.address]);
        const totalShares = await YieldAggregator.read.totalShares();
        console.log(aliceShares,DEPOSIT_AMOUNT);
        console.log(totalShares,DEPOSIT_AMOUNT); 
    });
        it("二次存入按比例铸造份额", async () => {
        // User1 先存 100,份额 100
        await mintAndApprove(user1, DEPOSIT_AMOUNT);
        const h1 = await YieldAggregator.write.deposit([DEPOSIT_AMOUNT], { account: user1.account });
        await publicClient.waitForTransactionReceipt({ hash: h1 });

        // User2 再存 50
        const user2Amount = parseUnits("50", 18);
        await mintAndApprove(user2, user2Amount);
        const h2 = await YieldAggregator.write.deposit([user2Amount], { account: user2.account });
        await publicClient.waitForTransactionReceipt({ hash: h2 });

        const user2Shares = await YieldAggregator.read.shares([user2.account.address]);
        const totalShares = await YieldAggregator.read.totalShares();

        // 比例计算:(50 * 100) / 100 = 50
        assert.strictEqual(user2Shares, user2Amount);
        assert.strictEqual(totalShares, DEPOSIT_AMOUNT + user2Amount);
    });

    it("提取后份额与资产减少", async () => {
        await mintAndApprove(user1, DEPOSIT_AMOUNT);
        const h1 = await YieldAggregator.write.deposit([DEPOSIT_AMOUNT], { account: user1.account });
        await publicClient.waitForTransactionReceipt({ hash: h1 });

        const withdrawShares = DEPOSIT_AMOUNT / 2n;
        const h2 = await YieldAggregator.write.withdraw([withdrawShares], { account: user1.account });
        await publicClient.waitForTransactionReceipt({ hash: h2 });

        const aliceShares = await YieldAggregator.read.shares([user1.account.address]);
        const totalAssets = await YieldAggregator.read.totalAssetsDeposited();

        assert.strictEqual(aliceShares, DEPOSIT_AMOUNT - withdrawShares);
        assert.strictEqual(totalAssets, DEPOSIT_AMOUNT - withdrawShares);
    });
 
    it("无法提取超过自身份额", async () => {
        await mintAndApprove(user1, DEPOSIT_AMOUNT);
        const h1 = await YieldAggregator.write.deposit([DEPOSIT_AMOUNT], { account: user1.account });
        await publicClient.waitForTransactionReceipt({ hash: h1 });

        // 尝试提取超过持有的份额
        await assert.rejects(
            async () => {
                await YieldAggregator.write.withdraw([DEPOSIT_AMOUNT + 1n], { account: user1.account });
            },
            (err: any) => {
                return err.message.includes("Not enough shares");
            }
        );
    });
    
    it("rescue 只能 owner 调用", async () => {
        const rescueAmount = parseUnits("10", 18);
        // 模拟有人误把代币直接转入合约
        const h1 = await asset.write.mint([YieldAggregator.address, rescueAmount], { account: user1.account });
        await publicClient.waitForTransactionReceipt({ hash: h1 });

        // user1 (非 owner) 尝试 rescue 应该失败
        await assert.rejects(
            async () => {
                await YieldAggregator.write.rescue([asset.address, rescueAmount], { account: user1.account });
            }
        );

        // owner 尝试 rescue 应该成功
        const beforeBalance = await asset.read.balanceOf([owner.account.address]);
        const h2 = await YieldAggregator.write.rescue([asset.address, rescueAmount], { account: owner.account });
        await publicClient.waitForTransactionReceipt({ hash: h2 });
        const afterBalance = await asset.read.balanceOf([owner.account.address]);

        assert.strictEqual(afterBalance - beforeBalance, rescueAmount);
    });
    it(" getETHPrice 返回 Mock 价格", async () => {
        const price = await YieldAggregator.read.getETHPrice();
        assert.strictEqual(price, INITIAL_PRICE);
    });

    it("getUserAssetValue 计算正确", async () => {
        await mintAndApprove(user1, DEPOSIT_AMOUNT);
        const h1 = await YieldAggregator.write.deposit([DEPOSIT_AMOUNT], { account: user1.account });
        await publicClient.waitForTransactionReceipt({ hash: h1 });

        // 1:1 情况下,资产价值应等于存入量
        const value = await YieldAggregator.read.getUserAssetValue([user1.account.address]);
        assert.strictEqual(value, DEPOSIT_AMOUNT);
    });

});

部署合约

// scripts/deploy.js
import { network, artifacts } from "hardhat";
import { parseUnits } from "viem";
async function main() {
  // 获取客户端(hardhat-viem 插件会自动处理网络连接)
   const { viem } = await network.connect({ network: network.name });//指定网络进行链接
   
  const [deployer, user1, user2] = await viem.getWalletClients();
  const publicClient = await viem.getPublicClient();
  const deployerAddress = deployer.account.address;
  console.log("部署者地址:", deployerAddress);

  // 1. 获取 ABI 和 Bytecode
  const MyMultipleTokenArtifact = await artifacts.readArtifact("MyMultipleToken");
  const MockV3Aggregator2Artifact = await artifacts.readArtifact("MockV3Aggregator2");
  const YieldAggregatorArtifact = await artifacts.readArtifact("YieldAggregator");

  // 2. 部署 Asset 合约
  const assetHash = await deployer.deployContract({
    abi: MyMultipleTokenArtifact.abi,
    bytecode: MyMultipleTokenArtifact.bytecode,
    args: ["Asset", "ASSET", [user1.account.address, user2.account.address]],
  });
  const { contractAddress: assetAddr } = await publicClient.waitForTransactionReceipt({ hash: assetHash });
  console.log("资产合约地址:", assetAddr);

  // 3. 部署 Reward 合约
  const rewardHash = await deployer.deployContract({
    abi: MyMultipleTokenArtifact.abi, // 重用同一个 ABI
    bytecode: MyMultipleTokenArtifact.bytecode,
    args: ["Reward", "REWARD", [user1.account.address, user2.account.address]],
  });
  const { contractAddress: rewardAddr } = await publicClient.waitForTransactionReceipt({ hash: rewardHash });
  console.log("奖励合约地址:", rewardAddr);

  // 4. 部署 MockV3Aggregator2
  const mockHash = await deployer.deployContract({
    abi: MockV3Aggregator2Artifact.abi,
    bytecode: MockV3Aggregator2Artifact.bytecode,
    args: [8, "ETH/USD", 200000000000n], // 建议使用 BigInt (n)
  });
  const { contractAddress: mockAddr } = await publicClient.waitForTransactionReceipt({ hash: mockHash });
  console.log("MockV3Aggregator2 地址:", mockAddr);

  // 5. 部署 YieldAggregator
  // 确保前面的地址都不为 null
  if (!assetAddr || !rewardAddr || !mockAddr) throw new Error("部署失败,地址为空");

  const yieldHash = await deployer.deployContract({
    abi: YieldAggregatorArtifact.abi,
    bytecode: YieldAggregatorArtifact.bytecode,
    args: [assetAddr, rewardAddr, mockAddr],
  });
  const { contractAddress: yieldAddr } = await publicClient.waitForTransactionReceipt({ hash: yieldHash });
  console.log("YieldAggregator 地址:", yieldAddr);
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

结语

至此,关于去中心化收益聚合器的底层逻辑梳理与核心代码实践已全部完成。从策略制定到合约部署,我们共同探索了 DeFi 乐高世界中‘自动化收益’的构建艺术。代码虽已落定,但 DeFi 的进化永无止境。希望这段旅程能为你打开通往更广阔 Web3 应用开发的大门,愿你在未来的链上构建中,不仅能捕获价值,更能创造价值。