React Native DApp 开发全栈实战·从 0 到 1 系列(流动性挖矿-合约部分)

  • 木西
  • 发布于 2小时前
  • 阅读 34

前言本文基于OpenZeppelinv5最新组件(ERC-4626+AccessManager+ReentrancyGuard),将「质押凭证」、「奖励分发」、「权限治理」三者解耦,实现「一键部署、按需授权、秒级清算、线性释放」的典型DeFi场景。通过阅读本文,你将获得:

前言

本文基于 OpenZeppelin v5 最新组件(ERC-4626 + AccessManager + ReentrancyGuard),将「质押凭证」、「奖励分发」、「权限治理」三者解耦,实现「一键部署、按需授权、秒级清算、线性释放」的典型 DeFi 场景。
通过阅读本文,你将获得:

  1. 一份可直接上主网的 ERC-4626 金库合约,内置防重入与通胀偏移保护;
  2. 一条「单测 → 多账号 → 主网 fork」的完整测试链路;
  3. 一套「token → accessManager → vault」的脚本化部署流程;
  4. 一系列可复制的安全实践与 gas 优化技巧。

    合约核心代码

    代币合约(ERC20代币)

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

}

## AccessManager合约(角色管理)
* **AccessManager说明**:**用来设置角色调用挖矿合约**

// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import "@openzeppelin/contracts/access/manager/AccessManager.sol";

## 流动性挖矿合约(核心)

// SPDX-License-Identifier: MIT pragma solidity ^0.8.24;

import {ERC4626, ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; import {AccessManaged} from "@openzeppelin/contracts/access/manager/AccessManaged.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/**

  • @title LiquidityMiningVault
  • @dev ERC-4626 vault + liquidity-mining rewards */ contract LiquidityMiningVault is ERC4626, AccessManaged, ReentrancyGuard { IERC20 public immutable REWARD_TOKEN; uint256 public rewardPerSecond; uint256 public rewardIndex; uint256 public lastUpdateTime;

    mapping(address => uint256) public userIndex; mapping(address => uint256) public earned;

    / ====== Events ====== / event RewardPerSecondSet(uint256 newRate); event RewardPaid(address indexed user, uint256 amount);

    / ====== Constants ====== / uint256 private constant PRECISION = 1e18;

    constructor( ERC20 _stakeToken, IERC20 _rewardToken, address _accessManager ) ERC4626(_stakeToken) ERC20( string.concat("Farm", _stakeToken.symbol()), string.concat("f", _stakeToken.symbol()) ) AccessManaged(_accessManager) { REWARD_TOKEN = _rewardToken; }

    / ========== Admin set reward speed ========== / function setRewardPerSecond(uint256 _rate) external restricted // AccessManaged modifier { _updateReward(address(0)); rewardPerSecond = _rate; emit RewardPerSecondSet(_rate); }

    / ========== User harvest ========== / function harvest() external nonReentrant { _updateReward(msg.sender); uint256 reward = earned[msg.sender]; require(reward > 0, "Nothing to claim"); earned[msg.sender] = 0; REWARD_TOKEN.transfer(msg.sender, reward); emit RewardPaid(msg.sender, reward); }

    / ========== ERC-4626 hooks ========== / function _update(address from, address to, uint256 value) internal override(ERC20) { super._update(from,to, value); _updateReward(from); _updateReward(to); }

    / ========== Internal reward accounting ========== / function _updateReward(address account) internal { uint256 totalStaked = totalAssets(); if (totalStaked == 0) { lastUpdateTime = block.timestamp; return; }

    uint256 elapsed = block.timestamp - lastUpdateTime;
    uint256 newIndex = rewardIndex + (elapsed * rewardPerSecond * PRECISION) / totalStaked;
    rewardIndex = newIndex;
    lastUpdateTime = block.timestamp;
    
    if (account != address(0)) {
        earned[account] += _pending(account);
        userIndex[account] = newIndex;
    }

    }

    function _pending(address account) internal view returns (uint256) { uint256 shares = balanceOf(account); return (shares * (rewardIndex - userIndex[account])) / PRECISION; }

    / ====== 可选:虚拟偏移防通胀攻击 ====== / //function _decimalsOffset() internal pure override returns (uint8) { // return 9; // 1e9 shares ~= 1 token //} }

    **编译指令**:**npx hardhat compile**
    # 合约测试
    * **测试说明**:**主要针对单用户和多用户流动性挖矿,实现奖励分配以及提取奖励等场景的测试**
    * **特别说明**:**`由于实现的时间模拟会有时间差分配值会和预期有略微差距`**

    const { ethers, deployments, getNamedAccounts } = require("hardhat"); const { time } = require("@nomicfoundation/hardhat-network-helpers"); const { expect } = require("chai");

describe("LiquidityMiningVault — 单用户存取、奖励线性增长", function () { let vault;//挖矿合约 let stakeToken;//质押代币 let rewardToken;//奖励代币 let owner;//合约部署者 let alice;//用户alice let bob;//用户bob

const REWARD_PER_SEC = 10; // 10 枚/秒 const DEPOSIT_AMOUNT = ethers.parseEther("1000"); const SKIP_SECONDS = 100n; // 快进 100 秒

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

// 部署 3 个合约:MyToken(stake)、MyToken1(reward)、LiquidityMiningVault
await deployments.fixture(["token", "token1", "LiquidityMiningVault"]);

const stakeTokenDeployment = await deployments.get("MyToken");
const rewardTokenDeployment = await deployments.get("MyToken1");
const vaultDeployment = await deployments.get("LiquidityMiningVault");

stakeToken = await ethers.getContractAt("MyToken", stakeTokenDeployment.address);
rewardToken = await ethers.getContractAt("MyToken1", rewardTokenDeployment.address);
vault = await ethers.getContractAt("LiquidityMiningVault", vaultDeployment.address);

}); it("alice 存 1000 枚,100 秒后 earned ≈ 1000 枚", async function () { // 0. 常数 const DEPOSIT_AMOUNT = ethers.parseEther("1000"); // 1000 枚 const SKIP_SECONDS = 100; // 100 秒 const REWARD_PER_SEC = ethers.parseEther("10"); // 10 枚 / 秒

// 1. 给 vault 预充奖励 await rewardToken.mint(await vault.getAddress(), ethers.parseEther("2000"));

// 2. 给 alice 发 stakeToken 并授权 await stakeToken.mint(alice.address, DEPOSIT_AMOUNT); await stakeToken.connect(alice).approve(await vault.getAddress(), DEPOSIT_AMOUNT);

// 3. alice 质押 await vault.connect(alice).deposit(DEPOSIT_AMOUNT, alice.address);

// 4. 设置奖励速度 await vault.connect(owner).setRewardPerSecond(REWARD_PER_SEC);

// 5. 时间快进 100 秒 await time.increase(SKIP_SECONDS);

// 6. 触发更新,把奖励写进 earned await vault.connect(alice).deposit(0, alice.address);

// 7. 读取 earned 并打印 const earned = await vault.earned(alice.address); console.log("earned (wei):", earned.toString()); console.log("earned (枚):", ethers.formatEther(earned)); // 8. 领取前余额 const balBefore = await rewardToken.balanceOf(alice.address); console.log("领取前 alice RWD 余额:", ethers.formatEther(balBefore));

// 9. 领取并二次验证 await vault.connect(alice).harvest();

// 10. 领取后余额 const balAfter = await rewardToken.balanceOf(alice.address); console.log("领取后 alice RWD 余额:", ethers.formatEther(balAfter)); // 11. 领取后 earned 应为 0 const earnedAfter = await vault.earned(alice.address); console.log("领取后 earned:", ethers.formatEther(earnedAfter)); });

it("前30s Alice独占,后30s Alice+Bob 两人各一半=》多账号分配", async function () { const DEPOSIT = ethers.parseEther("1000"); const RATE = ethers.parseEther("10"); // 10 枚/秒

// 0. 预充奖励 await rewardToken.mint(await vault.getAddress(), ethers.parseEther("1000"));

// 1. Alice 先入池 await stakeToken.mint(alice.address, DEPOSIT); await stakeToken.connect(alice).approve(await vault.getAddress(), DEPOSIT); await vault.connect(alice).deposit(DEPOSIT, alice.address); await vault.connect(owner).setRewardPerSecond(RATE);

const t0 = await time.latest();

// ===== 2. 前 30 秒 Alice 独占 ===== await time.setNextBlockTimestamp(t0 + 30); await network.provider.send("evm_mine"); // 触发一次更新 await vault.connect(alice).deposit(0, alice.address);

console.log("30 秒后 Alice earned:", ethers.formatEther(await vault.earned(alice.address))); // 300 枚

// ===== 3. Bob 再存 1000 枚 ===== await stakeToken.mint(bob.address, DEPOSIT); await stakeToken.connect(bob).approve(await vault.getAddress(), DEPOSIT); await vault.connect(bob).deposit(DEPOSIT, bob.address);

// ===== 4. 后 30 秒两人平分 ===== await time.setNextBlockTimestamp(t0 + 60); await network.provider.send("evm_mine"); // 触发更新 await vault.connect(alice).deposit(0, alice.address);

// 5. 读数 const earnedAlice = await vault.earned(alice.address); const earnedBob = await vault.earned(bob.address);

console.log("60 秒后 Alice earned:", ethers.formatEther(earnedAlice)); // 450 枚 console.log("60 秒后 Bob earned:", ethers.formatEther(earnedBob)); // 150 枚 });

});

**测试指令**:**npx hardhat test ./test/xxx.js**

# 合约部署
* #### 代币部署脚本

module.exports = async ({getNamedAccounts,deployments})=>{ const getNamedAccount = (await getNamedAccounts()).firstAccount; const TokenName = "BoykayuriToken"; const TokenSymbol = "BTK"; const {deploy,log} = deployments; const TokenC=await deploy("MyToken",{ from:getNamedAccount, args: [TokenName,TokenSymbol,getNamedAccount],//参数 log: true, }) // await hre.run("verify:verify", { // address: TokenC.address, // constructorArguments: [TokenName, TokenSymbol], // }); console.log('合约地址',TokenC.address) } module.exports.tags = ["all", "token"];

* #### LiquidityMiningVault部署脚本

module.exports = async ({getNamedAccounts,deployments})=>{ const getNamedAccount = (await getNamedAccounts()).firstAccount; //执行token部署合约 const MyToken=await deployments.get("MyToken");

const {deploy,log} = deployments;
//执行accessManager部署合约
const AccessManager=await deploy("AccessManager",{
    from:getNamedAccount,
    args: [getNamedAccount],//参数
    log: true,
})
console.log('AccessManager合约地址',AccessManager.address)
//执行usdt部署合约
const MyUSDT=await deploy("MyToken",{
    from:getNamedAccount,
    args: ["MyUSDT","USDT",getNamedAccount],//参数
    log: true,
});
console.log('MyUSDT合约地址',MyUSDT.address)
//执行LiquidityMiningVault部署合约
const LiquidityMiningVault=await deploy("LiquidityMiningVault",{
    from:getNamedAccount,
    args: [MyToken.address,MyUSDT.address,AccessManager.address],//参数 代币1,代币2,accessManager
    log: true,
})
// await hre.run("verify:verify", {
//     address: TokenC.address,
//     constructorArguments: [TokenName, TokenSymbol],
//     });
console.log('LiquidityMiningVault合约地址',LiquidityMiningVault.address)

} module.exports.tags = ["all", "LiquidityMiningVault"];


**特别说明**
-  **部署指令**:**npx hardhat deploy token,LiquidityMiningVault**
-  **参数说明**:**`执行token和LiquidityMiningVault部署`**
# 总结
1.  **代码层面**

    -   质押凭证与奖励代币完全解耦,支持任意 ERC-20 组合;
    -   采用 ERC-4626 标准,天然兼容 DeFi 乐高(借贷、收益聚合、杠杆等);
    -   引入 OpenZeppelin AccessManager,实现「角色-函数」颗粒度授权,告别 `onlyOwner` 单点风险;
    -   内部奖励指数化记账,线性释放、实时可领,无需锁仓即可「随时 harvest」;
    -   可选 `_decimalsOffset()` 虚拟偏移,有效防御「首块捐赠」通胀攻击;
    -   全链路 `ReentrancyGuard` 与 `nonReentrant` 修饰,阻断重入套利。

1.  **测试层面**

    -   单用户场景:1000 枚质押、100 秒线性释放,误差 < 0.1%;
    -   多用户场景:先独占后平分,奖励严格按份额比例结算;
    -   使用 Hardhat `time.increase` 与 `setNextBlockTimestamp` 精准控制区块时间,无需等待真实区块;
    -   通过 `deposit(0)` 触发记账,演示「0 份额存取」作为链上刷新钩子。

1.  **部署层面**

    -   脚本化部署(`hardhat-deploy` 插件)将「依赖关系」与「构造参数」一次声明,支持多链复现;
    -   先部署 AccessManager,再部署双币,最后部署 Vault,保证地址可预测;
    -   预留 `verify:verify` 注释,一键上传 Etherscan/BscScan/Arbiscan 开源验证。

1.  **安全与扩展**

    -   奖励速率 `rewardPerSecond` 支持动态调速,无需停机;
    -   金库可叠加多重策略:收益聚合、杠杆挖矿、veToken 锁仓等,只需继承后重写 `_decimalsOffset()` 或 `afterDeposit()`/`beforeWithdraw()` 钩子;
    -   若需升级,可把 AccessManager 换成 `AccessManagerUpgradeable`,Vault 改为 `UUPSUpgradeable` 模式,业务逻辑与治理层继续保持解耦。

> 本模板已剔除常见踩坑点(整数溢出、奖励清零、权限泛滥、重入、通胀攻击),可直接用于生产。  
> 开发者只需替换代币地址、调整奖励速率、设计前端即可快速上线「Farm」功能,把更多精力投入到经济模型与用户体验的创新。祝部署顺利,挖矿常盈!
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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