# Solidity 如何实现质押和奖励合约

## 简单质押奖励合约

Unipool 质押合约影响巨大，k06a 是世界级的开发者，但出于教育目的，我决定以更清晰的方式再次实现该算法。

Simple Rewards合约允许用户 "押注"一个 `stakingToken`，并获得一个 `rewardsToken`，用户必须 "领取（`claim`）"这个 `rewardsToken`。用户可以随时"提取 "质押，但奖励也会停止累积。这是一个无权限合约，将在部署时定义的时间间隔内分配奖励。仅此而已。

<p align="center">SimpleRewards.sol 中的函数</p>

### 计算奖励的数学

``currentRewardsPerToken = accumulatedRewardsPerToken + elapsed * rate  / totalStaked``

``````currentUserRewards =
accumulatedUserRewards +
userStake * (userRecordedRewardsPerToken - currentRewardsPerToken)``````

### 实现

``````
pragma solidity ^0.8.0;

import { ERC20 } from "@solmate/tokens/ERC20.sol";
import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol";

/// @notice Permissionless staking contract for a single rewards program.
/// From the start of the program, to the end of the program, a fixed amount of rewards tokens will be distributed among stakers.
/// The rate at which rewards are distributed is constant over time, but proportional to the amount of tokens staked by each staker.
/// The contract expects to have received enough rewards tokens by the time they are claimable. The rewards tokens can only be recovered by claiming stakers.
/// This is a rewriting of [Unipool.sol](https://github.com/k06a/Unipool/blob/master/contracts/Unipool.sol), modified for clarity and simplified.
/// Careful if using non-standard ERC20 tokens, as they might break things.

contract SimpleRewards {
using SafeTransferLib for ERC20;
using Cast for uint256;

event Staked(address user, uint256 amount);
event Unstaked(address user, uint256 amount);
event Claimed(address user, uint256 amount);
event RewardsPerTokenUpdated(uint256 accumulated);
event UserRewardsUpdated(address user, uint256 rewards, uint256 checkpoint);

struct RewardsPerToken {
uint128 accumulated;                                        // Accumulated rewards per token for the interval, scaled up by 1e18
uint128 lastUpdated;                                        // Last time the rewards per token accumulator was updated
}

struct UserRewards {
uint128 accumulated;                                        // Accumulated rewards for the user until the checkpoint
uint128 checkpoint;                                         // RewardsPerToken the last time the user rewards were updated
}

ERC20 public immutable stakingToken;                            // Token to be staked
uint256 public totalStaked;                                     // Total amount staked
mapping (address => uint256) public userStake;                  // Amount staked per user

ERC20 public immutable rewardsToken;                            // Token used as rewards
uint256 public immutable rewardsRate;                           // Wei rewarded per second among all token holders
uint256 public immutable rewardsStart;                          // Start of the rewards program
uint256 public immutable rewardsEnd;                            // End of the rewards program
RewardsPerToken public rewardsPerToken;                         // Accumulator to track rewards per token
mapping (address => UserRewards) public accumulatedRewards;     // Rewards accumulated per user

constructor(ERC20 stakingToken_, ERC20 rewardsToken_, uint256 rewardsStart_, uint256 rewardsEnd_, uint256 totalRewards)
{
stakingToken = stakingToken_;
rewardsToken = rewardsToken_;
rewardsStart = rewardsStart_;
rewardsEnd = rewardsEnd_;
rewardsRate = totalRewards / (rewardsEnd_ - rewardsStart_); // The contract will fail to deploy if end &lt;= start, as it should
rewardsPerToken.lastUpdated = rewardsStart_.u128();
}

/// @notice Update the rewards per token accumulator according to the rate, the time elapsed since the last update, and the current total staked amount.
function _calculateRewardsPerToken(RewardsPerToken memory rewardsPerTokenIn) internal view returns(RewardsPerToken memory) {
RewardsPerToken memory rewardsPerTokenOut = RewardsPerToken(rewardsPerTokenIn.accumulated, rewardsPerTokenIn.lastUpdated);
uint256 totalStaked_ = totalStaked;

// No changes if the program hasn't started
if (block.timestamp &lt; rewardsStart) return rewardsPerTokenOut;

// Stop accumulating at the end of the rewards interval
uint256 updateTime = block.timestamp &lt; rewardsEnd ? block.timestamp : rewardsEnd;
uint256 elapsed = updateTime - rewardsPerTokenIn.lastUpdated;

// No changes if no time has passed
if (elapsed == 0) return rewardsPerTokenOut;
rewardsPerTokenOut.lastUpdated = updateTime.u128();

// If there are no stakers we just change the last update time, the rewards for intervals without stakers are not accumulated
if (totalStaked == 0) return rewardsPerTokenOut;

// Calculate and update the new value of the accumulator.
rewardsPerTokenOut.accumulated = (rewardsPerT...``````

0x9e64...7c84