React Native DApp 开发全栈实战·从 0 到 1 系列(收益聚合器-合约部分)

  • 木西
  • 发布于 8小时前
  • 阅读 54

前言本文基于OpenZeppelinv5+Solidity0.8.20+,用200行代码带你从零搭建「可编译、可部署、可扩容」的收益聚合器MVP。流程只有四步:双ERC20:一份生息资产,一份收益凭证;Chainlink喂价:链上美元计价,一秒搞定;份额制资金池:按份

前言

本文基于 OpenZeppelin v5 + Solidity 0.8.20+,用 200 行代码带你从零搭建「可编译、可部署、可扩容」的收益聚合器 MVP。流程只有四步:

  1. 双 ERC20:一份生息资产,一份收益凭证;
  2. Chainlink 喂价:链上美元计价,一秒搞定;
  3. 份额制资金池:按份额自动分收益,无需记账;
  4. Hardhat 全套:防重入、Owner 救援、单元测试、部署脚本,一键 compile / test / deploy 打通闭环。

    智能合约

    • 代币合约

      
      // 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);
}

}

- #### 喂价合约

// 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; // 存入的资产,如 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);
}

}

#### 编译指令:npx hardhat compile
# 测试

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

describe("YieldAggregator", function () { let yieldAg; // 被测合约 let asset; // 存入资产(MyToken3) let reward; // 奖励代币(MyToken1 / USDC) let feed; // MockV3Aggregator let owner, alice, bob;

const INITIAL_PRICE = 2000_0000_0000; // 8 位小数,2000 USD/ETH const DEPOSIT_AMOUNT = ethers.parseUnits("100", 18); // 100 个 asset 代币

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

// 必须保证 deployments 文件夹里有对应的脚本:
// 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");          // 存入资产
const b = await deployments.get("MyToken3");          // 奖励代币(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("部署后初始状态正确", 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("首次存入后资产总量:",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,应得 100 份额

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

});

it("提取后份额与资产减少", 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 调用", 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 价格", async () => { expect(await yieldAg.getETHPrice()).to.eq(INITIAL_PRICE); });

it("getUserAssetValue 计算正确", async () => { await mintAndApprove(alice, DEPOSIT_AMOUNT); await yieldAg.connect(alice).deposit(DEPOSIT_AMOUNT);

// 1:1 对应,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; //资产 const MyAsset=await deploy("MyToken1",{ from:getNamedAccount, args: ["MyAsset","MyAsset",getNamedAccount],//参数 log: true, }); console.log('MyToken 资产合约地址',MyAsset.address)

//奖励代币
const MyAward = await deploy("MyToken3",{
    from:getNamedAccount,
    args: ["MyAward","MA",getNamedAccount],//参数
    log: true,
})
console.log('MyAward 奖励代币合约地址',MyAward.address)
//执行MockV3Aggregator部署合约

const MockV3Aggregator=await deploy("MockV3Aggregator",{ from:getNamedAccount, args: [8,"USDC/USD", 200000000000],//参数 log: true, }) console.log("MockV3Aggregator合约地址:", MockV3Aggregator.address); const YieldAggregator=await deploy("YieldAggregator",{ from:getNamedAccount, args: [MyAsset.address,MyAward.address,MockV3Aggregator.address],//参数 资产地址,奖励地址,喂价 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
# 总结
本文用三份合约 + 一套测试/部署脚本,交付了一台「袖珍版 Y 机」:

-   MyToken1:可增发、可燃烧的生息资产,也是用户最终取回的「本金+收益」;
-   MockV3Aggregator:本地模拟 Chainlink,8 位小数 USDC/USD 喂价,方便离线调试;
-   YieldAggregator:核心资金池,采用「份额制」记账,任何时刻按份额比例享有底层资产, deposit/withdraw 均防重入,并预留策略插槽,可无缝接入外部 DeFi 挖矿。

测试用例覆盖首次/二次存入、部分/超额提取、Owner 救援、价格获取、用户资产估值等 8 条核心路径,全部绿灯。部署脚本一键完成代币、喂价、聚合器的链上生成,并打印地址,方便前端直接对接。

后续可以进行如下优化迭代:

1.  把 Mock 换成主网 Oracle,接入真实 USDC;
1.  在 deposit/withdraw 里调用 Aave/Compound/Yearn 策略,让资金真的去挖矿;
1.  叠加 Keeper 自动复利、多策略路由、Vault Token 可交易化,逐步进化成商业级收益聚合器。

代码已开源,fork 即可开干——愿你的 ETH 24 小时不打烊,收益永远在线!
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
木西
木西
0x5D5C...2dD7
江湖只有他的大名,没有他的介绍。