Skip to content

Commit db49040

Browse files
authored
ERC20 staking prebuilt and extension (#286)
* ERC20 staking extension and prebuilt * tests for erc20 staking prebuilt * base contract * reward calculation in BPS * reward ratio * withdraw function to prevent locking of reward tokens
1 parent 5c9198d commit db49040

12 files changed

+1343
-15
lines changed

contracts/base/Staking20Base.sol

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.0;
3+
4+
import "../extension/ContractMetadata.sol";
5+
import "../extension/Multicall.sol";
6+
import "../extension/Ownable.sol";
7+
import "../extension/Staking20.sol";
8+
9+
import "../eip/interface/IERC20.sol";
10+
11+
/**
12+
* note: This is a Beta release.
13+
*
14+
* EXTENSION: Staking20
15+
*
16+
* The `Staking20Base` smart contract implements Token staking mechanism.
17+
* Allows users to stake their ERC-20 Tokens and earn rewards in form of another ERC-20 tokens.
18+
*
19+
* Following features and implementation setup must be noted:
20+
*
21+
* - ERC-20 Tokens from only one contract can be staked.
22+
*
23+
* - Contract admin can choose to give out rewards by either transferring or minting the rewardToken,
24+
* which is ideally a different ERC20 token. See {_mintRewards}.
25+
*
26+
* - To implement custom logic for staking, reward calculation, etc. corresponding functions can be
27+
* overridden from the extension `Staking20`.
28+
*
29+
* - Ownership of the contract, with the ability to restrict certain functions to
30+
* only be called by the contract's owner.
31+
*
32+
* - Multicall capability to perform multiple actions atomically.
33+
*
34+
*/
35+
contract Staking20Base is ContractMetadata, Multicall, Ownable, Staking20 {
36+
/// @dev ERC20 Reward Token address. See {_mintRewards} below.
37+
address public rewardToken;
38+
39+
constructor(
40+
uint256 _timeUnit,
41+
uint256 _rewardRatioNumerator,
42+
uint256 _rewardRatioDenominator,
43+
address _stakingToken,
44+
address _rewardToken
45+
) Staking20(_stakingToken) {
46+
_setupOwner(msg.sender);
47+
_setTimeUnit(_timeUnit);
48+
_setRewardRatio(_rewardRatioNumerator, _rewardRatioDenominator);
49+
50+
require(_rewardToken != _stakingToken, "Reward Token and Staking Token can't be same.");
51+
rewardToken = _rewardToken;
52+
}
53+
54+
/*//////////////////////////////////////////////////////////////
55+
Minting logic
56+
//////////////////////////////////////////////////////////////*/
57+
58+
/**
59+
* @dev Mint ERC20 rewards to the staker. Must override.
60+
*
61+
* @param _staker Address for which to calculated rewards.
62+
* @param _rewards Amount of tokens to be given out as reward.
63+
*
64+
*/
65+
function _mintRewards(address _staker, uint256 _rewards) internal override {
66+
// Mint or transfer reward-tokens here.
67+
// e.g.
68+
//
69+
// IERC20(rewardToken).transfer(_staker, _rewards);
70+
//
71+
// OR
72+
//
73+
// Use a mintable ERC20, such as thirdweb's `TokenERC20.sol`
74+
//
75+
// TokenERC20(rewardToken).mintTo(_staker, _rewards);
76+
// note: The staking contract should have minter role to mint tokens.
77+
}
78+
79+
/*//////////////////////////////////////////////////////////////
80+
Other Internal functions
81+
//////////////////////////////////////////////////////////////*/
82+
83+
/// @dev Returns whether staking restrictions can be set in given execution context.
84+
function _canSetStakeConditions() internal view override returns (bool) {
85+
return msg.sender == owner();
86+
}
87+
88+
/// @dev Returns whether contract metadata can be set in the given execution context.
89+
function _canSetContractURI() internal view virtual override returns (bool) {
90+
return msg.sender == owner();
91+
}
92+
93+
/// @dev Returns whether owner can be set in the given execution context.
94+
function _canSetOwner() internal view virtual override returns (bool) {
95+
return msg.sender == owner();
96+
}
97+
}

contracts/extension/Staking20.sol

+281
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.11;
3+
4+
import "../openzeppelin-presets/security/ReentrancyGuard.sol";
5+
import "../eip/interface/IERC20.sol";
6+
import "../lib/CurrencyTransferLib.sol";
7+
8+
import "./interface/IStaking20.sol";
9+
10+
/**
11+
* note: This is a Beta release.
12+
*/
13+
14+
abstract contract Staking20 is ReentrancyGuard, IStaking20 {
15+
/*///////////////////////////////////////////////////////////////
16+
State variables / Mappings
17+
//////////////////////////////////////////////////////////////*/
18+
19+
///@dev Address of ERC20 contract -- staked tokens belong to this contract.
20+
address public token;
21+
22+
/// @dev Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc.
23+
uint256 public timeUnit;
24+
25+
///@dev Rewards ratio is the number of reward tokens for a number of staked tokens, per unit of time.
26+
uint256 public rewardRatioNumerator;
27+
28+
///@dev Rewards ratio is the number of reward tokens for a number of staked tokens, per unit of time.
29+
uint256 public rewardRatioDenominator;
30+
31+
///@dev Mapping staker address to Staker struct. See {struct IStaking20.Staker}.
32+
mapping(address => Staker) public stakers;
33+
34+
/// @dev List of accounts that have staked that token-id.
35+
address[] public stakersArray;
36+
37+
constructor(address _token) ReentrancyGuard() {
38+
require(address(_token) != address(0), "address 0");
39+
token = _token;
40+
}
41+
42+
/*///////////////////////////////////////////////////////////////
43+
External/Public Functions
44+
//////////////////////////////////////////////////////////////*/
45+
46+
/**
47+
* @notice Stake ERC20 Tokens.
48+
*
49+
* @dev See {_stake}. Override that to implement custom logic.
50+
*
51+
* @param _amount Amount to stake.
52+
*/
53+
function stake(uint256 _amount) external nonReentrant {
54+
_stake(_amount);
55+
}
56+
57+
/**
58+
* @notice Withdraw staked ERC20 tokens.
59+
*
60+
* @dev See {_withdraw}. Override that to implement custom logic.
61+
*
62+
* @param _amount Amount to withdraw.
63+
*/
64+
function withdraw(uint256 _amount) external nonReentrant {
65+
_withdraw(_amount);
66+
}
67+
68+
/**
69+
* @notice Claim accumulated rewards.
70+
*
71+
* @dev See {_claimRewards}. Override that to implement custom logic.
72+
* See {_calculateRewards} for reward-calculation logic.
73+
*/
74+
function claimRewards() external nonReentrant {
75+
_claimRewards();
76+
}
77+
78+
/**
79+
* @notice Set time unit. Set as a number of seconds.
80+
* Could be specified as -- x * 1 hours, x * 1 days, etc.
81+
*
82+
* @dev Only admin/authorized-account can call it.
83+
*
84+
* @param _timeUnit New time unit.
85+
*/
86+
function setTimeUnit(uint256 _timeUnit) external virtual {
87+
if (!_canSetStakeConditions()) {
88+
revert("Not authorized");
89+
}
90+
91+
_updateUnclaimedRewardsForAll();
92+
93+
uint256 currentTimeUnit = timeUnit;
94+
_setTimeUnit(_timeUnit);
95+
96+
emit UpdatedTimeUnit(currentTimeUnit, _timeUnit);
97+
}
98+
99+
/**
100+
* @notice Set rewards per unit of time.
101+
* Interpreted as (numerator/denominator) rewards per second/per day/etc based on time-unit.
102+
*
103+
* For e.g., ratio of 1/20 would mean 1 reward token for every 20 tokens staked.
104+
*
105+
* @dev Only admin/authorized-account can call it.
106+
*
107+
* @param _numerator Reward ratio numerator.
108+
* @param _denominator Reward ratio denominator.
109+
*/
110+
function setRewardRatio(uint256 _numerator, uint256 _denominator) external virtual {
111+
if (!_canSetStakeConditions()) {
112+
revert("Not authorized");
113+
}
114+
115+
_updateUnclaimedRewardsForAll();
116+
117+
uint256 currentNumerator = rewardRatioNumerator;
118+
uint256 currentDenominator = rewardRatioDenominator;
119+
_setRewardRatio(_numerator, _denominator);
120+
121+
emit UpdatedRewardRatio(currentNumerator, _numerator, currentDenominator, _denominator);
122+
}
123+
124+
/**
125+
* @notice View amount staked and rewards for a user.
126+
*
127+
* @param _staker Address for which to calculated rewards.
128+
* @return _tokensStaked Amount of tokens staked.
129+
* @return _rewards Available reward amount.
130+
*/
131+
function getStakeInfo(address _staker) public view virtual returns (uint256 _tokensStaked, uint256 _rewards) {
132+
_tokensStaked = stakers[_staker].amountStaked;
133+
_rewards = _availableRewards(_staker);
134+
}
135+
136+
/*///////////////////////////////////////////////////////////////
137+
Internal Functions
138+
//////////////////////////////////////////////////////////////*/
139+
140+
/// @dev Staking logic. Override to add custom logic.
141+
function _stake(uint256 _amount) internal virtual {
142+
require(_amount != 0, "Staking 0 tokens");
143+
address _token = token;
144+
145+
if (stakers[msg.sender].amountStaked > 0) {
146+
_updateUnclaimedRewardsForStaker(msg.sender);
147+
} else {
148+
stakersArray.push(msg.sender);
149+
stakers[msg.sender].timeOfLastUpdate = block.timestamp;
150+
}
151+
152+
CurrencyTransferLib.transferCurrency(_token, msg.sender, address(this), _amount);
153+
154+
stakers[msg.sender].amountStaked += _amount;
155+
156+
emit TokensStaked(msg.sender, _amount);
157+
}
158+
159+
/// @dev Withdraw logic. Override to add custom logic.
160+
function _withdraw(uint256 _amount) internal virtual {
161+
uint256 _amountStaked = stakers[msg.sender].amountStaked;
162+
require(_amount != 0, "Withdrawing 0 tokens");
163+
require(_amountStaked >= _amount, "Withdrawing more than staked");
164+
165+
_updateUnclaimedRewardsForStaker(msg.sender);
166+
167+
if (_amountStaked == _amount) {
168+
address[] memory _stakersArray = stakersArray;
169+
for (uint256 i = 0; i < _stakersArray.length; ++i) {
170+
if (_stakersArray[i] == msg.sender) {
171+
stakersArray[i] = stakersArray[_stakersArray.length - 1];
172+
stakersArray.pop();
173+
break;
174+
}
175+
}
176+
}
177+
stakers[msg.sender].amountStaked -= _amount;
178+
179+
CurrencyTransferLib.transferCurrency(token, address(this), msg.sender, _amount);
180+
181+
emit TokensWithdrawn(msg.sender, _amount);
182+
}
183+
184+
/// @dev Logic for claiming rewards. Override to add custom logic.
185+
function _claimRewards() internal virtual {
186+
uint256 rewards = stakers[msg.sender].unclaimedRewards + _calculateRewards(msg.sender);
187+
188+
require(rewards != 0, "No rewards");
189+
190+
stakers[msg.sender].timeOfLastUpdate = block.timestamp;
191+
stakers[msg.sender].unclaimedRewards = 0;
192+
193+
_mintRewards(msg.sender, rewards);
194+
195+
emit RewardsClaimed(msg.sender, rewards);
196+
}
197+
198+
/// @dev View available rewards for a user.
199+
function _availableRewards(address _staker) internal view virtual returns (uint256 _rewards) {
200+
if (stakers[_staker].amountStaked == 0) {
201+
_rewards = stakers[_staker].unclaimedRewards;
202+
} else {
203+
_rewards = stakers[_staker].unclaimedRewards + _calculateRewards(_staker);
204+
}
205+
}
206+
207+
/// @dev Update unclaimed rewards for all users. Called when setting timeUnit or rewardsPerUnitTime.
208+
function _updateUnclaimedRewardsForAll() internal virtual {
209+
address[] memory _stakers = stakersArray;
210+
uint256 len = _stakers.length;
211+
for (uint256 i = 0; i < len; ++i) {
212+
address _staker = _stakers[i];
213+
214+
uint256 rewards = _calculateRewards(_staker);
215+
stakers[_staker].unclaimedRewards += rewards;
216+
stakers[_staker].timeOfLastUpdate = block.timestamp;
217+
}
218+
}
219+
220+
/// @dev Update unclaimed rewards for a users. Called for every state change for a user.
221+
function _updateUnclaimedRewardsForStaker(address _staker) internal virtual {
222+
uint256 rewards = _calculateRewards(_staker);
223+
stakers[_staker].unclaimedRewards += rewards;
224+
stakers[_staker].timeOfLastUpdate = block.timestamp;
225+
}
226+
227+
/// @dev Set time unit in seconds.
228+
function _setTimeUnit(uint256 _timeUnit) internal virtual {
229+
timeUnit = _timeUnit;
230+
}
231+
232+
/// @dev Set reward ratio per unit time.
233+
function _setRewardRatio(uint256 _numerator, uint256 _denominator) internal virtual {
234+
require(_denominator != 0, "divide by 0");
235+
rewardRatioNumerator = _numerator;
236+
rewardRatioDenominator = _denominator;
237+
}
238+
239+
/// @dev Reward calculation logic. Override to implement custom logic.
240+
function _calculateRewards(address _staker) internal view virtual returns (uint256 _rewards) {
241+
Staker memory staker = stakers[_staker];
242+
243+
_rewards = (((((block.timestamp - staker.timeOfLastUpdate) * staker.amountStaked) * rewardRatioNumerator) /
244+
timeUnit) / rewardRatioDenominator);
245+
}
246+
247+
/**
248+
* @dev Mint/Transfer ERC20 rewards to the staker. Must override.
249+
*
250+
* @param _staker Address for which to calculated rewards.
251+
* @param _rewards Amount of tokens to be given out as reward.
252+
*
253+
* For example, override as below to mint ERC20 rewards:
254+
*
255+
* ```
256+
* function _mintRewards(address _staker, uint256 _rewards) internal override {
257+
*
258+
* TokenERC20(rewardTokenAddress).mintTo(_staker, _rewards);
259+
*
260+
* }
261+
* ```
262+
*/
263+
function _mintRewards(address _staker, uint256 _rewards) internal virtual;
264+
265+
/**
266+
* @dev Returns whether staking restrictions can be set in given execution context.
267+
* Must override.
268+
*
269+
*
270+
* For example, override as below to restrict access to admin:
271+
*
272+
* ```
273+
* function _canSetStakeConditions() internal override {
274+
*
275+
* return msg.sender == adminAddress;
276+
*
277+
* }
278+
* ```
279+
*/
280+
function _canSetStakeConditions() internal view virtual returns (bool);
281+
}

0 commit comments

Comments
 (0)