Skip to content

Commit

Permalink
Staking - ERC1155 (#282)
Browse files Browse the repository at this point in the history
* staking for ERC1155

* tests for ERC1155 staking prebuilt

* more tests

* view functions

* code comments, docs
  • Loading branch information
kumaryash90 authored Nov 23, 2022
1 parent 95e1be8 commit af41ffb
Show file tree
Hide file tree
Showing 22 changed files with 4,697 additions and 68 deletions.
412 changes: 412 additions & 0 deletions contracts/extension/Staking1155.sol

Large diffs are not rendered by default.

414 changes: 414 additions & 0 deletions contracts/extension/Staking1155Upgradeable.sol

Large diffs are not rendered by default.

69 changes: 57 additions & 12 deletions contracts/extension/Staking721.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,25 @@ import "./interface/IStaking.sol";

abstract contract Staking721 is ReentrancyGuard, IStaking {
/*///////////////////////////////////////////////////////////////
State variables
State variables / Mappings
//////////////////////////////////////////////////////////////*/

///@dev Address of ERC721 NFT contract -- staked tokens belong to this contract.
address public nftCollection;

/// @dev Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc.
uint256 public timeUnit;

///@dev Rewards accumulated per unit of time.
uint256 public rewardsPerUnitTime;

///@dev Address of ERC721 NFT contract -- staked tokens belong to this contract.
address public nftCollection;
///@dev List of token-ids ever staked.
uint256[] public indexedTokens;

///@dev Mapping from token-id to whether it is indexed or not.
mapping(uint256 => bool) public isIndexed;

///@dev Mapping from staker address to Staker struct. See {struct IStaking.Staker}.
///@dev Mapping from staker address to Staker struct. See {struct IStaking721.Staker}.
mapping(address => Staker) public stakers;

/// @dev Mapping from staked token-id to staker address.
Expand Down Expand Up @@ -121,10 +127,35 @@ abstract contract Staking721 is ReentrancyGuard, IStaking {
/**
* @notice View amount staked and total rewards for a user.
*
* @param _staker Address for which to calculated rewards.
* @param _staker Address for which to calculated rewards.
* @return _tokensStaked List of token-ids staked by staker.
* @return _rewards Available reward amount.
*/
function getStakeInfo(address _staker) public view virtual returns (uint256 _tokensStaked, uint256 _rewards) {
_tokensStaked = stakers[_staker].amountStaked;
function getStakeInfo(address _staker)
public
view
virtual
returns (uint256[] memory _tokensStaked, uint256 _rewards)
{
uint256[] memory _indexedTokens = indexedTokens;
bool[] memory _isStakerToken = new bool[](_indexedTokens.length);
uint256 indexedTokenCount = _indexedTokens.length;
uint256 stakerTokenCount = 0;

for (uint256 i = 0; i < indexedTokenCount; i++) {
_isStakerToken[i] = stakerAddress[_indexedTokens[i]] == _staker;
if (_isStakerToken[i]) stakerTokenCount += 1;
}

_tokensStaked = new uint256[](stakerTokenCount);
uint256 count = 0;
for (uint256 i = 0; i < indexedTokenCount; i++) {
if (_isStakerToken[i]) {
_tokensStaked[count] = _indexedTokens[i];
count += 1;
}
}

_rewards = _availableRewards(_staker);
}

Expand All @@ -137,16 +168,28 @@ abstract contract Staking721 is ReentrancyGuard, IStaking {
uint256 len = _tokenIds.length;
require(len != 0, "Staking 0 tokens");

address _nftCollection = nftCollection;

if (stakers[msg.sender].amountStaked > 0) {
_updateUnclaimedRewardsForStaker(msg.sender);
} else {
stakersArray.push(msg.sender);
stakers[msg.sender].timeOfLastUpdate = block.timestamp;
}
for (uint256 i = 0; i < len; ++i) {
require(IERC721(nftCollection).ownerOf(_tokenIds[i]) == msg.sender, "Not owner");
IERC721(nftCollection).transferFrom(msg.sender, address(this), _tokenIds[i]);
require(
IERC721(_nftCollection).ownerOf(_tokenIds[i]) == msg.sender &&
(IERC721(_nftCollection).getApproved(_tokenIds[i]) == address(this) ||
IERC721(_nftCollection).isApprovedForAll(msg.sender, address(this))),
"Not owned or approved"
);
IERC721(_nftCollection).transferFrom(msg.sender, address(this), _tokenIds[i]);
stakerAddress[_tokenIds[i]] = msg.sender;

if (!isIndexed[_tokenIds[i]]) {
isIndexed[_tokenIds[i]] = true;
indexedTokens.push(_tokenIds[i]);
}
}
stakers[msg.sender].amountStaked += len;

Expand All @@ -160,6 +203,8 @@ abstract contract Staking721 is ReentrancyGuard, IStaking {
require(len != 0, "Withdrawing 0 tokens");
require(_amountStaked >= len, "Withdrawing more than staked");

address _nftCollection = nftCollection;

_updateUnclaimedRewardsForStaker(msg.sender);

if (_amountStaked == len) {
Expand All @@ -175,7 +220,7 @@ abstract contract Staking721 is ReentrancyGuard, IStaking {
for (uint256 i = 0; i < len; ++i) {
require(stakerAddress[_tokenIds[i]] == msg.sender, "Not staker");
stakerAddress[_tokenIds[i]] = address(0);
IERC721(nftCollection).transferFrom(address(this), msg.sender, _tokenIds[i]);
IERC721(_nftCollection).transferFrom(address(this), msg.sender, _tokenIds[i]);
}

emit TokensWithdrawn(msg.sender, _tokenIds);
Expand Down Expand Up @@ -242,7 +287,7 @@ abstract contract Staking721 is ReentrancyGuard, IStaking {
}

/**
* @dev Mint ERC20 rewards to the staker. Must override.
* @dev Mint/Transfer ERC20 rewards to the staker. Must override.
*
* @param _staker Address for which to calculated rewards.
* @param _rewards Amount of tokens to be given out as reward.
Expand All @@ -252,7 +297,7 @@ abstract contract Staking721 is ReentrancyGuard, IStaking {
* ```
* function _mintRewards(address _staker, uint256 _rewards) internal override {
*
* IERC20(rewardTokenAddress)._mint(_staker, _rewards);
* TokenERC20(rewardTokenAddress).mintTo(_staker, _rewards);
*
* }
* ```
Expand Down
69 changes: 57 additions & 12 deletions contracts/extension/Staking721Upgradeable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,25 @@ import "./interface/IStaking.sol";

abstract contract Staking721Upgradeable is ReentrancyGuardUpgradeable, IStaking {
/*///////////////////////////////////////////////////////////////
State variables
State variables / Mappings
//////////////////////////////////////////////////////////////*/

///@dev Address of ERC721 NFT contract -- staked tokens belong to this contract.
address public nftCollection;

/// @dev Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc.
uint256 public timeUnit;

///@dev Rewards accumulated per unit of time.
uint256 public rewardsPerUnitTime;

///@dev Address of ERC721 NFT contract -- staked tokens belong to this contract.
address public nftCollection;
///@dev List of token-ids ever staked.
uint256[] public indexedTokens;

///@dev Mapping from token-id to whether it is indexed or not.
mapping(uint256 => bool) public isIndexed;

///@dev Mapping from staker address to Staker struct. See {struct IStaking.Staker}.
///@dev Mapping from staker address to Staker struct. See {struct IStaking721.Staker}.
mapping(address => Staker) public stakers;

/// @dev Mapping from staked token-id to staker address.
Expand Down Expand Up @@ -123,10 +129,35 @@ abstract contract Staking721Upgradeable is ReentrancyGuardUpgradeable, IStaking
/**
* @notice View amount staked and total rewards for a user.
*
* @param _staker Address for which to calculated rewards.
* @param _staker Address for which to calculated rewards.
* @return _tokensStaked List of token-ids staked by staker.
* @return _rewards Available reward amount.
*/
function getStakeInfo(address _staker) public view virtual returns (uint256 _tokensStaked, uint256 _rewards) {
_tokensStaked = stakers[_staker].amountStaked;
function getStakeInfo(address _staker)
public
view
virtual
returns (uint256[] memory _tokensStaked, uint256 _rewards)
{
uint256[] memory _indexedTokens = indexedTokens;
bool[] memory _isStakerToken = new bool[](_indexedTokens.length);
uint256 indexedTokenCount = _indexedTokens.length;
uint256 stakerTokenCount = 0;

for (uint256 i = 0; i < indexedTokenCount; i++) {
_isStakerToken[i] = stakerAddress[_indexedTokens[i]] == _staker;
if (_isStakerToken[i]) stakerTokenCount += 1;
}

_tokensStaked = new uint256[](stakerTokenCount);
uint256 count = 0;
for (uint256 i = 0; i < indexedTokenCount; i++) {
if (_isStakerToken[i]) {
_tokensStaked[count] = _indexedTokens[i];
count += 1;
}
}

_rewards = _availableRewards(_staker);
}

Expand All @@ -139,16 +170,28 @@ abstract contract Staking721Upgradeable is ReentrancyGuardUpgradeable, IStaking
uint256 len = _tokenIds.length;
require(len != 0, "Staking 0 tokens");

address _nftCollection = nftCollection;

if (stakers[msg.sender].amountStaked > 0) {
_updateUnclaimedRewardsForStaker(msg.sender);
} else {
stakersArray.push(msg.sender);
stakers[msg.sender].timeOfLastUpdate = block.timestamp;
}
for (uint256 i = 0; i < len; ++i) {
require(IERC721(nftCollection).ownerOf(_tokenIds[i]) == msg.sender, "Not owner");
IERC721(nftCollection).transferFrom(msg.sender, address(this), _tokenIds[i]);
require(
IERC721(_nftCollection).ownerOf(_tokenIds[i]) == msg.sender &&
(IERC721(_nftCollection).getApproved(_tokenIds[i]) == address(this) ||
IERC721(_nftCollection).isApprovedForAll(msg.sender, address(this))),
"Not owned or approved"
);
IERC721(_nftCollection).transferFrom(msg.sender, address(this), _tokenIds[i]);
stakerAddress[_tokenIds[i]] = msg.sender;

if (!isIndexed[_tokenIds[i]]) {
isIndexed[_tokenIds[i]] = true;
indexedTokens.push(_tokenIds[i]);
}
}
stakers[msg.sender].amountStaked += len;

Expand All @@ -162,6 +205,8 @@ abstract contract Staking721Upgradeable is ReentrancyGuardUpgradeable, IStaking
require(len != 0, "Withdrawing 0 tokens");
require(_amountStaked >= len, "Withdrawing more than staked");

address _nftCollection = nftCollection;

_updateUnclaimedRewardsForStaker(msg.sender);

if (_amountStaked == len) {
Expand All @@ -177,7 +222,7 @@ abstract contract Staking721Upgradeable is ReentrancyGuardUpgradeable, IStaking
for (uint256 i = 0; i < len; ++i) {
require(stakerAddress[_tokenIds[i]] == msg.sender, "Not staker");
stakerAddress[_tokenIds[i]] = address(0);
IERC721(nftCollection).transferFrom(address(this), msg.sender, _tokenIds[i]);
IERC721(_nftCollection).transferFrom(address(this), msg.sender, _tokenIds[i]);
}

emit TokensWithdrawn(msg.sender, _tokenIds);
Expand Down Expand Up @@ -244,7 +289,7 @@ abstract contract Staking721Upgradeable is ReentrancyGuardUpgradeable, IStaking
}

/**
* @dev Mint ERC20 rewards to the staker. Must override.
* @dev Mint/Transfer ERC20 rewards to the staker. Must override.
*
* @param _staker Address for which to calculated rewards.
* @param _rewards Amount of tokens to be given out as reward.
Expand All @@ -254,7 +299,7 @@ abstract contract Staking721Upgradeable is ReentrancyGuardUpgradeable, IStaking
* ```
* function _mintRewards(address _staker, uint256 _rewards) internal override {
*
* IERC20(rewardTokenAddress)._mint(_staker, _rewards);
* TokenERC20(rewardTokenAddress).mintTo(_staker, _rewards);
*
* }
* ```
Expand Down
2 changes: 1 addition & 1 deletion contracts/extension/interface/IStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,5 @@ interface IStaking {
*
* @param staker Address for which to calculated rewards.
*/
function getStakeInfo(address staker) external view returns (uint256 _tokensStaked, uint256 _rewards);
function getStakeInfo(address staker) external view returns (uint256[] memory _tokensStaked, uint256 _rewards);
}
92 changes: 92 additions & 0 deletions contracts/extension/interface/IStaking1155.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.11;

interface IStaking1155 {
/// @dev Emitted when tokens are staked.
event TokensStaked(address indexed staker, uint256 indexed tokenId, uint256 amount);

/// @dev Emitted when a set of staked token-ids are withdrawn.
event TokensWithdrawn(address indexed staker, uint256 indexed tokenId, uint256 amount);

/// @dev Emitted when a staker claims staking rewards.
event RewardsClaimed(address indexed staker, uint256 rewardAmount);

/// @dev Emitted when contract admin updates timeUnit.
event UpdatedTimeUnit(uint256 indexed _tokenId, uint256 oldTimeUnit, uint256 newTimeUnit);

/// @dev Emitted when contract admin updates rewardsPerUnitTime.
event UpdatedRewardsPerUnitTime(
uint256 indexed _tokenId,
uint256 oldRewardsPerUnitTime,
uint256 newRewardsPerUnitTime
);

/// @dev Emitted when contract admin updates timeUnit.
event UpdatedDefaultTimeUnit(uint256 oldTimeUnit, uint256 newTimeUnit);

/// @dev Emitted when contract admin updates rewardsPerUnitTime.
event UpdatedDefaultRewardsPerUnitTime(uint256 oldRewardsPerUnitTime, uint256 newRewardsPerUnitTime);

/**
* @notice Staker Info.
*
* @param amountStaked Total number of tokens staked by the staker.
*
* @param timeOfLastUpdate Last reward-update timestamp.
*
* @param unclaimedRewards Rewards accumulated but not claimed by user yet.
*/
struct Staker {
uint256 amountStaked;
uint256 timeOfLastUpdate;
uint256 unclaimedRewards;
}

/**
* @notice Stake ERC721 Tokens.
*
* @param tokenId ERC1155 token-id to stake.
* @param amount Amount to stake.
*/
function stake(uint256 tokenId, uint256 amount) external;

/**
* @notice Withdraw staked tokens.
*
* @param tokenId ERC1155 token-id to withdraw.
* @param amount Amount to withdraw.
*/
function withdraw(uint256 tokenId, uint256 amount) external;

/**
* @notice Claim accumulated rewards.
*
* @param tokenId Staked token Id.
*/
function claimRewards(uint256 tokenId) external;

/**
* @notice View amount staked and total rewards for a user.
*
* @param tokenId Staked token Id.
* @param staker Address for which to calculated rewards.
*/
function getStakeInfoForToken(uint256 tokenId, address staker)
external
view
returns (uint256 _tokensStaked, uint256 _rewards);

/**
* @notice View amount staked and total rewards for a user.
*
* @param staker Address for which to calculated rewards.
*/
function getStakeInfo(address staker)
external
view
returns (
uint256[] memory _tokensStaked,
uint256[] memory _tokenAmounts,
uint256 _totalRewards
);
}
Loading

0 comments on commit af41ffb

Please sign in to comment.