Skip to content

Commit 4c03dfe

Browse files
authored
Staking Contract & Interface (#268)
* first iteration of staking interface design * multiple reward tokens * test setup * tests for stake and claimRewards * test state withdraw * extension and prebuilt * NFTStake prebuilt contract * upgradeable version of extension * interface IStaking.sol * correct function calls * tests for staking extension * more tests * reentrancy guard * make functions virtual * rename to Staking721 * cleanup * update withdraw * events * comments * add base contract Staking721Base.sol * docs * docs * v3.2.3-0 * external reward token * beta releases * fix slither warnings
1 parent 7307291 commit 4c03dfe

15 files changed

+3740
-2
lines changed

contracts/base/Staking721Base.sol

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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/Staking721.sol";
8+
9+
import "../eip/interface/IERC20.sol";
10+
11+
/**
12+
* note: This is a Beta release.
13+
*
14+
* EXTENSION: Staking721
15+
*
16+
* The `Staking721Base` smart contract implements NFT staking mechanism.
17+
* Allows users to stake their ERC-721 NFTs and earn rewards in form of ERC-20 tokens.
18+
*
19+
* Following features and implementation setup must be noted:
20+
*
21+
* - ERC-721 NFTs from only one NFT collection can be staked.
22+
*
23+
* - Contract admin can choose to give out rewards by either transferring or minting the rewardToken,
24+
* which is an ERC20 token. See {_mintRewards}.
25+
*
26+
* - To implement custom logic for staking, reward calculation, etc. corresponding functions can be
27+
* overridden from the extension `Staking721`.
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 Staking721Base is ContractMetadata, Multicall, Ownable, Staking721 {
36+
/// @dev ERC20 Reward Token address. See {_mintRewards} below.
37+
address public rewardToken;
38+
39+
constructor(
40+
uint256 _timeUnit,
41+
uint256 _rewardsPerUnitTime,
42+
address _nftCollection,
43+
address _rewardToken
44+
) Staking721(_nftCollection) {
45+
_setupOwner(msg.sender);
46+
_setTimeUnit(_timeUnit);
47+
_setRewardsPerUnitTime(_rewardsPerUnitTime);
48+
49+
rewardToken = _rewardToken;
50+
}
51+
52+
/*//////////////////////////////////////////////////////////////
53+
Minting logic
54+
//////////////////////////////////////////////////////////////*/
55+
56+
/**
57+
* @dev Mint ERC20 rewards to the staker. Must override.
58+
*
59+
* @param _staker Address for which to calculated rewards.
60+
* @param _rewards Amount of tokens to be given out as reward.
61+
*
62+
*/
63+
function _mintRewards(address _staker, uint256 _rewards) internal override {
64+
// Mint or transfer reward-tokens here.
65+
// e.g.
66+
//
67+
// IERC20(rewardToken).transfer(_staker, _rewards);
68+
//
69+
// OR
70+
//
71+
// Use a mintable ERC20, such as thirdweb's `TokenERC20.sol`
72+
//
73+
// TokenERC20(rewardToken).mintTo(_staker, _rewards);
74+
// note: The staking contract should have minter role to mint tokens.
75+
}
76+
77+
/*//////////////////////////////////////////////////////////////
78+
Other Internal functions
79+
//////////////////////////////////////////////////////////////*/
80+
81+
/// @dev Returns whether staking restrictions can be set in given execution context.
82+
function _canSetStakeConditions() internal view override returns (bool) {
83+
return msg.sender == owner();
84+
}
85+
86+
/// @dev Returns whether contract metadata can be set in the given execution context.
87+
function _canSetContractURI() internal view virtual override returns (bool) {
88+
return msg.sender == owner();
89+
}
90+
91+
/// @dev Returns whether owner can be set in the given execution context.
92+
function _canSetOwner() internal view virtual override returns (bool) {
93+
return msg.sender == owner();
94+
}
95+
}

contracts/extension/Staking721.sol

+278
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
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/IERC721.sol";
6+
7+
import "./interface/IStaking.sol";
8+
9+
/**
10+
* note: This is a Beta release.
11+
*/
12+
13+
abstract contract Staking721 is ReentrancyGuard, IStaking {
14+
/*///////////////////////////////////////////////////////////////
15+
State variables
16+
//////////////////////////////////////////////////////////////*/
17+
18+
/// @dev Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc.
19+
uint256 public timeUnit;
20+
21+
///@dev Rewards accumulated per unit of time.
22+
uint256 public rewardsPerUnitTime;
23+
24+
///@dev Address of ERC721 NFT contract -- staked tokens belong to this contract.
25+
address public nftCollection;
26+
27+
///@dev Mapping from staker address to Staker struct. See {struct IStaking.Staker}.
28+
mapping(address => Staker) public stakers;
29+
30+
/// @dev Mapping from staked token-id to staker address.
31+
mapping(uint256 => address) public stakerAddress;
32+
33+
/// @dev List of accounts that have staked their NFTs.
34+
address[] public stakersArray;
35+
36+
constructor(address _nftCollection) ReentrancyGuard() {
37+
require(address(_nftCollection) != address(0), "collection address 0");
38+
nftCollection = _nftCollection;
39+
}
40+
41+
/*///////////////////////////////////////////////////////////////
42+
External/Public Functions
43+
//////////////////////////////////////////////////////////////*/
44+
45+
/**
46+
* @notice Stake ERC721 Tokens.
47+
*
48+
* @dev See {_stake}. Override that to implement custom logic.
49+
*
50+
* @param _tokenIds List of tokens to stake.
51+
*/
52+
function stake(uint256[] calldata _tokenIds) external nonReentrant {
53+
_stake(_tokenIds);
54+
}
55+
56+
/**
57+
* @notice Withdraw staked tokens.
58+
*
59+
* @dev See {_withdraw}. Override that to implement custom logic.
60+
*
61+
* @param _tokenIds List of tokens to withdraw.
62+
*/
63+
function withdraw(uint256[] calldata _tokenIds) external nonReentrant {
64+
_withdraw(_tokenIds);
65+
}
66+
67+
/**
68+
* @notice Claim accumulated rewards.
69+
*
70+
* @dev See {_claimRewards}. Override that to implement custom logic.
71+
* See {_calculateRewards} for reward-calculation logic.
72+
*/
73+
function claimRewards() external nonReentrant {
74+
_claimRewards();
75+
}
76+
77+
/**
78+
* @notice Set time unit. Set as a number of seconds.
79+
* Could be specified as -- x * 1 hours, x * 1 days, etc.
80+
*
81+
* @dev Only admin/authorized-account can call it.
82+
*
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 x rewards per second/per day/etc based on time-unit.
102+
*
103+
* @dev Only admin/authorized-account can call it.
104+
*
105+
*
106+
* @param _rewardsPerUnitTime New rewards per unit time.
107+
*/
108+
function setRewardsPerUnitTime(uint256 _rewardsPerUnitTime) external virtual {
109+
if (!_canSetStakeConditions()) {
110+
revert("Not authorized");
111+
}
112+
113+
_updateUnclaimedRewardsForAll();
114+
115+
uint256 currentRewardsPerUnitTime = rewardsPerUnitTime;
116+
_setRewardsPerUnitTime(_rewardsPerUnitTime);
117+
118+
emit UpdatedRewardsPerUnitTime(currentRewardsPerUnitTime, _rewardsPerUnitTime);
119+
}
120+
121+
/**
122+
* @notice View amount staked and total rewards for a user.
123+
*
124+
* @param _staker Address for which to calculated rewards.
125+
*/
126+
function getStakeInfo(address _staker) public view virtual returns (uint256 _tokensStaked, uint256 _rewards) {
127+
_tokensStaked = stakers[_staker].amountStaked;
128+
_rewards = _availableRewards(_staker);
129+
}
130+
131+
/*///////////////////////////////////////////////////////////////
132+
Internal Functions
133+
//////////////////////////////////////////////////////////////*/
134+
135+
/// @dev Staking logic. Override to add custom logic.
136+
function _stake(uint256[] calldata _tokenIds) internal virtual {
137+
uint256 len = _tokenIds.length;
138+
require(len != 0, "Staking 0 tokens");
139+
140+
if (stakers[msg.sender].amountStaked > 0) {
141+
_updateUnclaimedRewardsForStaker(msg.sender);
142+
} else {
143+
stakersArray.push(msg.sender);
144+
stakers[msg.sender].timeOfLastUpdate = block.timestamp;
145+
}
146+
for (uint256 i = 0; i < len; ++i) {
147+
require(IERC721(nftCollection).ownerOf(_tokenIds[i]) == msg.sender, "Not owner");
148+
IERC721(nftCollection).transferFrom(msg.sender, address(this), _tokenIds[i]);
149+
stakerAddress[_tokenIds[i]] = msg.sender;
150+
}
151+
stakers[msg.sender].amountStaked += len;
152+
153+
emit TokensStaked(msg.sender, _tokenIds);
154+
}
155+
156+
/// @dev Withdraw logic. Override to add custom logic.
157+
function _withdraw(uint256[] calldata _tokenIds) internal virtual {
158+
uint256 _amountStaked = stakers[msg.sender].amountStaked;
159+
uint256 len = _tokenIds.length;
160+
require(len != 0, "Withdrawing 0 tokens");
161+
require(_amountStaked >= len, "Withdrawing more than staked");
162+
163+
_updateUnclaimedRewardsForStaker(msg.sender);
164+
165+
if (_amountStaked == len) {
166+
for (uint256 i = 0; i < stakersArray.length; ++i) {
167+
if (stakersArray[i] == msg.sender) {
168+
stakersArray[i] = stakersArray[stakersArray.length - 1];
169+
stakersArray.pop();
170+
}
171+
}
172+
}
173+
stakers[msg.sender].amountStaked -= len;
174+
175+
for (uint256 i = 0; i < len; ++i) {
176+
require(stakerAddress[_tokenIds[i]] == msg.sender, "Not staker");
177+
stakerAddress[_tokenIds[i]] = address(0);
178+
IERC721(nftCollection).transferFrom(address(this), msg.sender, _tokenIds[i]);
179+
}
180+
181+
emit TokensWithdrawn(msg.sender, _tokenIds);
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 _user) internal view virtual returns (uint256 _rewards) {
200+
if (stakers[_user].amountStaked == 0) {
201+
_rewards = stakers[_user].unclaimedRewards;
202+
} else {
203+
_rewards = stakers[_user].unclaimedRewards + _calculateRewards(_user);
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 user = _stakers[i];
213+
214+
uint256 rewards = _calculateRewards(user);
215+
stakers[user].unclaimedRewards += rewards;
216+
stakers[user].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 rewards per unit time.
233+
function _setRewardsPerUnitTime(uint256 _rewardsPerUnitTime) internal virtual {
234+
rewardsPerUnitTime = _rewardsPerUnitTime;
235+
}
236+
237+
/// @dev Reward calculation logic. Override to implement custom logic.
238+
function _calculateRewards(address _staker) internal view virtual returns (uint256 _rewards) {
239+
Staker memory staker = stakers[_staker];
240+
_rewards = ((((block.timestamp - staker.timeOfLastUpdate) * staker.amountStaked) * rewardsPerUnitTime) /
241+
timeUnit);
242+
}
243+
244+
/**
245+
* @dev Mint ERC20 rewards to the staker. Must override.
246+
*
247+
* @param _staker Address for which to calculated rewards.
248+
* @param _rewards Amount of tokens to be given out as reward.
249+
*
250+
* For example, override as below to mint ERC20 rewards:
251+
*
252+
* ```
253+
* function _mintRewards(address _staker, uint256 _rewards) internal override {
254+
*
255+
* IERC20(rewardTokenAddress)._mint(_staker, _rewards);
256+
*
257+
* }
258+
* ```
259+
*/
260+
function _mintRewards(address _staker, uint256 _rewards) internal virtual;
261+
262+
/**
263+
* @dev Returns whether staking restrictions can be set in given execution context.
264+
* Must override.
265+
*
266+
*
267+
* For example, override as below to restrict access to admin:
268+
*
269+
* ```
270+
* function _canSetStakeConditions() internal override {
271+
*
272+
* return msg.sender == adminAddress;
273+
*
274+
* }
275+
* ```
276+
*/
277+
function _canSetStakeConditions() internal view virtual returns (bool);
278+
}

0 commit comments

Comments
 (0)