在 DeFi 生态系统中,质押挖矿是一个十分流行的概念。简而言之,质押挖矿指的是用户通过将其代币“质押”到合约中来赚取奖励,通常是项目的原生代币。今天我们来看看一个典型的质押奖励合约——StakingRewards
合约,分析一下它的工作原理,以及背后的代码是如何实现这些功能的。
什么是 StakingRewards 合约?
StakingRewards
是一个允许用户质押某种代币,并根据其质押的数量和时间获取奖励的智能合约。用户质押的代币(即“staking token”)被锁定在合约中,而在奖励代币(“rewards token”)方面,用户则能按比例获得奖励。
简单来说,这个合约有两个主要功能:
- 用户通过质押代币来获得奖励。
- 用户可以随时提取他们的质押代币和已获得的奖励。
核心功能和重要变量
首先,让我们了解合约中的一些关键变量和如何管理用户的奖励、质押情况。
IERC20 public immutable stakingToken;
IERC20 public immutable rewardsToken;
address public owner;
uint256 public duration;
uint256 public finishAt;
uint256 public updatedAt;
uint256 public rewardRate;
uint256 public rewardPerTokenStored;
mapping(address => uint256) public userRewardPerTokenPaid;
mapping(address => uint256) public rewards;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
这些变量代表了合约的核心状态:
stakingToken
和rewardsToken
分别是质押代币和奖励代币的合约地址。duration
是奖励发放的持续时间,单位为秒。finishAt
是奖励结束时间的时间戳。rewardRate
表示每秒发放的奖励量。rewardPerTokenStored
记录每个代币的奖励量,它是计算奖励分配的基础。totalSupply
是所有用户当前质押代币的总量,而balanceOf
则是每个用户质押的代币数量。
奖励的计算和发放
合约的核心之一是如何计算每个用户的奖励。奖励的发放是根据用户质押的比例和时间来决定的。具体来说,奖励是通过 rewardPerToken()
函数进行计算的:
function rewardPerToken() public view returns (uint256) {
if (totalSupply == 0) {
return rewardPerTokenStored;
}
return rewardPerTokenStored
+ (rewardRate * (lastTimeRewardApplicable() - updatedAt) * 1e18)
/ totalSupply;
}
这段代码的意思是:
- 如果合约中没有任何质押(
totalSupply == 0
),那么每个代币的奖励就是之前存储的rewardPerTokenStored
。 - 否则,它会根据
rewardRate
(每秒奖励的数量)、已经过去的时间以及总质押量来动态计算每个代币应得的奖励。
奖励是按质押的比例进行分配的,所以如果你质押得越多,得到的奖励也越多。
质押和提取代币
用户可以通过 stake()
函数将代币质押到合约中,或者通过 withdraw()
函数提取他们的质押代币。这两个操作会触发奖励的重新计算。
function stake(uint256 _amount) external updateReward(msg.sender) {
require(_amount > 0, "amount = 0");
stakingToken.transferFrom(msg.sender, address(this), _amount);
balanceOf[msg.sender] += _amount;
totalSupply += _amount;
}
在 stake()
函数中,用户必须将代币通过 transferFrom
转账到合约中,质押量增加后,合约会相应更新总质押量和用户的质押余额。
提取代币的逻辑类似,通过 withdraw()
函数,用户可以将他们的质押代币提取出来。提取时,同样会更新合约的状态。
奖励领取
一旦用户积累了一定的奖励,他们就可以通过 getReward()
函数领取。这一操作会将用户的奖励转账到他们的账户,并清零他们的奖励余额。
function getReward() external updateReward(msg.sender) {
uint256 reward = rewards[msg.sender];
if (reward > 0) {
rewards[msg.sender] = 0;
rewardsToken.transfer(msg.sender, reward);
}
}
这里的逻辑非常简单:检查用户的奖励余额,若余额大于零,就转账并清空奖励。
设置奖励和奖励分配
合约的所有者可以控制奖励的持续时间和奖励的总额,使用 notifyRewardAmount()
函数来设置新的奖励信息。
function notifyRewardAmount(uint256 _amount)
external
onlyOwner
updateReward(address(0))
{
if (block.timestamp >= finishAt) {
rewardRate = _amount / duration;
} else {
uint256 remainingRewards = (finishAt - block.timestamp) * rewardRate;
rewardRate = (_amount + remainingRewards) / duration;
}
require(rewardRate > 0, "reward rate = 0");
require(
rewardRate * duration <= rewardsToken.balanceOf(address(this)),
"reward amount > balance"
);
finishAt = block.timestamp + duration;
updatedAt = block.timestamp;
}
这段代码的作用是:根据奖励的结束时间和剩余的奖励量,计算新的奖励分配速率 rewardRate
,并确保奖励总额不超过合约中的奖励代币余额。这样做的目的是保持奖励的公平性和合约的稳定性。
时间控制
合约中的 lastTimeRewardApplicable()
函数确保奖励不会超时发放,奖励的计算依据的是当前时间和奖励结束时间中的较小值:
function lastTimeRewardApplicable() public view returns (uint256) {
return _min(finishAt, block.timestamp);
}
function _min(uint256 x, uint256 y) private pure returns (uint256) {
return x <= y ? x : y;
}
这意味着,如果当前时间已经超过了奖励结束时间,奖励就不再继续累积。
总结
通过 StakingRewards
合约,项目方能够激励用户质押代币,从而增强项目的生态活力。这个合约不仅实现了质押和奖励发放的基本功能,还通过动态调整奖励速率和持续时间来保持公平性。对于用户来说,参与质押挖矿既能获得奖励,又能为项目的长期发展做出贡献。而对于合约的所有者而言,通过灵活调整奖励机制,能够更好地激励社区参与并提升项目的流动性。