前言
本文是對《React Native DApp 開發(fā)全棧實(shí)戰(zhàn)·從 0 到 1:收益聚合器合約篇》的補(bǔ)充與勘誤,旨在同步更新合約變動與前端調(diào)用示例,保持代碼與文章一致性。
說明
主要針對代幣合約和收益聚合器的修改,其他合約不變
代幣合約
說明:實(shí)現(xiàn)一個多地址授權(quán)代幣,合約中資產(chǎn)代幣和獎勵代幣雷同
// 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 // ?? 部署時一次性給多地址授權(quán)
) ERC20(name_, symbol_) {
// 部署者擁有 DEFAULT_ADMIN_ROLE(可繼續(xù)授權(quán)/撤銷)
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
// 把 MINTER_ROLE 給所有傳入地址
for (uint256 i = 0; i < initialMinters.length; ++i) {
_grantRole(MINTER_ROLE, initialMinters[i]);
}
// 給部署者自己先發(fā) 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]],//參數(shù) 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"];
收益聚合器合約
說明:收益聚合器合約不變,只對部署腳本和測試腳本進(jìn)行調(diào)整
部署腳本
-
特別說明:**部署腳本必須滿足「代幣先、聚合器后」的硬順序:給聚合器腳本加上dependencies: ['token3', 'token4']并讓文件名序號小于聚合器即可,hardhat-deploy 會自動按序執(zhí)行,無需手動調(diào)整。 - 在hardhat項目中deploy/文件夾下也要保證代幣文件要在聚合器部署之前:
# 例如
deploy/
├── 01.deploy.token3.js // 多授權(quán)資產(chǎn)代幣 MyToken3
├── 02.deploy.token4.js // 多授權(quán)獎勵代幣 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");
//資產(chǎn)
// const MyAsset=await deploy("MyToken3",{
// from:getNamedAccount,
// args: ["MyAsset","MyAsset",[getNamedAccount,secondAccount]],//參數(shù)
// log: true,
// });
// console.log('MyToken 資產(chǎn)合約地址',MyAsset.address)
//獎勵代幣
// const MyAward = await deploy("MyToken4",{
// from:getNamedAccount,
// args: ["MyAward","MA",[getNamedAccount,secondAccount]],//參數(shù)
// log: true,
// })
// console.log('MyAward 獎勵代幣合約地址',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)地址,獎勵地址,喂價
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; // 存入資產(chǎn)(MyToken3)
let reward; // 獎勵代幣(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 個 asset 代幣
beforeEach(async () => {
[owner, alice, bob] = await ethers.getSigners();
// 必須保證 deployments 文件夾里有對應(yīng)的腳本:
// 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"); // 存入資產(chǎn)
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);
/* ===== 現(xiàn)勘 ===== */
// 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("部署后初始狀態(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 () => {
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("首次存入后資產(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 份額
console.log(await yieldAg.shares(bob.address))
// .to.eq(DEPOSIT_AMOUNT);
console.log(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 價格", 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 對應(yīng),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
總結(jié)
本文一次性把「多授權(quán)代幣 → 收益聚合器 → 順序部署 → 單測/前端調(diào)用」全鏈路補(bǔ)齊:合約只動部署參數(shù),腳本加 dependencies 保順序,測試用例直接平移前端,mint-approve-deposit/withdraw 一條龍,復(fù)制即可跑通。