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

前言

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

通过阅读本文,你将获得:

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

合约核心代码

代币合约(ERC20代币)

typescript 复制代码
// 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说明用来设置角色调用挖矿合约
arduino 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/access/manager/AccessManager.sol";

流动性挖矿合约(核心)

ini 复制代码
// 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

合约测试

  • 测试说明主要针对单用户和多用户流动性挖矿,实现奖励分配以及提取奖励等场景的测试
  • 特别说明由于实现的时间模拟会有时间差分配值会和预期有略微差距
ini 复制代码
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

合约部署

代币部署脚本
ini 复制代码
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部署脚本
javascript 复制代码
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() 虚拟偏移,有效防御「首块捐赠」通胀攻击;
    • 全链路 ReentrancyGuardnonReentrant 修饰,阻断重入套利。
  2. 测试层面

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

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

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

本模板已剔除常见踩坑点(整数溢出、奖励清零、权限泛滥、重入、通胀攻击),可直接用于生产。

开发者只需替换代币地址、调整奖励速率、设计前端即可快速上线「Farm」功能,把更多精力投入到经济模型与用户体验的创新。祝部署顺利,挖矿常盈!

相关推荐
大白猴17 小时前
【大白话解析】OpenZeppelin 的 ReentrancyGuard 库:以太坊防重入攻击安全工具箱(附源代码)
区块链·智能合约·solidity·以太坊·evm·重入攻击·恶意合约
空中湖2 天前
solidity从入门到精通 第七章:高级特性与实战项目
区块链·solidity
TinTin Land2 天前
从程序员到「认识罕见病 DAO」发起人,他用 Web3 承载爱与责任
web3
天涯学馆3 天前
深入分析在Solidity中实现多签钱包
智能合约·solidity
木西3 天前
React Native DApp 开发全栈实战·从 0 到 1 系列(NFT交易所-合约部分)
web3·智能合约·solidity
0x派大星4 天前
智能合约安全全解析:常见漏洞、真实案例与防范实践
安全·去中心化·区块链·智能合约
deepdata_cn4 天前
基于JavaScript的智能合约平台(Agoric)
javascript·区块链·智能合约
木西5 天前
React Native DApp 开发全栈实战·从 0 到 1 系列(铸造NFT-前端部分)
前端·react native·web3
木西5 天前
React Native DApp 开发全栈实战·从 0 到 1 系列(铸造NFT-合约部分)
web3·智能合约·solidity