前言
本文是对《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 一条龙,复制即可跑通。