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

前言

本文是对《React Native DApp 开发全栈实战·从 0 到 1:收益聚合器合约篇》的补充与勘误,旨在同步更新合约变动与前端调用示例,保持代码与文章一致性。

说明

主要针对代币合约和收益聚合器的修改,其他合约不变

代币合约

说明:实现一个多地址授权代币,合约中资产代币和奖励代币雷同

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

部署脚本

module.exports = async  ({getNamedAccounts,deployments})=>{
    const getNamedAccount = (await getNamedAccounts()).firstAccount;
    const secondAccount= (await getNamedAccounts()).secondAccount;
    console.log('secondAccount',secondAccount)
    const TokenName = "MyETH";
    const TokenSymbol = "MYETH";
    const {deploy,log} = deployments;
    const TokenC=await deploy("MyToken3",{
        from:getNamedAccount,
        args: [TokenName,TokenSymbol,[getNamedAccount,secondAccount]],//参数 name,symblo,[Owner1,Owner1]
        log: true,
    })
    // await hre.run("verify:verify", {
    //     address: TokenC.address,
    //     constructorArguments: [TokenName, TokenSymbol],
    //     });
    console.log('MYTOKEN3合约地址 多Owner合约',TokenC.address)
}
module.exports.tags = ["all", "token3"];

收益聚合器合约

说明:收益聚合器合约不变,只对部署脚本和测试脚本进行调整

部署脚本

  • 特别说明**部署脚本必须满足「代币先、聚合器后」的硬顺序:给聚合器脚本加上dependencies: ['token3', 'token4']并让文件名序号小于聚合器即可,hardhat-deploy 会自动按序执行,无需手动调整。
  • 在hardhat项目中deploy/文件夹下也要保证代币文件要在聚合器部署之前:
# 例如
deploy/
├── 01.deploy.token3.js              // 多授权资产代币 MyToken3
├── 02.deploy.token4.js              // 多授权奖励代币 MyToken4
├── 03.deploy.MockV3Aggregator.js    // ETH/USD 喂价 Mock
└── 04.deploy.YieldAggregator.js     // 收益聚合器(依赖 01-03)
  • 部署脚本
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 deployments.get("MyToken3");
    const MyAward = await deployments.get("MyToken4");

    //资产
    //    const MyAsset=await deploy("MyToken3",{
    //     from:getNamedAccount,
    //     args: ["MyAsset","MyAsset",[getNamedAccount,secondAccount]],//参数
    //     log: true,
    // });
    // console.log('MyToken 资产合约地址',MyAsset.address)
    
    //奖励代币 
    // const MyAward = await deploy("MyToken4",{
    //     from:getNamedAccount,
    //     args: ["MyAward","MA",[getNamedAccount,secondAccount]],//参数
    //     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"];

测试脚本

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", "token4", "MockV3Aggregator", "YieldAggregator"]);

    const a = await deployments.get("MyToken3");          // 存入资产
    const b = await deployments.get("MyToken4");          // 奖励代币(USDC)
    const c = await deployments.get("MockV3Aggregator");
    const d = await deployments.get("YieldAggregator");

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


//     console.log("=== 地址核对 ===");
// console.log("asset  :", await asset.getAddress());
// console.log("reward :", await reward.getAddress());
// console.log("yieldAg:", await yieldAg.getAddress());
// console.log("asset in yieldAg:", await yieldAg.asset());
// console.log("reward in yieldAg:", await yieldAg.rewardToken());
    
  });

  /* ------------------  helper  ------------------ */
  async function mintAndApprove(user, amount) {
    await asset.mint(user.address, amount);
   /* ===== 现勘 ===== */
// console.log("user地址        :", user.address);
// console.log("yieldAg地址     :", await yieldAg.getAddress());
// console.log("approve前额度   :", await asset.allowance(user.address, await yieldAg.getAddress()));
await asset.connect(user).approve(await yieldAg.getAddress(), amount);
// console.log("approve后额度   :", await asset.allowance(user.address, await yieldAg.getAddress()));

  }
// async function mintAndApprove(user, amount) {
//   const yieldAddr = await yieldAg.getAddress();
//   console.log("asset 地址:", await asset.getAddress());
//   console.log("yield 地址:", yieldAddr);
//   console.log("user 地址 :", user.address);

//   await asset.mint(user.address, amount);

//   const allowanceBefore = await asset.allowance(user.address, yieldAddr);
//   console.log("approve 前 allowance:", allowanceBefore.toString());

//   const tx = await asset.connect(user).approve(yieldAddr, amount);
//   await tx.wait(); // 确保上链

//   const allowanceAfter = await asset.allowance(user.address, yieldAddr);
//   console.log("approve 后 allowance:", allowanceAfter.toString());
// }

  /* ------------------  测试用例  ------------------ */
  // 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 () => {
    console.log("================")
    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 份额

    console.log(await yieldAg.shares(bob.address))
    // .to.eq(DEPOSIT_AMOUNT);
    console.log(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 compile
  • 部署npx hardhat deploy --tags xxx,xxx
  • 测试npx hardhat test ./test/xxx.js

总结

本文一次性把「多授权代币 → 收益聚合器 → 顺序部署 → 单测/前端调用」全链路补齐:合约只动部署参数,脚本加 dependencies 保顺序,测试用例直接平移前端,mint-approve-deposit/withdraw 一条龙,复制即可跑通。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容