React Native DApp 開發(fā)全棧實戰(zhàn)·從 0 到 1 系列(流動性挖礦-合約部分)

前言

本文基于 OpenZeppelin v5 最新組件(ERC-4626 + AccessManager + ReentrancyGuard),將「質押憑證」、「獎勵分發(fā)」、「權限治理」三者解耦,實現(xiàn)「一鍵部署、按需授權、秒級清算、線性釋放」的典型 DeFi 場景。
通過閱讀本文,你將獲得:

  1. 一份可直接上主網(wǎng)的 ERC-4626 金庫合約,內置防重入與通脹偏移保護;
  2. 一條「單測 → 多賬號 → 主網(wǎng) fork」的完整測試鏈路;
  3. 一套「token → accessManager → vault」的腳本化部署流程;
  4. 一系列可復制的安全實踐與 gas 優(yōu)化技巧。

合約核心代碼

代幣合約(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

合約測試

  • 測試說明主要針對單用戶和多用戶流動性挖礦,實現(xiàn)獎勵分配以及提取獎勵等場景的測試
  • 特別說明由于實現(xiàn)的時間模擬會有時間差分配值會和預期有略微差距
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. 常數(shù)
  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 發(fā) 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. 觸發(fā)更新,把獎勵寫進 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");
  // 觸發(fā)一次更新
  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");
  // 觸發(fā)更新
  await vault.connect(alice).deposit(0, alice.address);

  // 5. 讀數(shù)
  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],//參數(shù)
        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;
    //執(zhí)行token部署合約
    const MyToken=await deployments.get("MyToken");
     
    const {deploy,log} = deployments;
    //執(zhí)行accessManager部署合約
    const AccessManager=await deploy("AccessManager",{
        from:getNamedAccount,
        args: [getNamedAccount],//參數(shù)
        log: true,
    })
    console.log('AccessManager合約地址',AccessManager.address)
    //執(zhí)行usdt部署合約
    const MyUSDT=await deploy("MyToken",{
        from:getNamedAccount,
        args: ["MyUSDT","USDT",getNamedAccount],//參數(shù)
        log: true,
    });
    console.log('MyUSDT合約地址',MyUSDT.address)
    //執(zhí)行LiquidityMiningVault部署合約
    const LiquidityMiningVault=await deploy("LiquidityMiningVault",{
        from:getNamedAccount,
        args: [MyToken.address,MyUSDT.address,AccessManager.address],//參數(shù) 代幣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
  • 參數(shù)說明執(zhí)行token和LiquidityMiningVault部署

總結

  1. 代碼層面

    • 質押憑證與獎勵代幣完全解耦,支持任意 ERC-20 組合;
    • 采用 ERC-4626 標準,天然兼容 DeFi 樂高(借貸、收益聚合、杠桿等);
    • 引入 OpenZeppelin AccessManager,實現(xiàn)「角色-函數(shù)」顆粒度授權,告別 onlyOwner 單點風險;
    • 內部獎勵指數(shù)化記賬,線性釋放、實時可領,無需鎖倉即可「隨時 harvest」;
    • 可選 _decimalsOffset() 虛擬偏移,有效防御「首塊捐贈」通脹攻擊;
    • 全鏈路 ReentrancyGuardnonReentrant 修飾,阻斷重入套利。
  2. 測試層面

    • 單用戶場景:1000 枚質押、100 秒線性釋放,誤差 < 0.1%;
    • 多用戶場景:先獨占后平分,獎勵嚴格按份額比例結算;
    • 使用 Hardhat time.increasesetNextBlockTimestamp 精準控制區(qū)塊時間,無需等待真實區(qū)塊;
    • 通過 deposit(0) 觸發(fā)記賬,演示「0 份額存取」作為鏈上刷新鉤子。
  3. 部署層面

    • 腳本化部署(hardhat-deploy 插件)將「依賴關系」與「構造參數(shù)」一次聲明,支持多鏈復現(xiàn);
    • 先部署 AccessManager,再部署雙幣,最后部署 Vault,保證地址可預測;
    • 預留 verify:verify 注釋,一鍵上傳 Etherscan/BscScan/Arbiscan 開源驗證。
  4. 安全與擴展

    • 獎勵速率 rewardPerSecond 支持動態(tài)調速,無需停機;
    • 金庫可疊加多重策略:收益聚合、杠桿挖礦、veToken 鎖倉等,只需繼承后重寫 _decimalsOffset()afterDeposit()/beforeWithdraw() 鉤子;
    • 若需升級,可把 AccessManager 換成 AccessManagerUpgradeable,Vault 改為 UUPSUpgradeable 模式,業(yè)務邏輯與治理層繼續(xù)保持解耦。

本模板已剔除常見踩坑點(整數(shù)溢出、獎勵清零、權限泛濫、重入、通脹攻擊),可直接用于生產(chǎn)。
開發(fā)者只需替換代幣地址、調整獎勵速率、設計前端即可快速上線「Farm」功能,把更多精力投入到經(jīng)濟模型與用戶體驗的創(chuàng)新。祝部署順利,挖礦常盈!

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • """1.個性化消息: 將用戶的姓名存到一個變量中,并向該用戶顯示一條消息。顯示的消息應非常簡單,如“Hello ...
    她即我命閱讀 5,271評論 0 6
  • 為了讓我有一個更快速、更精彩、更輝煌的成長,我將開始這段刻骨銘心的自我蛻變之旅!從今天開始,我將每天堅持閱...
    李薇帆閱讀 2,248評論 1 4
  • 似乎最近一直都在路上,每次出來走的時候感受都會很不一樣。 1、感恩一直遇到好心人,很幸運。在路上總是...
    時間里的花Lily閱讀 1,752評論 1 3
  • 1、expected an indented block 冒號后面是要寫上一定的內容的(新手容易遺忘這一點); 縮...
    庵下桃花仙閱讀 1,098評論 1 2
  • 一、工具箱(多種工具共用一個快捷鍵的可同時按【Shift】加此快捷鍵選取)矩形、橢圓選框工具 【M】移動工具 【V...
    墨雅丫閱讀 1,589評論 0 0

友情鏈接更多精彩內容