Goat27LPStaking 合约函数文档

本文档列出 src/Goat27/Goat27LPStaking.sol 中所有外部可调用函数的签名、用途、参数与返回值。
权限说明:ANYONE 表示任何地址可调用;GOAT27 表示只能是 goat27 不可变量指定的合约地址;OWNER 表示需要 Ownable.onlyOwner;REENTRANCY_SAFE 表示挂 nonReentrant 修饰符(可重入调用会 revert OZ ReentrancyGuardReentrantCall())。

合约定位:Goat27-USDT LP 代币的无锁仓质押池,Goat27 合约在每次税费分配时把 USDT 转入本合约并调用 triggerDistribution 按 LP 占比分配 USDT 奖励,同时按 10× 同步累积 Goat27Points 奖励。


1. 公共函数:用户操作

1.1 stake

  • 签名:function stake(uint256 amount) external
  • 权限:ANYONE(REENTRANCY_SAFE)
  • 用途:用户质押 GOAT27-USDT LP 代币,获取按 LP 占比瓜分 USDT 奖励的资格。调用前需先自行 approve 本合约对 LP 代币的额度。首次质押的地址会写入 stakers 列表,后续奖励分配会遍历此列表。
  • 参数:
    • amount:本次质押的 LP 数量(LP 自身精度,通常 18 位)。
  • 返回值:无。
  • 错误:
    • ZeroAmount()amount == 0 时抛出。
    • string("ERC20: transfer amount exceeds balance") 等标准 ERC20 错误:用户未授权或 LP 余额不足(底层 safeTransferFrom 抛出)。
  • 副作用:
    • 首次质押的用户:isStaker[msg.sender] = true 并追加到 stakers 列表。
    • stakedLp[msg.sender] += amounttotalLpStaked += amount
  • 事件:Staked(msg.sender, amount)

1.2 redeem

  • 签名:function redeem(uint256 amount) external
  • 权限:ANYONE(REENTRANCY_SAFE)
  • 用途:无条件赎回已质押的 LP 代币,无任何惩罚/费用/锁仓期。即使 pendingUsdt / pendingPoints > 0 也可以随时赎回,奖励不会被扣减。
  • 参数:
    • amount:本次赎回的 LP 数量,必须 ≤ 当前质押数量
  • 返回值:无。
  • 错误:
    • ZeroAmount()amount == 0 时抛出。
    • InsufficientStake()amount > stakedLp[msg.sender] 时抛出。
  • 副作用:
    • stakedLp[msg.sender] -= amounttotalLpStaked -= amount
    • stakedLp 减到 0,不会自动清理 stakers 列表与 isStaker 标记(用户仍然出现在 getStakerCount() 中,分配时通过 stakedLp == 0 跳过)。
  • 事件:Redeemed(msg.sender, amount)

1.3 claimRewards

  • 签名:function claimRewards() external
  • 权限:ANYONE(REENTRANCY_SAFE)
  • 用途:一次性提取自己的全部 pendingUsdtpendingPoints 奖励。两个 token 独立判断余额,任一不足都整体回退。
  • 参数:无。
  • 返回值:无。
  • 错误:
    • ZeroAmount()pendingUsdt == 0 && pendingPoints == 0 时抛出。
    • InsufficientPoolBalance()pendingUsdt > 0 但合约 USDT 余额不足,或 pendingPoints > 0 但合约 Points 余额不足时抛出。
  • 执行逻辑:
    1
    2
    3
    4
    5
    6
    若 pendingUsdt > 0:
    USDT 余额不足 → revert InsufficientPoolBalance
    否则 pendingUsdt 清零,USDT 转给 msg.sender
    若 pendingPoints > 0:
    Points 余额不足 → revert InsufficientPoolBalance
    否则 pendingPoints 清零,Points 转给 msg.sender

    即使 USDT 余额充足但 Points 不足,USDT 也不会发放(整体回退,防止零碎发放)。

  • 事件:RewardsClaimed(msg.sender, usdtReward, pointsReward)

2. 受限函数:触发分配

2.1 triggerDistribution

  • 签名:function triggerDistribution(uint256 usdtAmount) external
  • 权限:GOAT27(REENTRANCY_SAFE)
  • 用途:由 Goat27 主合约在每次税费分配时调用,按 LP 占比把 usdtAmount USDT 瓜分给所有 staker,同时按 POINTS_MULTIPLIER 同步累积 Goat27Points 奖励。调用前 Goat27 必须先把对应数量的 USDT 转入本合约;若转账与 triggerDistribution 不在同一交易原子完成,USDT 余额会不足,claimRewards 时会 revert。
  • 参数:
    • usdtAmount:本次分配的 USDT 数量(USDT 自身精度,通常 18 位;BSC 主网 USDT 也是 18 位)。
  • 返回值:无。
  • 错误:
    • OnlyGoat27()msg.sender != goat27 时抛出。
  • 边缘情况:
    • usdtAmount == 0:直接返回,不发事件、不改 storage。
    • totalLpStaked == 0:发 DistributionTriggered(usdtAmount, 0),USDT 留在合约余额中等待未来分配;不会清零 staker 状态。
    • 某个 staker 的 stakedLp == 0(全额赎回过):跳过,不计入 stakerCount
    • 某个 staker 的本次份额 (usdtAmount * userStake) / total 计算结果为 0(份额太小导致向下取整):跳过,避免分配尘埃。
  • 公式:
    1
    2
    usdtReward_i = usdtAmount * stakedLp[i] / totalLpStaked
    pointsReward_i = usdtReward_i * POINTS_MULTIPLIER (10)
  • 事件:DistributionTriggered(usdtAmount, stakerCount)(stakerCount 是本次实际获得奖励的用户数)。

3. 管理员函数:资产救援

3.1 rescueToken

  • 签名:function rescueToken(address token, uint256 amount, address recipient) external
  • 权限:OWNER
  • 用途:owner 紧急提取合约中误转入或暂时滞留的任意 BEP20 代币。可用于救援 USDT、Goat27Points、LP 代币,也可救援项目方误转入的任意其他代币。
  • 参数:
    • token:被救援的代币地址,不能为零地址
    • amount:救援数量,不会做余额校验,若超过余额则 safeTransfer 会 revert。
    • recipient:接收地址,不能为零地址
  • 返回值:无。
  • 错误:
    • ZeroAddress()token == 0recipient == 0 时抛出。
    • 标准 ERC20 错误:余额不足、未授权等(底层 safeTransfer 抛出)。
  • 事件:TokenRescued(token, recipient, amount)

4. 视图函数

4.1 goat27

  • 签名:function goat27() external view returns (address)
  • 权限:ANYONE
  • 用途:返回允许调用 triggerDistribution 的 Goat27 主合约地址(不可变量)。

4.2 points

  • 签名:function points() external view returns (address)
  • 权限:ANYONE
  • 用途:返回用于同步发放积分奖励的 Goat27Points 合约地址(不可变量)。

4.3 usdt

  • 签名:function usdt() external view returns (address)
  • 权限:ANYONE
  • 用途:返回用于发放 USDT 奖励的 USDT 合约地址(不可变量)。

4.4 lpToken

  • 签名:function lpToken() external view returns (address)
  • 权限:ANYONE
  • 用途:返回通过 PancakeLibrary.pairFor(goat27, usdt) CREATE2 算法预先算出的 LP 代币地址(不可变量)。注意:此处只算出地址,并不要求 LP 已部署

4.5 POINTS_MULTIPLIER

  • 签名:function POINTS_MULTIPLIER() external view returns (uint256)
  • 权限:ANYONE
  • 用途:返回常量 10,即每分到 1 USDT 同步累积 10 枚 Goat27Points。

4.6 stakedLp

  • 签名:function stakedLp(address) external view returns (uint256)
  • 权限:ANYONE
  • 用途:查询指定用户当前质押的 LP 数量。

4.7 pendingUsdt

  • 签名:function pendingUsdt(address) external view returns (uint256)
  • 权限:ANYONE
  • 用途:查询指定用户待领取的 USDT 奖励余额。

4.8 pendingPoints

  • 签名:function pendingPoints(address) external view returns (uint256)
  • 权限:ANYONE
  • 用途:查询指定用户待领取的 Goat27Points 奖励余额。

4.9 isStaker

  • 签名:function isStaker(address) external view returns (bool)
  • 权限:ANYONE
  • 用途:查询指定地址是否在 stakers 列表中(首次质押后置为 true,永不清除)。

4.10 stakers

  • 签名:function stakers(uint256) external view returns (address)
  • 权限:ANYONE
  • 用途:按索引返回 stakers 列表中的地址。该列表用于分配奖励时的遍历。

4.11 totalLpStaked

  • 签名:function totalLpStaked() external view returns (uint256)
  • 权限:ANYONE
  • 用途:返回当前所有用户质押的 LP 总数,triggerDistribution 计算占比时分母。

4.12 getStakerCount

  • 签名:function getStakerCount() external view returns (uint256)
  • 权限:ANYONE
  • 用途:返回 stakers 数组的长度。注意:可能多于当前实际有质押的用户数,因为已全额赎回的用户不会被清理。

5. Ownable 标准函数

合约继承 OpenZeppelin Ownable(initialOwner_),提供以下标准接口(节选):

函数 签名 说明
owner function owner() external view returns (address) 当前合约所有者
transferOwnership function transferOwnership(address newOwner) external 发起所有权转移(仅 owner)
renounceOwnership function renounceOwnership() external 放弃所有权(仅 owner)
OwnershipTransferred event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) 所有权转移事件(来自 OZ)

6. 事件索引

事件 签名 触发时机
Staked event Staked(address indexed user, uint256 amount) 用户质押 LP
Redeemed event Redeemed(address indexed user, uint256 amount) 用户赎回 LP
DistributionTriggered event DistributionTriggered(uint256 indexed usdtAmount, uint256 stakerCount) Goat27 调用 triggerDistribution 完成分配,stakerCount 为本次实际获得奖励的用户数
RewardsClaimed event RewardsClaimed(address indexed user, uint256 usdtAmount, uint256 pointsAmount) 用户通过 claimRewards 提取奖励
TokenRescued event TokenRescued(address indexed token, address indexed recipient, uint256 amount) owner 救援代币
OwnershipTransferred event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) 所有权转移(来自 OZ)

7. 错误索引

错误 触发条件
ZeroAddress() 关键参数为零地址(适用于 rescueToken 等)
ZeroAmount() 质押/赎回/领取奖励时数量为 0
InsufficientStake() 赎回数量超过当前质押数量
NoBnb() 直接向合约发送 BNB
OnlyGoat27() goat27 不可变量地址调用了 triggerDistribution
InsufficientPoolBalance() claimRewards 时 USDT 或 Points 余额不足
ReentrancyGuardReentrantCall() 重入调用(OZ 自带)

8. 接收 BNB

合约不接受 BNB:任何 msg.value > 0 的直接转账都会 revert NoBnb()

⚠️ 此项设计意味着合约不会出现误转入 BNB 滞留的情况;若希望接收 BNB,只能通过 wrap → WBNB → 救援的方式。


9. 分配示例

假设 3 个用户 A/B/C 分别质押 100/300/600 LP(总 1000 LP),Goat27 调用 triggerDistribution(100 * 1e18) 分配 100 USDT:

用户 LP 质押量 USDT 奖励(100 * stake / 1000) Points 奖励(USDT × 10)
A 100 10 USDT 100 Points
B 300 30 USDT 300 Points
C 600 60 USDT 600 Points
总计 1000 100 USDT 1000 Points

注意:此处忽略向下取整损失;若有零头,实际总量可能略小于 100 USDT(归合约余额,可通过 rescueToken 回收或等待下一次分配触发再次分配)。


10. 部署前/后清单

部署前必读

  • Goat27、Goat27Points、USDT、初始 owner 四个地址必须正确;三者若为零地址则 constructor revert ZeroAddress()
  • LP 代币地址由 PancakeLibrary.pairFor(goat27, usdt) 计算;只要 Goat27 + USDT 正确,LP 地址自动正确,无需查 PancakeSwap Factory。

部署后必做

  • 手动向本合约转入 Goat27Points(例如管理员调 transfer 转入若干积分);否则用户首次 claimRewards 会因 Points 余额不足 revert。
  • 不需要手动转入 USDT;每次 Goat27 自动分配时会同步转入。
  • 推荐把 LP 质押池加入 Goat27 的双白名单(addToWhitelist 调用),避免 LP 池自身持币时被 5% 税污染(尽管本合约不主动接收 Goat27)。

救援场景

  • 若合约 USDT 或 Points 余额不足且长期未补充,claimRewards 会持续 revert InsufficientPoolBalance;通过 transfer 向合约补充对应 token 后即可正常领取。
  • 若误转入其他代币,owner 可通过 rescueToken 提回;但若误转入 LP 代币,注意救援会降低合约总 LP 持有量,从而让剩余 staker 占比变大,不影响已记录但未发放的奖励额度。