From 97e7530728ac387b5847b714d7257203b79824e3 Mon Sep 17 00:00:00 2001 From: nikki-kiga <42276368+nikki-kiga@users.noreply.github.com> Date: Tue, 25 Apr 2023 22:28:36 -0700 Subject: [PATCH 1/7] initial stakingPoints --- .../manifold/staking/ERC721StakingPoints.sol | 67 +++++++ .../manifold/staking/IERC721StakingPoints.sol | 42 +++++ contracts/manifold/staking/IStakingPoints.sol | 106 +++++++++++ contracts/manifold/staking/StakingPoints.sol | 178 ++++++++++++++++++ 4 files changed, 393 insertions(+) create mode 100644 contracts/manifold/staking/ERC721StakingPoints.sol create mode 100644 contracts/manifold/staking/IERC721StakingPoints.sol create mode 100644 contracts/manifold/staking/IStakingPoints.sol create mode 100644 contracts/manifold/staking/StakingPoints.sol diff --git a/contracts/manifold/staking/ERC721StakingPoints.sol b/contracts/manifold/staking/ERC721StakingPoints.sol new file mode 100644 index 00000000..766bccf3 --- /dev/null +++ b/contracts/manifold/staking/ERC721StakingPoints.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @author: manifold.xyz + +import "@manifoldxyz/creator-core-solidity/contracts/core/IERC721CreatorCore.sol"; +import "../../libraries/IERC721CreatorCoreVersion.sol"; +import "./IERC721StakingPoints.sol"; +import "./StakingPoints.sol"; + +contract ERC721StakingPoints is StakingPoints, IERC721StakingPoints { + using Strings for uint256; + + // { creatorContractAddress => { instanceId => bool } } + mapping(address => mapping(uint256 => bool)) private _identicalTokenURI; + + function supportsInterface(bytes4 interfaceId) public view virtual override(StakingPoints, IERC165) returns (bool) { + return interfaceId == type(IERC721StakingPoints).interfaceId || super.supportsInterface(interfaceId); + } + + function initializeStakingPoints( + address creatorContractAddress, + uint256 instanceId, + bool identicalTokenURI, + StakingPointsParams calldata stakingPointsParams + ) external override creatorAdminRequired(creatorContractAddress) { + // Max uint56 for instanceId + require(instanceId > 0 && instanceId <= MAX_UINT_56, "Invalid instanceId"); + // Revert if StakingPoints at instanceId already exists + StakingPoints storage instance = _getStakingPointsInstance(creatorContractAddress, instanceId); + require(instance.storageProtocol == StorageProtocol.INVALID, "StakingPoints already initialized"); + require( + stakingPointsParams.storageProtocol != StorageProtocol.INVALID, + "Cannot initialize with invalid storage protocol" + ); + + uint8 creatorContractVersion; + try IERC721CreatorCoreVersion(creatorContractAddress).VERSION() returns (uint256 version) { + require(version <= 255, "Unsupported contract version"); + creatorContractVersion = uint8(version); + } catch {} + + // + _initialize(creatorContractAddress, creatorContractVersion, instanceId, stakingPointsParams); + _identicalTokenURI[creatorContractAddress][instanceId] = identicalTokenURI; + + emit StakingPointsInitialized(creatorContractAddress, instanceId, msg.sender); + } + + /** + * See {IERC721StakingPoints-updateTokenURI} + */ + function updateTokenURI( + address creatorContractAddress, + uint256 instanceId, + StorageProtocol storageProtocol, + bool identicalTokenURI, + string calldata location + ) external override creatorAdminRequired(creatorContractAddress) { + StakingPoints storage stakingPointsInstance = _getStakingPointsInstance(creatorContractAddress, instanceId); + stakingPointsInstance.storageProtocol = storageProtocol; + stakingPointsInstance.location = location; + _identicalTokenURI[creatorContractAddress][instanceId] = identicalTokenURI; + emit StakingPointsUpdated(creatorContractAddress, instanceId); + } +} diff --git a/contracts/manifold/staking/IERC721StakingPoints.sol b/contracts/manifold/staking/IERC721StakingPoints.sol new file mode 100644 index 00000000..6b7d501e --- /dev/null +++ b/contracts/manifold/staking/IERC721StakingPoints.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @author: manifold.xyz + +import "./IStakingPoints.sol"; +import "./StakingPoints.sol"; + +interface IERC721StakingPoints is IStakingPoints, StakingPoints { + /** + * @notice initialize a new staking points, emit initialize event + * @param creatorContractAddress t + * @param instanceId t + * @param stakingPointsParams t + * @param identicalTokenURI t + */ + function initializeStakingPoints( + address creatorContractAddress, + uint256 instanceId, + bool identicalTokenURI, + StakingPointsParams calldata stakingPointsParams + ) external; + + // function updateStakingPoints(address creatorContractAddress, uint256 instanceId, Staking); + + /** + * @notice update tokenURI parameters for an existing claim at instanceId + * @param creatorContractAddress the creator contract corresponding to the claim + * @param instanceId the claim instanceId for the creator contract + * @param storageProtocol the new storage protocol + * @param identical the new value of identical + * @param location the new location + */ + function updateTokenURIParams( + address creatorContractAddress, + uint256 instanceId, + StorageProtocol storageProtocol, + bool identical, + string calldata location + ) external; +} diff --git a/contracts/manifold/staking/IStakingPoints.sol b/contracts/manifold/staking/IStakingPoints.sol new file mode 100644 index 00000000..c2946b32 --- /dev/null +++ b/contracts/manifold/staking/IStakingPoints.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @author: manifold.xyz + +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; + +/** + * StakingPoints interface + */ +interface IStakingPoints is IERC165, IERC721Receiver, IERC1155Receiver { + enum StorageProtocol { + INVALID, + NONE, + ARWEAVE, + IPFS + } + + enum TokenSpec { + INVALID, + ERC721, + ERC1155 + } + + /** TODO: CONFIRM STRUCTS */ + struct StakedToken { + uint256 id; + uint256 tokenId; + address tokenAddress; + uint256 timeStamp; + } + + struct StakingRule { + address tokenAddress; + TokenSpec tokenSpec; + uint256 pointsRate; + uint256 timeUnit; + uint256 startTime; + uint256 endTime; + } + + struct StakingPoints { + address payable paymentReceiver; + StorageProtocol storageProtocol; + uint8 contractVersion; + string location; + StakingRule[] stakingRules; + } + + struct StakingPointsParams { + address payable paymentReceiver; + StorageProtocol storageProtocol; + string location; + StakingRule[] stakingRules; + } + + struct StakingTokenParam { + address tokenAddress; + uint256 tokenId; + } + + //** TODO: EVENTS */ + + event StakingPointsInitialized(address indexed creatorContract, uint256 indexed instanceId, address initializer); + event StakingPointsUpdated(address indexed creatorContract, uint256 indexed instanceId); + event TokensStaked(); + event TokensUnstaked(); + event PointsDistributed(); + + /** + * @notice stake tokens + * @param owner the address of the token owner + * @param stakingTokens a list of tokenIds with token contract addresses + */ + function stakeTokens(address owner, StakingTokenParam[] calldata stakingTokens) external payable; + + /** + * @notice unstake tokens + * @param owner the address of the token owner + * @param unstakingTokens a list of tokenIds with token contract addresses + */ + function unstakeTokens(address owner, StakingTokenParam[] calldata unstakingTokens) external payable; + + /** + * @notice get a staking points instance corresponding to a creator contract and instanceId + * @param creatorContractAddress the address of the creator contract + * @param instanceId the instanceId of the staking points for the creator contract + * @return StakingPoints the staking points object + */ + function getStakingPointsInstance( + address creatorContractAddress, + uint256 instanceId + ) external view returns (StakingPoints memory); + + /** + * @notice recover a token that was sent to the contract without safeTransferFrom + * @param tokenAddress the address of the token contract + * @param tokenId the id of the token + * @param destination the address to send the token to + */ + function recoverERC721(address tokenAddress, uint256 tokenId, address destination) external; +} diff --git a/contracts/manifold/staking/StakingPoints.sol b/contracts/manifold/staking/StakingPoints.sol new file mode 100644 index 00000000..0f57db94 --- /dev/null +++ b/contracts/manifold/staking/StakingPoints.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@manifoldxyz/creator-core-solidity/contracts/extensions/ICreatorExtensionTokenURI.sol"; +import "@manifoldxyz/libraries-solidity/contracts/access/AdminControl.sol"; +import "@manifoldxyz/libraries-solidity/contracts/access/IAdminControl.sol"; + +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + +import "./IStakingPoints.sol"; + +/** + * @title Staking Points Core + * @author manifold.xyz + * @notice Core logic for Staking Points shared extensions. + */ + +abstract contract StakingPoints is ReentrancyGuard, AdminControl, IStakingPoints, ICreatorExtensionTokenURI { + using Strings for uint256; + + string internal constant ARWEAVE_PREFIX = "https://arweave.net/"; + string internal constant IPFS_PREFIX = "ipfs://"; + + /** TODO: FEES */ + + uint256 internal constant MAX_UINT_24 = 0xffffff; + uint256 internal constant MAX_UINT_32 = 0xffffffff; + uint256 internal constant MAX_UINT_56 = 0xffffffffffffff; + uint256 internal constant MAX_UINT_256 = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + address private constant ADDRESS_ZERO = 0x0000000000000000000000000000000000000000; + + // { creatorContractAddress => { instanceId => StakingPoints } } + mapping(address => mapping(uint256 => StakingPoints)) internal _stakingPointsInstances; + + // { walletAddress => tokenAddress[]} + mapping(address => address[]) walletsStakedContracts; + // { tokenAddress => { StakedToken[] } } + mapping(address => StakedToken[]) internal stakedTokens; + mapping(uint256 => uint256) private tokenIndexMapping; + + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165, AdminControl) returns (bool) { + return + interfaceId == type(IStakingPoints).interfaceId || + interfaceId == type(IERC721Receiver).interfaceId || + interfaceId == type(IERC1155Receiver).interfaceId || + interfaceId == type(ICreatorExtensionTokenURI).interfaceId || + super.supportsInterface(interfaceId); + } + + /** + * @notice This extension is shared, not single-creator. So we must ensure + * that a claim's initializer is an admin on the creator contract + * @param creatorContractAddress the address of the creator contract to check the admin against + */ + modifier creatorAdminRequired(address creatorContractAddress) { + require(IAdminControl(creatorContractAddress).isAdmin(msg.sender), "Wallet is not an admin"); + _; + } + + /** + * Initialiazes a StakingPoints with base parameters + */ + function _initialize( + address creatorContractAddress, + uint8 creatorContractVersion, + uint256 instanceId, + StakingPointsParams calldata stakingPointsParams + ) internal { + StakingPoints storage stakingPointsInstance = _getStakingPointsInstance(creatorContractAddress, instanceId); + require( + stakingPointsInstance.storageProtocol == StakingPoints.StorageProtocol.INVALID, + "StakingPoints already initialized" + ); + _validateStakingPointsParams(stakingPointsParams); + stakingPointsInstance.paymentReceiver = stakingPointsParams.paymentReceiver; + stakingPointsInstance.storageProtocol = stakingPointsParams.storageProtocol; + stakingPointsInstance.contractVersion = creatorContractVersion; + stakingPointsInstance.location = stakingPointsParams.location; + _setStakingRules(stakingPointsInstance, stakingPointsParams.stakingRules); + + emit StakingPointsInitialized(creatorContractAddress, instanceId, msg.sender); + } + + /** VIEW */ + + // function getTotalPoints() {} + + // function getStakedToken() {} + + /** STAKING */ + + function stakeTokens() external override adminRequired { + // TODO + // emit TokensStaked() + } + + function _stake(address _tokenAddress, uint256 _tokenId) internal { + // TODO + } + + /** UNSTAKING */ + function unstakeTokens() external override adminRequired { + // TODO + // emit TokensUnstaked() + } + + function _unstake(address _user, uint256 _tokenId) internal { + // TODO + } + + /** HELPERS */ + + /** + * See {IStakingPoints-getStakingPointsInstance}. + */ + function getStakingPointsInstance( + address creatorContractAddress, + uint256 instanceId + ) external view override returns (StakingPoints memory) { + return _getStakingPointsInstance(creatorContractAddress, instanceId); + } + + /** + * Helper to get staking points instance + */ + function _getStakingPointsInstance( + address creatorContractAddress, + uint256 instanceId + ) internal view returns (StakingPoints storage stakingPointsInstance) { + stakingPointsInstance = _stakingPointsInstances[creatorContractAddress][instanceId]; + require(stakingPointsInstance.storageProtocol != StorageProtocol.INVALID, "Staking points not initialized"); + } + + /** + * @dev See {IStakingPoints-recoverERC721}. + */ + function recoverERC721(address tokenAddress, uint256 tokenId, address destination) external override adminRequired { + IERC721(tokenAddress).transferFrom(address(this), destination, tokenId); + } + + /** + * @dev See {IERC721Receiver-onERC721Received}. + */ + function onERC721Received( + address, + address from, + uint256 id, + bytes calldata data + ) external override nonReentrant returns (bytes4) { + _onERC721Received(from, id, data); + return this.onERC721Received.selector; + } + + function _onERC721Received(address from, uint256 id, bytes calldata data) private {} + + function _validateStakingPointsParams(StakingPointsParams calldata stakingPointsParams) internal pure { + require(stakingPointsParams.storageProtocol != StakingPoints.StorageProtocol.INVALID, "Storage protocol invalid"); + require(stakingPointsParams.paymentReceiver != address(0), "Payment receiver required"); + } + + function _setStakingRules(StakingPoints storage stakingPointsInstance, StakingRule[] calldata stakingRules) private { + delete stakingPointsInstance.stakingRules; + for (uint256 i; i < stakingRules.length; ) { + StakingRule storage stakingRule = stakingRules[i]; + require(stakingRule.tokenSpec == TokenSpec.ERC721, "Only supports ERC721 at this time"); + require((stakingRule.endTime == 0) || (stakingRule.startTime < stakingRule.endTime), "Invalid start or end time"); + //check for timeUnit + require(stakingRule.pointsRate > 0, "Invalid points rate"); + stakingPointsInstance.stakingRules.push(stakingRules[i]); + unchecked { + ++i; + } + } + } +} From a0a434b5db85ebdc4ef1aad3aa915897c9e16d2c Mon Sep 17 00:00:00 2001 From: nikki-kiga <42276368+nikki-kiga@users.noreply.github.com> Date: Wed, 26 Apr 2023 19:59:21 -0700 Subject: [PATCH 2/7] update stakingPoints and add tests --- .../manifold/staking/ERC721StakingPoints.sol | 45 +++- .../manifold/staking/IERC721StakingPoints.sol | 15 +- ...akingPoints.sol => IStakingPointsCore.sol} | 44 +++- ...takingPoints.sol => StakingPointsCore.sol} | 59 ++--- test/manifold/stakingpoints721.js | 230 ++++++++++++++++++ 5 files changed, 336 insertions(+), 57 deletions(-) rename contracts/manifold/staking/{IStakingPoints.sol => IStakingPointsCore.sol} (74%) rename contracts/manifold/staking/{StakingPoints.sol => StakingPointsCore.sol} (72%) create mode 100644 test/manifold/stakingpoints721.js diff --git a/contracts/manifold/staking/ERC721StakingPoints.sol b/contracts/manifold/staking/ERC721StakingPoints.sol index 766bccf3..3cfe33ba 100644 --- a/contracts/manifold/staking/ERC721StakingPoints.sol +++ b/contracts/manifold/staking/ERC721StakingPoints.sol @@ -7,15 +7,21 @@ pragma solidity ^0.8.0; import "@manifoldxyz/creator-core-solidity/contracts/core/IERC721CreatorCore.sol"; import "../../libraries/IERC721CreatorCoreVersion.sol"; import "./IERC721StakingPoints.sol"; -import "./StakingPoints.sol"; +import "./StakingPointsCore.sol"; +import "./IStakingPointsCore.sol"; -contract ERC721StakingPoints is StakingPoints, IERC721StakingPoints { +/** + * @title ERC721 Staking Points + * @author manifold.xyz + * @notice logic for Staking Points for ERC721 extension. + */ +contract ERC721StakingPoints is StakingPointsCore, IERC721StakingPoints { using Strings for uint256; // { creatorContractAddress => { instanceId => bool } } mapping(address => mapping(uint256 => bool)) private _identicalTokenURI; - function supportsInterface(bytes4 interfaceId) public view virtual override(StakingPoints, IERC165) returns (bool) { + function supportsInterface(bytes4 interfaceId) public view virtual override(StakingPointsCore, IERC165) returns (bool) { return interfaceId == type(IERC721StakingPoints).interfaceId || super.supportsInterface(interfaceId); } @@ -24,12 +30,9 @@ contract ERC721StakingPoints is StakingPoints, IERC721StakingPoints { uint256 instanceId, bool identicalTokenURI, StakingPointsParams calldata stakingPointsParams - ) external override creatorAdminRequired(creatorContractAddress) { + ) external creatorAdminRequired(creatorContractAddress) nonReentrant { // Max uint56 for instanceId require(instanceId > 0 && instanceId <= MAX_UINT_56, "Invalid instanceId"); - // Revert if StakingPoints at instanceId already exists - StakingPoints storage instance = _getStakingPointsInstance(creatorContractAddress, instanceId); - require(instance.storageProtocol == StorageProtocol.INVALID, "StakingPoints already initialized"); require( stakingPointsParams.storageProtocol != StorageProtocol.INVALID, "Cannot initialize with invalid storage protocol" @@ -41,13 +44,37 @@ contract ERC721StakingPoints is StakingPoints, IERC721StakingPoints { creatorContractVersion = uint8(version); } catch {} - // _initialize(creatorContractAddress, creatorContractVersion, instanceId, stakingPointsParams); _identicalTokenURI[creatorContractAddress][instanceId] = identicalTokenURI; emit StakingPointsInitialized(creatorContractAddress, instanceId, msg.sender); } + /** + * See {ICreatorExtensionTokenURI-tokenURI}. + */ + function tokenURI(address creatorContractAddress, uint256 tokenId) external override view returns(string memory uri) { + + } + + /** + * See {ICreatorExtensionTokenURI-updateTokenURIParams}. + */ + function updateTokenURIParams( + address creatorContractAddress, + uint256 instanceId, + StorageProtocol storageProtocol, + string calldata location + ) external creatorAdminRequired(creatorContractAddress) { + StakingPoints storage stakingPoint = _stakingPointsInstances[creatorContractAddress][instanceId]; + require(stakingPoint.storageProtocol != StorageProtocol.INVALID, "Staking points not initialized"); + require(storageProtocol != StorageProtocol.INVALID, "Cannot set invalid storage protocol"); + + stakingPoint.storageProtocol = storageProtocol; + stakingPoint.location = location; + emit StakingPointsUpdated(creatorContractAddress, instanceId); + } + /** * See {IERC721StakingPoints-updateTokenURI} */ @@ -57,7 +84,7 @@ contract ERC721StakingPoints is StakingPoints, IERC721StakingPoints { StorageProtocol storageProtocol, bool identicalTokenURI, string calldata location - ) external override creatorAdminRequired(creatorContractAddress) { + ) external creatorAdminRequired(creatorContractAddress) { StakingPoints storage stakingPointsInstance = _getStakingPointsInstance(creatorContractAddress, instanceId); stakingPointsInstance.storageProtocol = storageProtocol; stakingPointsInstance.location = location; diff --git a/contracts/manifold/staking/IERC721StakingPoints.sol b/contracts/manifold/staking/IERC721StakingPoints.sol index 6b7d501e..035f9c4c 100644 --- a/contracts/manifold/staking/IERC721StakingPoints.sol +++ b/contracts/manifold/staking/IERC721StakingPoints.sol @@ -4,10 +4,11 @@ pragma solidity ^0.8.0; /// @author: manifold.xyz -import "./IStakingPoints.sol"; -import "./StakingPoints.sol"; +import "./IStakingPointsCore.sol"; +import "./StakingPointsCore.sol"; + +interface IERC721StakingPoints is IStakingPointsCore { -interface IERC721StakingPoints is IStakingPoints, StakingPoints { /** * @notice initialize a new staking points, emit initialize event * @param creatorContractAddress t @@ -25,18 +26,16 @@ interface IERC721StakingPoints is IStakingPoints, StakingPoints { // function updateStakingPoints(address creatorContractAddress, uint256 instanceId, Staking); /** - * @notice update tokenURI parameters for an existing claim at instanceId - * @param creatorContractAddress the creator contract corresponding to the claim - * @param instanceId the claim instanceId for the creator contract + * @notice update tokenURI parameters for an existing stakingPoints at instanceId + * @param creatorContractAddress the creator contract corresponding to the stakingPoints + * @param instanceId the stakingPoints instanceId for the creator contract * @param storageProtocol the new storage protocol - * @param identical the new value of identical * @param location the new location */ function updateTokenURIParams( address creatorContractAddress, uint256 instanceId, StorageProtocol storageProtocol, - bool identical, string calldata location ) external; } diff --git a/contracts/manifold/staking/IStakingPoints.sol b/contracts/manifold/staking/IStakingPointsCore.sol similarity index 74% rename from contracts/manifold/staking/IStakingPoints.sol rename to contracts/manifold/staking/IStakingPointsCore.sol index c2946b32..a2a19163 100644 --- a/contracts/manifold/staking/IStakingPoints.sol +++ b/contracts/manifold/staking/IStakingPointsCore.sol @@ -7,12 +7,13 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; -import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; + +// import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; /** - * StakingPoints interface + * StakingPointsCore interface */ -interface IStakingPoints is IERC165, IERC721Receiver, IERC1155Receiver { +interface IStakingPointsCore is IERC165, IERC721Receiver { enum StorageProtocol { INVALID, NONE, @@ -30,17 +31,30 @@ interface IStakingPoints is IERC165, IERC721Receiver, IERC1155Receiver { struct StakedToken { uint256 id; uint256 tokenId; - address tokenAddress; - uint256 timeStamp; + address contractAddress; + uint48 groupIndex; + uint48 itemIndex; + uint256 timeStaked; } + struct StakedItem { + address contractAddress; + uint256 tokenId; + } + + // struct StakingTokenContract { + // address tokenAddress; + // TokenSpec tokenSpec; + // StakedItem[] items; + // } + struct StakingRule { address tokenAddress; - TokenSpec tokenSpec; uint256 pointsRate; uint256 timeUnit; uint256 startTime; uint256 endTime; + TokenSpec tokenSpec; } struct StakingPoints { @@ -58,10 +72,10 @@ interface IStakingPoints is IERC165, IERC721Receiver, IERC1155Receiver { StakingRule[] stakingRules; } - struct StakingTokenParam { - address tokenAddress; - uint256 tokenId; - } + // struct StakingItemParam { + // address tokenAddress; + // uint256 tokenId; + // } //** TODO: EVENTS */ @@ -76,14 +90,14 @@ interface IStakingPoints is IERC165, IERC721Receiver, IERC1155Receiver { * @param owner the address of the token owner * @param stakingTokens a list of tokenIds with token contract addresses */ - function stakeTokens(address owner, StakingTokenParam[] calldata stakingTokens) external payable; + // function stakeTokens(address owner, StakingItemParam[] calldata stakingTokens) external payable; /** * @notice unstake tokens * @param owner the address of the token owner * @param unstakingTokens a list of tokenIds with token contract addresses */ - function unstakeTokens(address owner, StakingTokenParam[] calldata unstakingTokens) external payable; + // function unstakeTokens(address owner, StakingItemParam[] calldata unstakingTokens) external payable; /** * @notice get a staking points instance corresponding to a creator contract and instanceId @@ -103,4 +117,10 @@ interface IStakingPoints is IERC165, IERC721Receiver, IERC1155Receiver { * @param destination the address to send the token to */ function recoverERC721(address tokenAddress, uint256 tokenId, address destination) external; + + /** + * @notice set the Manifold Membership contract address + * @param addr the address of the Manifold Membership contract + */ + function setMembershipAddress(address addr) external; } diff --git a/contracts/manifold/staking/StakingPoints.sol b/contracts/manifold/staking/StakingPointsCore.sol similarity index 72% rename from contracts/manifold/staking/StakingPoints.sol rename to contracts/manifold/staking/StakingPointsCore.sol index 0f57db94..363de615 100644 --- a/contracts/manifold/staking/StakingPoints.sol +++ b/contracts/manifold/staking/StakingPointsCore.sol @@ -10,15 +10,13 @@ import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; -import "./IStakingPoints.sol"; - +import "./IStakingPointsCore.sol"; /** * @title Staking Points Core * @author manifold.xyz * @notice Core logic for Staking Points shared extensions. */ - -abstract contract StakingPoints is ReentrancyGuard, AdminControl, IStakingPoints, ICreatorExtensionTokenURI { +abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IStakingPointsCore, ICreatorExtensionTokenURI { using Strings for uint256; string internal constant ARWEAVE_PREFIX = "https://arweave.net/"; @@ -35,24 +33,20 @@ abstract contract StakingPoints is ReentrancyGuard, AdminControl, IStakingPoints // { creatorContractAddress => { instanceId => StakingPoints } } mapping(address => mapping(uint256 => StakingPoints)) internal _stakingPointsInstances; - // { walletAddress => tokenAddress[]} - mapping(address => address[]) walletsStakedContracts; - // { tokenAddress => { StakedToken[] } } - mapping(address => StakedToken[]) internal stakedTokens; - mapping(uint256 => uint256) private tokenIndexMapping; + address public manifoldMembershipContract; function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165, AdminControl) returns (bool) { return - interfaceId == type(IStakingPoints).interfaceId || + interfaceId == type(IStakingPointsCore).interfaceId || interfaceId == type(IERC721Receiver).interfaceId || - interfaceId == type(IERC1155Receiver).interfaceId || + // interfaceId == type(IERC1155Receiver).interfaceId || interfaceId == type(ICreatorExtensionTokenURI).interfaceId || super.supportsInterface(interfaceId); } /** * @notice This extension is shared, not single-creator. So we must ensure - * that a claim's initializer is an admin on the creator contract + * that a staking points's initializer is an admin on the creator contract * @param creatorContractAddress the address of the creator contract to check the admin against */ modifier creatorAdminRequired(address creatorContractAddress) { @@ -69,16 +63,14 @@ abstract contract StakingPoints is ReentrancyGuard, AdminControl, IStakingPoints uint256 instanceId, StakingPointsParams calldata stakingPointsParams ) internal { - StakingPoints storage stakingPointsInstance = _getStakingPointsInstance(creatorContractAddress, instanceId); - require( - stakingPointsInstance.storageProtocol == StakingPoints.StorageProtocol.INVALID, - "StakingPoints already initialized" - ); + StakingPoints storage stakingPointsInstance = _stakingPointsInstances[creatorContractAddress][instanceId]; + require(stakingPointsInstance.storageProtocol == StorageProtocol.INVALID, "StakingPoints already initialized"); _validateStakingPointsParams(stakingPointsParams); stakingPointsInstance.paymentReceiver = stakingPointsParams.paymentReceiver; stakingPointsInstance.storageProtocol = stakingPointsParams.storageProtocol; stakingPointsInstance.contractVersion = creatorContractVersion; stakingPointsInstance.location = stakingPointsParams.location; + require(stakingPointsParams.stakingRules.length > 0, "Needs at least one stakingRule"); _setStakingRules(stakingPointsInstance, stakingPointsParams.stakingRules); emit StakingPointsInitialized(creatorContractAddress, instanceId, msg.sender); @@ -92,7 +84,7 @@ abstract contract StakingPoints is ReentrancyGuard, AdminControl, IStakingPoints /** STAKING */ - function stakeTokens() external override adminRequired { + function stakeTokens() external nonReentrant { // TODO // emit TokensStaked() } @@ -102,7 +94,7 @@ abstract contract StakingPoints is ReentrancyGuard, AdminControl, IStakingPoints } /** UNSTAKING */ - function unstakeTokens() external override adminRequired { + function unstakeTokens() external nonReentrant { // TODO // emit TokensUnstaked() } @@ -111,10 +103,12 @@ abstract contract StakingPoints is ReentrancyGuard, AdminControl, IStakingPoints // TODO } + function updateTokenURI() public view virtual {} + /** HELPERS */ /** - * See {IStakingPoints-getStakingPointsInstance}. + * See {IStakingPointsCore-getStakingPointsInstance}. */ function getStakingPointsInstance( address creatorContractAddress, @@ -135,7 +129,7 @@ abstract contract StakingPoints is ReentrancyGuard, AdminControl, IStakingPoints } /** - * @dev See {IStakingPoints-recoverERC721}. + * @dev See {IStakingPointsCore-recoverERC721}. */ function recoverERC721(address tokenAddress, uint256 tokenId, address destination) external override adminRequired { IERC721(tokenAddress).transferFrom(address(this), destination, tokenId); @@ -156,20 +150,29 @@ abstract contract StakingPoints is ReentrancyGuard, AdminControl, IStakingPoints function _onERC721Received(address from, uint256 id, bytes calldata data) private {} + /** + * @dev See {IstakingPointsCore-setManifoldMembership}. + */ + function setMembershipAddress(address addr) external override adminRequired { + manifoldMembershipContract = addr; + } + function _validateStakingPointsParams(StakingPointsParams calldata stakingPointsParams) internal pure { - require(stakingPointsParams.storageProtocol != StakingPoints.StorageProtocol.INVALID, "Storage protocol invalid"); + require(stakingPointsParams.storageProtocol != StorageProtocol.INVALID, "Storage protocol invalid"); require(stakingPointsParams.paymentReceiver != address(0), "Payment receiver required"); } function _setStakingRules(StakingPoints storage stakingPointsInstance, StakingRule[] calldata stakingRules) private { delete stakingPointsInstance.stakingRules; + StakingRule[] storage rules = stakingPointsInstance.stakingRules; for (uint256 i; i < stakingRules.length; ) { - StakingRule storage stakingRule = stakingRules[i]; - require(stakingRule.tokenSpec == TokenSpec.ERC721, "Only supports ERC721 at this time"); - require((stakingRule.endTime == 0) || (stakingRule.startTime < stakingRule.endTime), "Invalid start or end time"); - //check for timeUnit - require(stakingRule.pointsRate > 0, "Invalid points rate"); - stakingPointsInstance.stakingRules.push(stakingRules[i]); + StakingRule memory rule = stakingRules[i]; + require(rule.tokenAddress != address(0), "Staking rule: Contract address required"); + require(rule.tokenSpec == TokenSpec.ERC721, "Staking rule: Only supports ERC721 at this time"); + require((rule.endTime == 0) || (rule.startTime < rule.endTime), "Staking rule: Invalid time range"); + require(rule.timeUnit > 0, "Staking rule: Invalid timeUnit"); + require(rule.pointsRate > 0, "Staking rule: Invalid points rate"); + rules.push(rule); unchecked { ++i; } diff --git a/test/manifold/stakingpoints721.js b/test/manifold/stakingpoints721.js new file mode 100644 index 00000000..b9f2a341 --- /dev/null +++ b/test/manifold/stakingpoints721.js @@ -0,0 +1,230 @@ +const truffleAssert = require("truffle-assertions"); +const ERC721Creator = artifacts.require("@manifoldxyz/creator-core-extensions-solidity/ERC721Creator"); +const ERC721StakingPoints = artifacts.require("ERC721StakingPoints"); +const ERC1155Creator = artifacts.require("@manifoldxyz/creator-core-extensions-solidity/ERC1155Creator"); +const keccak256 = require("keccak256"); +const ethers = require("ethers"); +const MockManifoldMembership = artifacts.require("MockManifoldMembership"); +const ERC721 = artifacts.require("MockERC721"); +const ERC1155 = artifacts.require("MockERC1155"); + +const STAKE_FEE = ethers.BigNumber.from("690000000000000"); +const MULTI_STAKE_FEE = ethers.BigNumber.from("990000000000000"); + +contract("ERC721StakingPoints", function ([...accounts]) { + const [owner, anotherOwner, anyone1, anyone2] = accounts; + + describe("StakingPoints", function () { + let creator, stakingPoints; + let fee; + + beforeEach(async function () { + creator = await ERC721Creator.new("Test", "TEST", { from: owner }); + stakingPoints721 = await ERC721StakingPoints.new({ from: owner }); + manifoldMembership = await MockManifoldMembership.new({ from: owner }); + await stakingPoints721.setMembershipAddress(manifoldMembership.address); + + stakable721 = await ERC721Creator.new("Stakable NFT 1", "TEST", { from: owner }); + stakeable721_2 = await ERC721Creator.new("Stakable NFT 2", "TEST", { from: anotherOwner }); + notStakeable1155 = await ERC1155Creator.new("1155", "TEST", { from: owner }); + + oz721 = await ERC721.new("Test", "TEST", { from: owner }); + oz1155 = await ERC1155.new("test.com", { from: owner }); + + await creator.registerExtension(stakingPoints721.address, { from: owner }); + }); + // edge cases - can user withdraw points without unstaking? + // non-creator or owner cannot change rules + // can admin update the rules / rates while there is an affected token(?) + + it("Admin creates new stakingpoints with rate", async function () { + // must be an admin + await truffleAssert.reverts( + stakingPoints721.initializeStakingPoints( + creator.address, + 1, + true, + { + paymentReceiver: owner, + storageProtocol: 1, + location: "XXX", + stakingRules: [ + { + tokenAddress: manifoldMembership.address, + pointsRate: 1234, + timeUnit: 100000, + startTime: 1674768875, + endTime: 1682541275, + tokenSpec: 1, + }, + ], + }, + { from: anyone1 } + ), + "Wallet is not an admin" + ); + // has invalid staking rule (missing pointsRate value) + await truffleAssert.reverts( + stakingPoints721.initializeStakingPoints( + creator.address, + 1, + true, + { + paymentReceiver: owner, + storageProtocol: 1, + location: "XXX", + stakingRules: [ + { + tokenAddress: manifoldMembership.address, + pointsRate: 0, + timeUnit: 100000, + startTime: 1674768875, + endTime: 1682541275, + tokenSpec: 1, + }, + ], + }, + { from: owner } + ), + "Staking rule: Invalid points rate" + ); + // has invalid staking rule (missing timeUnit value) + await truffleAssert.reverts( + stakingPoints721.initializeStakingPoints( + creator.address, + 1, + true, + { + paymentReceiver: owner, + storageProtocol: 1, + location: "XXX", + stakingRules: [ + { + tokenAddress: manifoldMembership.address, + pointsRate: 1234, + timeUnit: 0, + startTime: 1674768875, + endTime: 1682541275, + tokenSpec: 1, + }, + ], + }, + { from: owner } + ), + "Staking rule: Invalid timeUnit" + ); + // has invalid staking rule (endTime is less than startTime) + await truffleAssert.reverts( + stakingPoints721.initializeStakingPoints( + creator.address, + 1, + true, + { + paymentReceiver: owner, + storageProtocol: 1, + location: "XXX", + stakingRules: [ + { + tokenAddress: manifoldMembership.address, + pointsRate: 1234, + timeUnit: 100000, + startTime: 1682541275, + endTime: 1674768875, + tokenSpec: 1, + }, + ], + }, + { from: owner } + ), + "Staking rule: Invalid time range" + ); + // has invalid staking rule (token spec is not erc721) + await truffleAssert.reverts( + stakingPoints721.initializeStakingPoints( + creator.address, + 1, + true, + { + paymentReceiver: owner, + storageProtocol: 1, + location: "XXX", + stakingRules: [ + { + tokenAddress: manifoldMembership.address, + pointsRate: 1234, + timeUnit: 100000, + startTime: 1674768875, + endTime: 1682541275, + tokenSpec: 2, + }, + ], + }, + { from: owner } + ), + "Staking rule: Only supports ERC721 at this time" + ); + // has valid staking rule (token spec is not erc721) + + await stakingPoints721.initializeStakingPoints( + creator.address, + 1, + true, + { + paymentReceiver: owner, + storageProtocol: 1, + location: "XXX", + stakingRules: [ + { + tokenAddress: manifoldMembership.address, + pointsRate: 1234, + timeUnit: 100000, + startTime: 1674768875, + endTime: 1682541275, + tokenSpec: 1, + }, + ], + }, + { from: owner } + ); + stakingPointsInstance = await stakingPoints721.getStakingPointsInstance(creator.address, 1); + assert.equal(stakingPointsInstance.stakingRules.length, 1); + }); + // TODO: + // it("Admin creates new stakingpoints with intial rate, updates rate", function () {}); + // it("Admin updates uri", function () {}); + it("User stakes two tokens, and unstakes one redeem points for that token", async function () { + await stakingPoints721.initializeStakingPoints( + creator.address, + 1, + true, + { + paymentReceiver: owner, + storageProtocol: 1, + location: "XXX", + stakingRules: [ + { + tokenAddress: manifoldMembership.address, + pointsRate: 1234, + timeUnit: 100000, + startTime: 1674768875, + endTime: 1682541275, + tokenSpec: 1, + }, + ], + }, + { from: owner } + ); + stakingPointsInstance = await stakingPoints721.getStakingPointsInstance(creator.address, 1); + + // stake token from ozERC1155 and have it revert + // stake token from oz + // stake token #2 from oz + + // unstake #1 + // assert pointsRedeemed + + // unstake #2 + // assert pointsRedeemed + }); + }); +}); From f72f1ed6865056582e85e036dcd815f7a51994ec Mon Sep 17 00:00:00 2001 From: nikki-kiga <42276368+nikki-kiga@users.noreply.github.com> Date: Thu, 27 Apr 2023 13:42:11 -0700 Subject: [PATCH 3/7] add staking and unstaking with tests --- .../manifold/staking/ERC721StakingPoints.sol | 59 +--- .../manifold/staking/IERC721StakingPoints.sol | 17 - .../manifold/staking/IStakingPointsCore.sol | 54 +-- .../manifold/staking/StakingPointsCore.sol | 185 ++++++++-- test/manifold/stakingpoints721.js | 323 ++++++++++++++---- 5 files changed, 440 insertions(+), 198 deletions(-) diff --git a/contracts/manifold/staking/ERC721StakingPoints.sol b/contracts/manifold/staking/ERC721StakingPoints.sol index 3cfe33ba..ee5eaf09 100644 --- a/contracts/manifold/staking/ERC721StakingPoints.sol +++ b/contracts/manifold/staking/ERC721StakingPoints.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; /// @author: manifold.xyz +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@manifoldxyz/creator-core-solidity/contracts/core/IERC721CreatorCore.sol"; import "../../libraries/IERC721CreatorCoreVersion.sol"; import "./IERC721StakingPoints.sol"; @@ -18,9 +19,6 @@ import "./IStakingPointsCore.sol"; contract ERC721StakingPoints is StakingPointsCore, IERC721StakingPoints { using Strings for uint256; - // { creatorContractAddress => { instanceId => bool } } - mapping(address => mapping(uint256 => bool)) private _identicalTokenURI; - function supportsInterface(bytes4 interfaceId) public view virtual override(StakingPointsCore, IERC165) returns (bool) { return interfaceId == type(IERC721StakingPoints).interfaceId || super.supportsInterface(interfaceId); } @@ -28,15 +26,11 @@ contract ERC721StakingPoints is StakingPointsCore, IERC721StakingPoints { function initializeStakingPoints( address creatorContractAddress, uint256 instanceId, - bool identicalTokenURI, StakingPointsParams calldata stakingPointsParams ) external creatorAdminRequired(creatorContractAddress) nonReentrant { // Max uint56 for instanceId require(instanceId > 0 && instanceId <= MAX_UINT_56, "Invalid instanceId"); - require( - stakingPointsParams.storageProtocol != StorageProtocol.INVALID, - "Cannot initialize with invalid storage protocol" - ); + require(stakingPointsParams.paymentReceiver != address(0), "Cannot initialize without payment receiver"); uint8 creatorContractVersion; try IERC721CreatorCoreVersion(creatorContractAddress).VERSION() returns (uint256 version) { @@ -45,50 +39,29 @@ contract ERC721StakingPoints is StakingPointsCore, IERC721StakingPoints { } catch {} _initialize(creatorContractAddress, creatorContractVersion, instanceId, stakingPointsParams); - _identicalTokenURI[creatorContractAddress][instanceId] = identicalTokenURI; emit StakingPointsInitialized(creatorContractAddress, instanceId, msg.sender); } /** - * See {ICreatorExtensionTokenURI-tokenURI}. - */ - function tokenURI(address creatorContractAddress, uint256 tokenId) external override view returns(string memory uri) { - - } - - /** - * See {ICreatorExtensionTokenURI-updateTokenURIParams}. + * @dev was originally using safeTransferFrom but was getting a reentrancy error */ - function updateTokenURIParams( - address creatorContractAddress, - uint256 instanceId, - StorageProtocol storageProtocol, - string calldata location - ) external creatorAdminRequired(creatorContractAddress) { - StakingPoints storage stakingPoint = _stakingPointsInstances[creatorContractAddress][instanceId]; - require(stakingPoint.storageProtocol != StorageProtocol.INVALID, "Staking points not initialized"); - require(storageProtocol != StorageProtocol.INVALID, "Cannot set invalid storage protocol"); - - stakingPoint.storageProtocol = storageProtocol; - stakingPoint.location = location; - emit StakingPointsUpdated(creatorContractAddress, instanceId); + function _transfer(address contractAddress, uint256 tokenId, address from, address to) internal override { + require( + IERC721(contractAddress).ownerOf(tokenId) == from && + (IERC721(contractAddress).getApproved(tokenId) == address(this) || + IERC721(contractAddress).isApprovedForAll(from, address(this))), + "Token not owned or not approved" + ); + require(IERC721(contractAddress).ownerOf(tokenId) == from, "Token not in sender possesion"); + IERC721(contractAddress).transferFrom(from, to, tokenId); } /** - * See {IERC721StakingPoints-updateTokenURI} + * @dev was originally using safeTransferFrom but was getting a reentrancy error */ - function updateTokenURI( - address creatorContractAddress, - uint256 instanceId, - StorageProtocol storageProtocol, - bool identicalTokenURI, - string calldata location - ) external creatorAdminRequired(creatorContractAddress) { - StakingPoints storage stakingPointsInstance = _getStakingPointsInstance(creatorContractAddress, instanceId); - stakingPointsInstance.storageProtocol = storageProtocol; - stakingPointsInstance.location = location; - _identicalTokenURI[creatorContractAddress][instanceId] = identicalTokenURI; - emit StakingPointsUpdated(creatorContractAddress, instanceId); + function _transferBack(address contractAddress, uint256 tokenId, address from, address to) internal override { + require(IERC721(contractAddress).ownerOf(tokenId) == from, "Token not in sender possesion"); + IERC721(contractAddress).transferFrom(from, to, tokenId); } } diff --git a/contracts/manifold/staking/IERC721StakingPoints.sol b/contracts/manifold/staking/IERC721StakingPoints.sol index 035f9c4c..5ef5c78f 100644 --- a/contracts/manifold/staking/IERC721StakingPoints.sol +++ b/contracts/manifold/staking/IERC721StakingPoints.sol @@ -14,28 +14,11 @@ interface IERC721StakingPoints is IStakingPointsCore { * @param creatorContractAddress t * @param instanceId t * @param stakingPointsParams t - * @param identicalTokenURI t */ function initializeStakingPoints( address creatorContractAddress, uint256 instanceId, - bool identicalTokenURI, StakingPointsParams calldata stakingPointsParams ) external; - // function updateStakingPoints(address creatorContractAddress, uint256 instanceId, Staking); - - /** - * @notice update tokenURI parameters for an existing stakingPoints at instanceId - * @param creatorContractAddress the creator contract corresponding to the stakingPoints - * @param instanceId the stakingPoints instanceId for the creator contract - * @param storageProtocol the new storage protocol - * @param location the new location - */ - function updateTokenURIParams( - address creatorContractAddress, - uint256 instanceId, - StorageProtocol storageProtocol, - string calldata location - ) external; } diff --git a/contracts/manifold/staking/IStakingPointsCore.sol b/contracts/manifold/staking/IStakingPointsCore.sol index a2a19163..9c6e59a6 100644 --- a/contracts/manifold/staking/IStakingPointsCore.sol +++ b/contracts/manifold/staking/IStakingPointsCore.sol @@ -14,39 +14,27 @@ import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; * StakingPointsCore interface */ interface IStakingPointsCore is IERC165, IERC721Receiver { - enum StorageProtocol { - INVALID, - NONE, - ARWEAVE, - IPFS - } - - enum TokenSpec { - INVALID, - ERC721, - ERC1155 - } - /** TODO: CONFIRM STRUCTS */ + struct StakedToken { - uint256 id; uint256 tokenId; address contractAddress; - uint48 groupIndex; - uint48 itemIndex; + address stakerAddress; uint256 timeStaked; + uint256 timeUnstaked; + uint256 stakerTokenIdx; } - struct StakedItem { - address contractAddress; + struct StakedTokenParams { + address tokenAddress; uint256 tokenId; } - // struct StakingTokenContract { - // address tokenAddress; - // TokenSpec tokenSpec; - // StakedItem[] items; - // } + struct Staker { + uint256 pointsRedeemed; + uint256 stakerTokenIdx; + StakedToken[] stakersTokens; + } struct StakingRule { address tokenAddress; @@ -54,35 +42,23 @@ interface IStakingPointsCore is IERC165, IERC721Receiver { uint256 timeUnit; uint256 startTime; uint256 endTime; - TokenSpec tokenSpec; } struct StakingPoints { address payable paymentReceiver; - StorageProtocol storageProtocol; uint8 contractVersion; - string location; StakingRule[] stakingRules; } struct StakingPointsParams { address payable paymentReceiver; - StorageProtocol storageProtocol; - string location; StakingRule[] stakingRules; } - // struct StakingItemParam { - // address tokenAddress; - // uint256 tokenId; - // } - - //** TODO: EVENTS */ - event StakingPointsInitialized(address indexed creatorContract, uint256 indexed instanceId, address initializer); event StakingPointsUpdated(address indexed creatorContract, uint256 indexed instanceId); - event TokensStaked(); - event TokensUnstaked(); + event TokensStaked(uint256 indexed instanceId, StakedToken[] stakedTokens, address owner); + event TokensUnstaked(uint256 indexed instanceId, StakedToken[] stakedTokens, address owner); event PointsDistributed(); /** @@ -90,14 +66,14 @@ interface IStakingPointsCore is IERC165, IERC721Receiver { * @param owner the address of the token owner * @param stakingTokens a list of tokenIds with token contract addresses */ - // function stakeTokens(address owner, StakingItemParam[] calldata stakingTokens) external payable; + // function stakeTokens(address owner, StakedTokenParams[] calldata stakingTokens) external; /** * @notice unstake tokens * @param owner the address of the token owner * @param unstakingTokens a list of tokenIds with token contract addresses */ - // function unstakeTokens(address owner, StakingItemParam[] calldata unstakingTokens) external payable; + // function unstakeTokens(address owner, StakedTokenParams[] calldata unstakingTokens) external; /** * @notice get a staking points instance corresponding to a creator contract and instanceId diff --git a/contracts/manifold/staking/StakingPointsCore.sol b/contracts/manifold/staking/StakingPointsCore.sol index 363de615..06c4077d 100644 --- a/contracts/manifold/staking/StakingPointsCore.sol +++ b/contracts/manifold/staking/StakingPointsCore.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; -import "@manifoldxyz/creator-core-solidity/contracts/extensions/ICreatorExtensionTokenURI.sol"; import "@manifoldxyz/libraries-solidity/contracts/access/AdminControl.sol"; import "@manifoldxyz/libraries-solidity/contracts/access/IAdminControl.sol"; @@ -11,19 +10,15 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import "./IStakingPointsCore.sol"; + /** * @title Staking Points Core * @author manifold.xyz * @notice Core logic for Staking Points shared extensions. */ -abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IStakingPointsCore, ICreatorExtensionTokenURI { +abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IStakingPointsCore { using Strings for uint256; - string internal constant ARWEAVE_PREFIX = "https://arweave.net/"; - string internal constant IPFS_PREFIX = "ipfs://"; - - /** TODO: FEES */ - uint256 internal constant MAX_UINT_24 = 0xffffff; uint256 internal constant MAX_UINT_32 = 0xffffffff; uint256 internal constant MAX_UINT_56 = 0xffffffffffffff; @@ -33,14 +28,27 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS // { creatorContractAddress => { instanceId => StakingPoints } } mapping(address => mapping(uint256 => StakingPoints)) internal _stakingPointsInstances; + // { instanceId => { walletAddress => Staker } } + mapping(uint256 => mapping(address => Staker)) public stakers; + + // { instanceId => { tokenAddress => StakingRule } } + mapping(uint256 => mapping(address => StakingRule)) internal _stakingRules; + + // { walletAddress => { tokenAddress => { tokenId => StakedToken } } } + mapping(address => mapping(address => mapping(uint256 => StakedToken))) public userStakedTokens; + + // { walletAddress => bool} + mapping(address => bool) internal _isStakerIndexed; + address public manifoldMembershipContract; + address[] public stakersAddresses; + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165, AdminControl) returns (bool) { return interfaceId == type(IStakingPointsCore).interfaceId || interfaceId == type(IERC721Receiver).interfaceId || // interfaceId == type(IERC1155Receiver).interfaceId || - interfaceId == type(ICreatorExtensionTokenURI).interfaceId || super.supportsInterface(interfaceId); } @@ -64,46 +72,139 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS StakingPointsParams calldata stakingPointsParams ) internal { StakingPoints storage stakingPointsInstance = _stakingPointsInstances[creatorContractAddress][instanceId]; - require(stakingPointsInstance.storageProtocol == StorageProtocol.INVALID, "StakingPoints already initialized"); + require(stakingPointsInstance.paymentReceiver == address(0), "StakingPoints already initialized"); _validateStakingPointsParams(stakingPointsParams); stakingPointsInstance.paymentReceiver = stakingPointsParams.paymentReceiver; - stakingPointsInstance.storageProtocol = stakingPointsParams.storageProtocol; stakingPointsInstance.contractVersion = creatorContractVersion; - stakingPointsInstance.location = stakingPointsParams.location; require(stakingPointsParams.stakingRules.length > 0, "Needs at least one stakingRule"); - _setStakingRules(stakingPointsInstance, stakingPointsParams.stakingRules); + _setStakingRules(stakingPointsInstance, instanceId, stakingPointsParams.stakingRules); emit StakingPointsInitialized(creatorContractAddress, instanceId, msg.sender); } - /** VIEW */ - - // function getTotalPoints() {} + /** + * Abstract helper to transfer tokens. To be implemented by inheriting contracts. + */ + function _transfer(address contractAddress, uint256 tokenId, address fromAddress, address toAddress) internal virtual; - // function getStakedToken() {} + /** + * Abstract helper to transfer tokens. To be implemented by inheriting contracts. + */ + function _transferBack(address contractAddress, uint256 tokenId, address fromAddress, address toAddress) internal virtual; /** STAKING */ - function stakeTokens() external nonReentrant { - // TODO - // emit TokensStaked() + function stakeTokens(uint256 instanceId, StakedTokenParams[] calldata stakingTokens) external nonReentrant { + _stakeTokens(instanceId, stakingTokens); + } + + function _stakeTokens(uint256 instanceId, StakedTokenParams[] calldata stakingTokens) private { + bool isIndexed = _isStakerIndexed[msg.sender]; + if (!isIndexed) { + Staker storage newStaker = stakers[instanceId][msg.sender]; + newStaker.stakerTokenIdx = stakersAddresses.length; + stakersAddresses.push(msg.sender); + _isStakerIndexed[msg.sender] = true; + } + StakedToken[] memory newlyStaked = new StakedToken[](stakingTokens.length); + Staker storage user = stakers[instanceId][msg.sender]; + StakedToken[] storage userTokens = user.stakersTokens; + uint256 length = stakingTokens.length; + for (uint256 i = 0; i < length; ) { + StakedToken storage currToken = userStakedTokens[msg.sender][stakingTokens[i].tokenAddress][stakingTokens[i].tokenId]; + require(currToken.stakerAddress == address(0), "Token already staked"); + currToken.tokenId = stakingTokens[i].tokenId; + currToken.contractAddress = stakingTokens[i].tokenAddress; + currToken.stakerAddress = msg.sender; + currToken.timeStaked = block.timestamp; + currToken.stakerTokenIdx = userTokens.length; + userStakedTokens[msg.sender][stakingTokens[i].tokenAddress][stakingTokens[i].tokenId] = currToken; + userTokens.push(currToken); + newlyStaked[i] = currToken; + _stake(instanceId, stakingTokens[i].tokenAddress, stakingTokens[i].tokenId); + unchecked { + ++i; + } + } + emit TokensStaked(instanceId, newlyStaked, msg.sender); } - function _stake(address _tokenAddress, uint256 _tokenId) internal { - // TODO + function _stake(uint256 instanceId, address tokenAddress, uint256 tokenId) private { + StakingRule memory ruleExists = _getTokenRule(instanceId, tokenAddress); + require(ruleExists.tokenAddress == tokenAddress, "Token does not match existing rule"); + + _transfer(tokenAddress, tokenId, msg.sender, address(this)); } /** UNSTAKING */ - function unstakeTokens() external nonReentrant { - // TODO - // emit TokensUnstaked() + function unstakeTokens(uint256 instanceId, StakedTokenParams[] calldata stakingTokens) external nonReentrant { + _unstakeTokens(instanceId, stakingTokens); + } + + function _unstakeTokens(uint256 instanceId, StakedTokenParams[] calldata stakingTokens) private { + require(stakingTokens.length != 0, "Cannot unstake 0 tokens"); + + StakedToken[] memory unstakedTokens = new StakedToken[](stakingTokens.length); + for (uint256 i = 0; i < stakingTokens.length; ++i) { + StakedToken storage currToken = userStakedTokens[msg.sender][stakingTokens[i].tokenAddress][stakingTokens[i].tokenId]; + require( + msg.sender != address(0) && msg.sender == currToken.stakerAddress, + "No sender address or not the original staker" + ); + currToken.timeUnstaked = block.timestamp; + stakers[instanceId][msg.sender].stakersTokens[currToken.stakerTokenIdx] = currToken; + unstakedTokens[i] = currToken; + _unstakeToken(stakingTokens[i].tokenAddress, stakingTokens[i].tokenId); + } + + emit TokensUnstaked(instanceId, unstakedTokens, msg.sender); + } + + function _unstakeToken(address tokenAddress, uint256 tokenId) private { + StakedToken storage userToken = userStakedTokens[msg.sender][tokenAddress][tokenId]; + require( + msg.sender != address(0) && msg.sender == userToken.stakerAddress, + "No sender address or not the original staker" + ); + _transferBack(tokenAddress, tokenId, address(this), msg.sender); + } + + // function redeemPoints() external nonReentrant { + // // guard against more than calculated + // // update Staker with redeemed points + // } + + // function _calculatePoints(address _staker, StakedToken[] stakingTokens) private view returns (uint256 memory points) { + // uint256 length = stakingTokens.length; + // for (uint256 i = 0; i < length; ) { + // unchecked { + // ++i; + // } + // } + // // get the tokenRule for each + // // get the balance from the user + // // add + // // safe multiply + // // check for overflow + // } + + /** VIEW HELPERS */ + + function getStakerDetails(uint256 instanceId, address staker) external view returns (Staker memory) { + return stakers[instanceId][staker]; } - function _unstake(address _user, uint256 _tokenId) internal { - // TODO + function _getTokenRule(uint256 instanceId, address tokenAddress) internal view returns (StakingRule memory) { + return _stakingRules[instanceId][tokenAddress]; } - function updateTokenURI() public view virtual {} + function getUserStakedToken( + address wallet, + address tokenAddress, + uint256 tokenId + ) external view returns (StakedToken memory) { + return userStakedTokens[wallet][tokenAddress][tokenId]; + } /** HELPERS */ @@ -113,8 +214,15 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS function getStakingPointsInstance( address creatorContractAddress, uint256 instanceId - ) external view override returns (StakingPoints memory) { - return _getStakingPointsInstance(creatorContractAddress, instanceId); + ) external view override returns (StakingPoints memory _stakingPoints) { + _stakingPoints = _getStakingPointsInstance(creatorContractAddress, instanceId); + } + + /** + * Helper to get Staker + */ + function _getStaker(uint256 instanceId, address walletAddress) internal view returns (Staker memory staker) { + staker = stakers[instanceId][walletAddress]; } /** @@ -125,7 +233,7 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS uint256 instanceId ) internal view returns (StakingPoints storage stakingPointsInstance) { stakingPointsInstance = _stakingPointsInstances[creatorContractAddress][instanceId]; - require(stakingPointsInstance.storageProtocol != StorageProtocol.INVALID, "Staking points not initialized"); + require(stakingPointsInstance.paymentReceiver != address(0), "Staking points not initialized"); } /** @@ -158,21 +266,30 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS } function _validateStakingPointsParams(StakingPointsParams calldata stakingPointsParams) internal pure { - require(stakingPointsParams.storageProtocol != StorageProtocol.INVALID, "Storage protocol invalid"); require(stakingPointsParams.paymentReceiver != address(0), "Payment receiver required"); } - function _setStakingRules(StakingPoints storage stakingPointsInstance, StakingRule[] calldata stakingRules) private { + function _setStakingRules( + StakingPoints storage stakingPointsInstance, + uint256 instanceId, + StakingRule[] calldata stakingRules + ) private { + // delete old rules in map + StakingRule[] memory oldRules = stakingPointsInstance.stakingRules; + for (uint256 i; i < oldRules.length; ) { + delete _stakingRules[instanceId][oldRules[i].tokenAddress]; + } delete stakingPointsInstance.stakingRules; StakingRule[] storage rules = stakingPointsInstance.stakingRules; - for (uint256 i; i < stakingRules.length; ) { + uint256 length = stakingRules.length; + for (uint256 i; i < length; ) { StakingRule memory rule = stakingRules[i]; require(rule.tokenAddress != address(0), "Staking rule: Contract address required"); - require(rule.tokenSpec == TokenSpec.ERC721, "Staking rule: Only supports ERC721 at this time"); require((rule.endTime == 0) || (rule.startTime < rule.endTime), "Staking rule: Invalid time range"); require(rule.timeUnit > 0, "Staking rule: Invalid timeUnit"); require(rule.pointsRate > 0, "Staking rule: Invalid points rate"); rules.push(rule); + _stakingRules[instanceId][rule.tokenAddress] = rule; unchecked { ++i; } diff --git a/test/manifold/stakingpoints721.js b/test/manifold/stakingpoints721.js index b9f2a341..9ea1028b 100644 --- a/test/manifold/stakingpoints721.js +++ b/test/manifold/stakingpoints721.js @@ -15,8 +15,7 @@ contract("ERC721StakingPoints", function ([...accounts]) { const [owner, anotherOwner, anyone1, anyone2] = accounts; describe("StakingPoints", function () { - let creator, stakingPoints; - let fee; + let creator, fee; beforeEach(async function () { creator = await ERC721Creator.new("Test", "TEST", { from: owner }); @@ -24,12 +23,17 @@ contract("ERC721StakingPoints", function ([...accounts]) { manifoldMembership = await MockManifoldMembership.new({ from: owner }); await stakingPoints721.setMembershipAddress(manifoldMembership.address); - stakable721 = await ERC721Creator.new("Stakable NFT 1", "TEST", { from: owner }); - stakeable721_2 = await ERC721Creator.new("Stakable NFT 2", "TEST", { from: anotherOwner }); - notStakeable1155 = await ERC1155Creator.new("1155", "TEST", { from: owner }); + mock721 = await ERC721Creator.new("721", "721", { from: owner }); + mock721_2 = await ERC721Creator.new("721_2", "721_2", { from: owner }); + mock1155 = await ERC1155Creator.new("1155.com", { from: owner }); - oz721 = await ERC721.new("Test", "TEST", { from: owner }); - oz1155 = await ERC1155.new("test.com", { from: owner }); + await mock721.mintBase(anyone1, { from: owner }); + await mock721.mintBase(anyone2, { from: owner }); + await mock721.mintBase(anyone2, { from: owner }); + await mock721.mintBase(anyone1, { from: owner }); + await mock721_2.mintBase(anyone1, { from: owner }); + await mock721_2.mintBase(anyone2, { from: owner }); + await mock721_2.mintBase(anotherOwner, { from: owner }); await creator.registerExtension(stakingPoints721.address, { from: owner }); }); @@ -43,11 +47,8 @@ contract("ERC721StakingPoints", function ([...accounts]) { stakingPoints721.initializeStakingPoints( creator.address, 1, - true, { paymentReceiver: owner, - storageProtocol: 1, - location: "XXX", stakingRules: [ { tokenAddress: manifoldMembership.address, @@ -55,7 +56,6 @@ contract("ERC721StakingPoints", function ([...accounts]) { timeUnit: 100000, startTime: 1674768875, endTime: 1682541275, - tokenSpec: 1, }, ], }, @@ -68,11 +68,8 @@ contract("ERC721StakingPoints", function ([...accounts]) { stakingPoints721.initializeStakingPoints( creator.address, 1, - true, { paymentReceiver: owner, - storageProtocol: 1, - location: "XXX", stakingRules: [ { tokenAddress: manifoldMembership.address, @@ -80,7 +77,6 @@ contract("ERC721StakingPoints", function ([...accounts]) { timeUnit: 100000, startTime: 1674768875, endTime: 1682541275, - tokenSpec: 1, }, ], }, @@ -93,11 +89,8 @@ contract("ERC721StakingPoints", function ([...accounts]) { stakingPoints721.initializeStakingPoints( creator.address, 1, - true, { paymentReceiver: owner, - storageProtocol: 1, - location: "XXX", stakingRules: [ { tokenAddress: manifoldMembership.address, @@ -105,7 +98,6 @@ contract("ERC721StakingPoints", function ([...accounts]) { timeUnit: 0, startTime: 1674768875, endTime: 1682541275, - tokenSpec: 1, }, ], }, @@ -118,11 +110,8 @@ contract("ERC721StakingPoints", function ([...accounts]) { stakingPoints721.initializeStakingPoints( creator.address, 1, - true, { paymentReceiver: owner, - storageProtocol: 1, - location: "XXX", stakingRules: [ { tokenAddress: manifoldMembership.address, @@ -130,7 +119,6 @@ contract("ERC721StakingPoints", function ([...accounts]) { timeUnit: 100000, startTime: 1682541275, endTime: 1674768875, - tokenSpec: 1, }, ], }, @@ -138,41 +126,13 @@ contract("ERC721StakingPoints", function ([...accounts]) { ), "Staking rule: Invalid time range" ); - // has invalid staking rule (token spec is not erc721) - await truffleAssert.reverts( - stakingPoints721.initializeStakingPoints( - creator.address, - 1, - true, - { - paymentReceiver: owner, - storageProtocol: 1, - location: "XXX", - stakingRules: [ - { - tokenAddress: manifoldMembership.address, - pointsRate: 1234, - timeUnit: 100000, - startTime: 1674768875, - endTime: 1682541275, - tokenSpec: 2, - }, - ], - }, - { from: owner } - ), - "Staking rule: Only supports ERC721 at this time" - ); // has valid staking rule (token spec is not erc721) await stakingPoints721.initializeStakingPoints( creator.address, 1, - true, { paymentReceiver: owner, - storageProtocol: 1, - location: "XXX", stakingRules: [ { tokenAddress: manifoldMembership.address, @@ -180,7 +140,6 @@ contract("ERC721StakingPoints", function ([...accounts]) { timeUnit: 100000, startTime: 1674768875, endTime: 1682541275, - tokenSpec: 1, }, ], }, @@ -191,16 +150,14 @@ contract("ERC721StakingPoints", function ([...accounts]) { }); // TODO: // it("Admin creates new stakingpoints with intial rate, updates rate", function () {}); - // it("Admin updates uri", function () {}); - it("User stakes two tokens, and unstakes one redeem points for that token", async function () { + // it("Can get stakers", function () {}); + + it("Will not stake if not owned or approved", async function () { await stakingPoints721.initializeStakingPoints( creator.address, 1, - true, { paymentReceiver: owner, - storageProtocol: 1, - location: "XXX", stakingRules: [ { tokenAddress: manifoldMembership.address, @@ -208,23 +165,259 @@ contract("ERC721StakingPoints", function ([...accounts]) { timeUnit: 100000, startTime: 1674768875, endTime: 1682541275, - tokenSpec: 1, + }, + { + tokenAddress: mock721.address, + pointsRate: 125, + timeUnit: 100000, + startTime: 1674768875, + endTime: 1682541275, }, ], }, { from: owner } ); - stakingPointsInstance = await stakingPoints721.getStakingPointsInstance(creator.address, 1); + await truffleAssert.reverts( + stakingPoints721.stakeTokens( + 1, + [ + { + tokenAddress: mock721.address, + tokenId: 1, + }, + ], + { from: owner } + ), + "Token not owned or not approved" + ); + }); + it("Stakes if owned and approved", async function () { + await stakingPoints721.initializeStakingPoints( + creator.address, + 1, + { + paymentReceiver: owner, + stakingRules: [ + { + tokenAddress: manifoldMembership.address, + pointsRate: 1234, + timeUnit: 100000, + startTime: 1674768875, + endTime: 1682541275, + }, + { + tokenAddress: mock721.address, + pointsRate: 125, + timeUnit: 100000, + startTime: 1674768875, + endTime: 1682541275, + }, + { + tokenAddress: mock721_2.address, + pointsRate: 125, + timeUnit: 100000, + startTime: 1674768875, + endTime: 1682541275, + }, + ], + }, + { from: owner } + ); + await mock721.setApprovalForAll(stakingPoints721.address, true, { from: anyone2 }); + await mock721_2.setApprovalForAll(stakingPoints721.address, true, { from: anyone2 }); + await stakingPoints721.stakeTokens( + 1, + [ + { + tokenAddress: mock721.address, + tokenId: 2, + }, + { + tokenAddress: mock721.address, + tokenId: 3, + }, + { + tokenAddress: mock721_2.address, + tokenId: 2, + }, + ], + { from: anyone2 } + ); + + let stakerDetails1 = await stakingPoints721.getStakerDetails(1, anyone2); + + assert.equal(stakerDetails1.stakersTokens.length, 3); + assert.equal(stakerDetails1.stakersTokens[0].tokenId, 2); + assert.equal(stakerDetails1.stakersTokens[1].tokenId, 3); + assert.equal(stakerDetails1.stakersTokens[1].contractAddress, mock721.address); + assert.equal(stakerDetails1.stakersTokens[2].tokenId, 2); + assert.equal(stakerDetails1.stakersTokens[2].contractAddress, mock721_2.address); + }); + it("Stakes and unstakes", async function () { + await stakingPoints721.initializeStakingPoints( + creator.address, + 1, + { + paymentReceiver: owner, + stakingRules: [ + { + tokenAddress: manifoldMembership.address, + pointsRate: 1234, + timeUnit: 100000, + startTime: 1674768875, + endTime: 1682541275, + }, + { + tokenAddress: mock721.address, + pointsRate: 125, + timeUnit: 100000, + startTime: 1674768875, + endTime: 1682541275, + }, + { + tokenAddress: mock721_2.address, + pointsRate: 125, + timeUnit: 100000, + startTime: 1674768875, + endTime: 1682541275, + }, + ], + }, + { from: owner } + ); + await mock721.setApprovalForAll(stakingPoints721.address, true, { from: anyone2 }); + await mock721_2.setApprovalForAll(stakingPoints721.address, true, { from: anyone2 }); + await stakingPoints721.stakeTokens( + 1, + [ + { + tokenAddress: mock721.address, + tokenId: 2, + }, + { + tokenAddress: mock721.address, + tokenId: 3, + }, + { + tokenAddress: mock721_2.address, + tokenId: 2, + }, + ], + { from: anyone2 } + ); + + let stakerDetails1 = await stakingPoints721.getStakerDetails(1, anyone2); - // stake token from ozERC1155 and have it revert - // stake token from oz - // stake token #2 from oz + assert.equal(stakerDetails1.stakersTokens.length, 3); + assert.equal(stakerDetails1.stakersTokens[0].tokenId, 2); + assert.equal(stakerDetails1.stakersTokens[1].tokenId, 3); + assert.equal(stakerDetails1.stakersTokens[1].contractAddress, mock721.address); + assert.equal(stakerDetails1.stakersTokens[2].tokenId, 2); + assert.equal(stakerDetails1.stakersTokens[2].contractAddress, mock721_2.address); // unstake #1 - // assert pointsRedeemed + await truffleAssert.reverts( + stakingPoints721.unstakeTokens( + 1, + [ + { + tokenAddress: mock721.address, + tokenId: 2, + }, + { + tokenAddress: mock721.address, + tokenId: 3, + }, + { + tokenAddress: mock721_2.address, + tokenId: 2, + }, + ], + { from: anyone1 } + ), + "No sender address or not the original staker" + ); - // unstake #2 - // assert pointsRedeemed + await stakingPoints721.unstakeTokens( + 1, + [ + { + tokenAddress: mock721.address, + tokenId: 2, + }, + { + tokenAddress: mock721_2.address, + tokenId: 2, + }, + ], + { from: anyone2 } + ); + + let stakerDetails = await stakingPoints721.getStakerDetails(1, anyone2); + assert.equal(stakerDetails.stakersTokens.length, 3); + let token = await stakingPoints721.getUserStakedToken(anyone2, mock721.address, 2); + assert.equal(token.timeUnstaked !== 0, true); + let token2 = await stakingPoints721.getUserStakedToken(anyone2, mock721.address, 3); + assert.equal(token2.timeUnstaked, 0); + let token3 = await stakingPoints721.getUserStakedToken(anyone2, mock721_2.address, 2); + assert.equal(token3.timeUnstaked !== 0, true); + assert.equal(stakerDetails.stakersTokens[0].timeUnstaked, token.timeUnstaked); + assert.equal(stakerDetails.stakersTokens[1].timeUnstaked, token2.timeUnstaked); + assert.equal(stakerDetails.stakersTokens[2].timeUnstaked, token3.timeUnstaked); }); + + // it("Redeems points", async function () { + // await stakingPoints721.initializeStakingPoints( + // creator.address, + // 1, + // { + // paymentReceiver: owner, + // stakingRules: [ + // { + // tokenAddress: manifoldMembership.address, + // pointsRate: 1234, + // timeUnit: 100000, + // startTime: 1674768875, + // endTime: 1682541275, + // }, + // { + // tokenAddress: mock721.address, + // pointsRate: 125, + // timeUnit: 100000, + // startTime: 1674768875, + // endTime: 1682541275, + // }, + // { + // tokenAddress: mock721_2.address, + // pointsRate: 125, + // timeUnit: 100000, + // startTime: 1674768875, + // endTime: 1682541275, + // }, + // ], + // }, + // { from: owner } + // ); + // await mock721.setApprovalForAll(stakingPoints721.address, true, { from: anyone2 }); + // await mock721_2.setApprovalForAll(stakingPoints721.address, true, { from: anyone2 }); + // await stakingPoints721.stakeTokens( + // 1, + // [ + // { + // tokenAddress: mock721.address, + // tokenId: 2, + // }, + // { + // tokenAddress: mock721.address, + // tokenId: 3, + // }, + // { + // tokenAddress: mock721_2.address, + // tokenId: 2, + // }, + // ], + // { from: anyone2 } + // ); + // }); }); }); From e8fc4cd7f1de1fbb744c1165e6cd965b766d43b6 Mon Sep 17 00:00:00 2001 From: nikki-kiga <42276368+nikki-kiga@users.noreply.github.com> Date: Thu, 27 Apr 2023 18:28:41 -0700 Subject: [PATCH 4/7] add points calcs tests --- .../manifold/staking/ERC721StakingPoints.sol | 14 +- .../manifold/staking/IERC721StakingPoints.sol | 6 +- .../manifold/staking/IStakingPointsCore.sol | 11 +- .../manifold/staking/StakingPointsCore.sol | 102 +++++--- test/manifold/stakingpoints721.js | 226 +++++++++--------- 5 files changed, 204 insertions(+), 155 deletions(-) diff --git a/contracts/manifold/staking/ERC721StakingPoints.sol b/contracts/manifold/staking/ERC721StakingPoints.sol index ee5eaf09..5ec783f1 100644 --- a/contracts/manifold/staking/ERC721StakingPoints.sol +++ b/contracts/manifold/staking/ERC721StakingPoints.sol @@ -17,7 +17,11 @@ import "./IStakingPointsCore.sol"; * @notice logic for Staking Points for ERC721 extension. */ contract ERC721StakingPoints is StakingPointsCore, IERC721StakingPoints { - using Strings for uint256; + + uint256 public totalPointsCirculation; + + // { instanceId => { walletAddress => points } } + mapping(uint256 => mapping(address => uint256)) public stakerPoints; function supportsInterface(bytes4 interfaceId) public view virtual override(StakingPointsCore, IERC165) returns (bool) { return interfaceId == type(IERC721StakingPoints).interfaceId || super.supportsInterface(interfaceId); @@ -64,4 +68,12 @@ contract ERC721StakingPoints is StakingPointsCore, IERC721StakingPoints { require(IERC721(contractAddress).ownerOf(tokenId) == from, "Token not in sender possesion"); IERC721(contractAddress).transferFrom(from, to, tokenId); } + + /** + * @dev + */ + function _redeem(uint256 instanceId, uint256 pointsAmount, address redeemer) internal override { + totalPointsCirculation = SafeMath.add(totalPointsCirculation, pointsAmount); + stakerPoints[instanceId][redeemer]; + } } diff --git a/contracts/manifold/staking/IERC721StakingPoints.sol b/contracts/manifold/staking/IERC721StakingPoints.sol index 5ef5c78f..3aa0f6a7 100644 --- a/contracts/manifold/staking/IERC721StakingPoints.sol +++ b/contracts/manifold/staking/IERC721StakingPoints.sol @@ -11,9 +11,9 @@ interface IERC721StakingPoints is IStakingPointsCore { /** * @notice initialize a new staking points, emit initialize event - * @param creatorContractAddress t - * @param instanceId t - * @param stakingPointsParams t + * @param creatorContractAddress the address of the creator contract + * @param instanceId the instanceId of the staking points for the creator contract + * @param stakingPointsParams the stakingPointsParams object */ function initializeStakingPoints( address creatorContractAddress, diff --git a/contracts/manifold/staking/IStakingPointsCore.sol b/contracts/manifold/staking/IStakingPointsCore.sol index 9c6e59a6..cc32517c 100644 --- a/contracts/manifold/staking/IStakingPointsCore.sol +++ b/contracts/manifold/staking/IStakingPointsCore.sol @@ -38,8 +38,7 @@ interface IStakingPointsCore is IERC165, IERC721Receiver { struct StakingRule { address tokenAddress; - uint256 pointsRate; - uint256 timeUnit; + uint256 pointsRatePerDay; uint256 startTime; uint256 endTime; } @@ -63,17 +62,17 @@ interface IStakingPointsCore is IERC165, IERC721Receiver { /** * @notice stake tokens - * @param owner the address of the token owner + * @param instanceId the instanceId of the staking points for the creator contract * @param stakingTokens a list of tokenIds with token contract addresses */ - // function stakeTokens(address owner, StakedTokenParams[] calldata stakingTokens) external; + function stakeTokens(uint256 instanceId, StakedTokenParams[] calldata stakingTokens) external; /** * @notice unstake tokens - * @param owner the address of the token owner + * @param instanceId the instanceId of the staking points for the creator contract * @param unstakingTokens a list of tokenIds with token contract addresses */ - // function unstakeTokens(address owner, StakedTokenParams[] calldata unstakingTokens) external; + function unstakeTokens(uint256 instanceId, StakedTokenParams[] calldata unstakingTokens) external; /** * @notice get a staking points instance corresponding to a creator contract and instanceId diff --git a/contracts/manifold/staking/StakingPointsCore.sol b/contracts/manifold/staking/StakingPointsCore.sol index 06c4077d..99eadfcd 100644 --- a/contracts/manifold/staking/StakingPointsCore.sol +++ b/contracts/manifold/staking/StakingPointsCore.sol @@ -7,18 +7,19 @@ import "@manifoldxyz/libraries-solidity/contracts/access/IAdminControl.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; import "./IStakingPointsCore.sol"; /** * @title Staking Points Core * @author manifold.xyz - * @notice Core logic for Staking Points shared extensions. + * @notice Core logic for Staking Points shared extensions. Currently only handles ERC721, next steps could include + * implementing batch fns, ERC1155 support, using a ERC20 token to represent points, and explore more point dynamics */ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IStakingPointsCore { - using Strings for uint256; - + using SafeMath for uint256; uint256 internal constant MAX_UINT_24 = 0xffffff; uint256 internal constant MAX_UINT_32 = 0xffffffff; uint256 internal constant MAX_UINT_56 = 0xffffffffffffff; @@ -81,6 +82,7 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS emit StakingPointsInitialized(creatorContractAddress, instanceId, msg.sender); } + /** * Abstract helper to transfer tokens. To be implemented by inheriting contracts. @@ -137,16 +139,19 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS } /** UNSTAKING */ - function unstakeTokens(uint256 instanceId, StakedTokenParams[] calldata stakingTokens) external nonReentrant { - _unstakeTokens(instanceId, stakingTokens); + + function unstakeTokens(uint256 instanceId, StakedTokenParams[] calldata unstakingTokens) external nonReentrant { + _unstakeTokens(instanceId, unstakingTokens); } - function _unstakeTokens(uint256 instanceId, StakedTokenParams[] calldata stakingTokens) private { - require(stakingTokens.length != 0, "Cannot unstake 0 tokens"); + function _unstakeTokens(uint256 instanceId, StakedTokenParams[] calldata unstakingTokens) private { + require(unstakingTokens.length != 0, "Cannot unstake 0 tokens"); - StakedToken[] memory unstakedTokens = new StakedToken[](stakingTokens.length); - for (uint256 i = 0; i < stakingTokens.length; ++i) { - StakedToken storage currToken = userStakedTokens[msg.sender][stakingTokens[i].tokenAddress][stakingTokens[i].tokenId]; + StakedToken[] memory unstakedTokens = new StakedToken[](unstakingTokens.length); + for (uint256 i = 0; i < unstakingTokens.length; ++i) { + StakedToken storage currToken = userStakedTokens[msg.sender][unstakingTokens[i].tokenAddress][ + unstakingTokens[i].tokenId + ]; require( msg.sender != address(0) && msg.sender == currToken.stakerAddress, "No sender address or not the original staker" @@ -154,7 +159,7 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS currToken.timeUnstaked = block.timestamp; stakers[instanceId][msg.sender].stakersTokens[currToken.stakerTokenIdx] = currToken; unstakedTokens[i] = currToken; - _unstakeToken(stakingTokens[i].tokenAddress, stakingTokens[i].tokenId); + _unstakeToken(unstakingTokens[i].tokenAddress, unstakingTokens[i].tokenId); } emit TokensUnstaked(instanceId, unstakedTokens, msg.sender); @@ -169,27 +174,55 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS _transferBack(tokenAddress, tokenId, address(this), msg.sender); } - // function redeemPoints() external nonReentrant { - // // guard against more than calculated - // // update Staker with redeemed points - // } - - // function _calculatePoints(address _staker, StakedToken[] stakingTokens) private view returns (uint256 memory points) { - // uint256 length = stakingTokens.length; - // for (uint256 i = 0; i < length; ) { - // unchecked { - // ++i; - // } - // } - // // get the tokenRule for each - // // get the balance from the user - // // add - // // safe multiply - // // check for overflow - // } + /** + * Abstract helper to redeem points. To be implemented by inheriting contracts. + */ + function _redeem(uint256 instanceId, uint256 pointsAmount, address redeemer) internal virtual; + + function redeemPoints(uint256 instanceId) external nonReentrant { + // guard against more than calculated + Staker storage staker = stakers[instanceId][msg.sender]; + uint256 totalQualifyingPoints = _calculatePoints(instanceId, staker.stakersTokens); + uint256 diff = SafeMath.sub(totalQualifyingPoints, staker.pointsRedeemed); + require(totalQualifyingPoints != 0 && diff >= 0, "Need more than zero points"); + // compare with pointsRedeemd + staker.pointsRedeemed = totalQualifyingPoints; + _redeem(instanceId, diff, msg.sender); + } + + function getPoints(uint256 instanceId) external view returns (uint256 totalPoints, uint256 diff) { + Staker storage staker = stakers[instanceId][msg.sender]; + totalPoints = _calculatePoints(instanceId, staker.stakersTokens); + diff = SafeMath.sub(totalPoints, staker.pointsRedeemed); + } + + /** + * @notice assumes points + */ + function _calculatePoints(uint256 instanceId, StakedToken[] memory stakingTokens) private view returns (uint256 points) { + uint256 length = stakingTokens.length; + for (uint256 i = 0; i < length; ) { + StakingRule storage rule = _stakingRules[instanceId][stakingTokens[i].contractAddress]; + require(rule.startTime >= 0 && rule.endTime >= 0, "Invalid rule values"); + uint256 tokenEnd = stakingTokens[i].timeUnstaked == 0 ? block.timestamp : stakingTokens[i].timeUnstaked; + uint256 start = Math.max(rule.startTime, stakingTokens[i].timeStaked); + uint256 end = Math.min(tokenEnd, rule.endTime); + uint256 diff = start - end; + uint256 qualified = _calculateQualifiedPoints(diff, rule.pointsRatePerDay, 86400); + points = points + qualified; + + unchecked { + ++i; + } + } + } /** VIEW HELPERS */ + function _calculateQualifiedPoints(uint256 diff, uint256 rate, uint256 div) private pure returns (uint256) { + return (diff * rate) / div; + } + function getStakerDetails(uint256 instanceId, address staker) external view returns (Staker memory) { return stakers[instanceId][staker]; } @@ -272,7 +305,7 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS function _setStakingRules( StakingPoints storage stakingPointsInstance, uint256 instanceId, - StakingRule[] calldata stakingRules + StakingRule[] calldata newStakingRules ) private { // delete old rules in map StakingRule[] memory oldRules = stakingPointsInstance.stakingRules; @@ -281,13 +314,12 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS } delete stakingPointsInstance.stakingRules; StakingRule[] storage rules = stakingPointsInstance.stakingRules; - uint256 length = stakingRules.length; + uint256 length = newStakingRules.length; for (uint256 i; i < length; ) { - StakingRule memory rule = stakingRules[i]; + StakingRule memory rule = newStakingRules[i]; require(rule.tokenAddress != address(0), "Staking rule: Contract address required"); require((rule.endTime == 0) || (rule.startTime < rule.endTime), "Staking rule: Invalid time range"); - require(rule.timeUnit > 0, "Staking rule: Invalid timeUnit"); - require(rule.pointsRate > 0, "Staking rule: Invalid points rate"); + require(rule.pointsRatePerDay > 0, "Staking rule: Invalid points rate"); rules.push(rule); _stakingRules[instanceId][rule.tokenAddress] = rule; unchecked { diff --git a/test/manifold/stakingpoints721.js b/test/manifold/stakingpoints721.js index 9ea1028b..438a4d6f 100644 --- a/test/manifold/stakingpoints721.js +++ b/test/manifold/stakingpoints721.js @@ -2,20 +2,13 @@ const truffleAssert = require("truffle-assertions"); const ERC721Creator = artifacts.require("@manifoldxyz/creator-core-extensions-solidity/ERC721Creator"); const ERC721StakingPoints = artifacts.require("ERC721StakingPoints"); const ERC1155Creator = artifacts.require("@manifoldxyz/creator-core-extensions-solidity/ERC1155Creator"); -const keccak256 = require("keccak256"); -const ethers = require("ethers"); const MockManifoldMembership = artifacts.require("MockManifoldMembership"); -const ERC721 = artifacts.require("MockERC721"); -const ERC1155 = artifacts.require("MockERC1155"); - -const STAKE_FEE = ethers.BigNumber.from("690000000000000"); -const MULTI_STAKE_FEE = ethers.BigNumber.from("990000000000000"); contract("ERC721StakingPoints", function ([...accounts]) { const [owner, anotherOwner, anyone1, anyone2] = accounts; describe("StakingPoints", function () { - let creator, fee; + let creator; beforeEach(async function () { creator = await ERC721Creator.new("Test", "TEST", { from: owner }); @@ -52,8 +45,7 @@ contract("ERC721StakingPoints", function ([...accounts]) { stakingRules: [ { tokenAddress: manifoldMembership.address, - pointsRate: 1234, - timeUnit: 100000, + pointsRatePerDay: 1234, startTime: 1674768875, endTime: 1682541275, }, @@ -63,7 +55,7 @@ contract("ERC721StakingPoints", function ([...accounts]) { ), "Wallet is not an admin" ); - // has invalid staking rule (missing pointsRate value) + // has invalid staking rule (missing pointsRatePerDay value) await truffleAssert.reverts( stakingPoints721.initializeStakingPoints( creator.address, @@ -73,8 +65,7 @@ contract("ERC721StakingPoints", function ([...accounts]) { stakingRules: [ { tokenAddress: manifoldMembership.address, - pointsRate: 0, - timeUnit: 100000, + pointsRatePerDay: 0, startTime: 1674768875, endTime: 1682541275, }, @@ -84,27 +75,6 @@ contract("ERC721StakingPoints", function ([...accounts]) { ), "Staking rule: Invalid points rate" ); - // has invalid staking rule (missing timeUnit value) - await truffleAssert.reverts( - stakingPoints721.initializeStakingPoints( - creator.address, - 1, - { - paymentReceiver: owner, - stakingRules: [ - { - tokenAddress: manifoldMembership.address, - pointsRate: 1234, - timeUnit: 0, - startTime: 1674768875, - endTime: 1682541275, - }, - ], - }, - { from: owner } - ), - "Staking rule: Invalid timeUnit" - ); // has invalid staking rule (endTime is less than startTime) await truffleAssert.reverts( stakingPoints721.initializeStakingPoints( @@ -115,8 +85,7 @@ contract("ERC721StakingPoints", function ([...accounts]) { stakingRules: [ { tokenAddress: manifoldMembership.address, - pointsRate: 1234, - timeUnit: 100000, + pointsRatePerDay: 1234, startTime: 1682541275, endTime: 1674768875, }, @@ -136,8 +105,7 @@ contract("ERC721StakingPoints", function ([...accounts]) { stakingRules: [ { tokenAddress: manifoldMembership.address, - pointsRate: 1234, - timeUnit: 100000, + pointsRatePerDay: 1234, startTime: 1674768875, endTime: 1682541275, }, @@ -148,9 +116,6 @@ contract("ERC721StakingPoints", function ([...accounts]) { stakingPointsInstance = await stakingPoints721.getStakingPointsInstance(creator.address, 1); assert.equal(stakingPointsInstance.stakingRules.length, 1); }); - // TODO: - // it("Admin creates new stakingpoints with intial rate, updates rate", function () {}); - // it("Can get stakers", function () {}); it("Will not stake if not owned or approved", async function () { await stakingPoints721.initializeStakingPoints( @@ -161,15 +126,13 @@ contract("ERC721StakingPoints", function ([...accounts]) { stakingRules: [ { tokenAddress: manifoldMembership.address, - pointsRate: 1234, - timeUnit: 100000, + pointsRatePerDay: 1234, startTime: 1674768875, endTime: 1682541275, }, { tokenAddress: mock721.address, - pointsRate: 125, - timeUnit: 100000, + pointsRatePerDay: 125, startTime: 1674768875, endTime: 1682541275, }, @@ -200,22 +163,19 @@ contract("ERC721StakingPoints", function ([...accounts]) { stakingRules: [ { tokenAddress: manifoldMembership.address, - pointsRate: 1234, - timeUnit: 100000, + pointsRatePerDay: 1234, startTime: 1674768875, endTime: 1682541275, }, { tokenAddress: mock721.address, - pointsRate: 125, - timeUnit: 100000, + pointsRatePerDay: 125, startTime: 1674768875, endTime: 1682541275, }, { tokenAddress: mock721_2.address, - pointsRate: 125, - timeUnit: 100000, + pointsRatePerDay: 125, startTime: 1674768875, endTime: 1682541275, }, @@ -262,22 +222,19 @@ contract("ERC721StakingPoints", function ([...accounts]) { stakingRules: [ { tokenAddress: manifoldMembership.address, - pointsRate: 1234, - timeUnit: 100000, + pointsRatePerDay: 1234, startTime: 1674768875, endTime: 1682541275, }, { tokenAddress: mock721.address, - pointsRate: 125, - timeUnit: 100000, + pointsRatePerDay: 125, startTime: 1674768875, endTime: 1682541275, }, { tokenAddress: mock721_2.address, - pointsRate: 125, - timeUnit: 100000, + pointsRatePerDay: 125, startTime: 1674768875, endTime: 1682541275, }, @@ -366,58 +323,107 @@ contract("ERC721StakingPoints", function ([...accounts]) { assert.equal(stakerDetails.stakersTokens[2].timeUnstaked, token3.timeUnstaked); }); - // it("Redeems points", async function () { - // await stakingPoints721.initializeStakingPoints( - // creator.address, - // 1, - // { - // paymentReceiver: owner, - // stakingRules: [ - // { - // tokenAddress: manifoldMembership.address, - // pointsRate: 1234, - // timeUnit: 100000, - // startTime: 1674768875, - // endTime: 1682541275, - // }, - // { - // tokenAddress: mock721.address, - // pointsRate: 125, - // timeUnit: 100000, - // startTime: 1674768875, - // endTime: 1682541275, - // }, - // { - // tokenAddress: mock721_2.address, - // pointsRate: 125, - // timeUnit: 100000, - // startTime: 1674768875, - // endTime: 1682541275, - // }, - // ], - // }, - // { from: owner } - // ); - // await mock721.setApprovalForAll(stakingPoints721.address, true, { from: anyone2 }); - // await mock721_2.setApprovalForAll(stakingPoints721.address, true, { from: anyone2 }); - // await stakingPoints721.stakeTokens( - // 1, - // [ - // { - // tokenAddress: mock721.address, - // tokenId: 2, - // }, - // { - // tokenAddress: mock721.address, - // tokenId: 3, - // }, - // { - // tokenAddress: mock721_2.address, - // tokenId: 2, - // }, - // ], - // { from: anyone2 } - // ); - // }); + it("Redeems points", async function () { + await stakingPoints721.initializeStakingPoints( + creator.address, + 1, + { + paymentReceiver: owner, + stakingRules: [ + { + tokenAddress: manifoldMembership.address, + pointsRatePerDay: 1234, + startTime: 1674768875, + endTime: 1682541275, + }, + { + tokenAddress: mock721.address, + pointsRatePerDay: 125, + startTime: 1674768875, + endTime: 1682541275, + }, + { + tokenAddress: mock721_2.address, + pointsRatePerDay: 120, + startTime: 1674768875, + endTime: 1682541275, + }, + ], + }, + { from: owner } + ); + await mock721.setApprovalForAll(stakingPoints721.address, true, { from: anyone2 }); + await mock721_2.setApprovalForAll(stakingPoints721.address, true, { from: anyone2 }); + await mock721_2.setApprovalForAll(stakingPoints721.address, true, { from: anotherOwner }); + await mock721_2.setApprovalForAll(stakingPoints721.address, true, { from: anyone1 }); + await mock721.setApprovalForAll(stakingPoints721.address, true, { from: anyone1 }); + await stakingPoints721.stakeTokens( + 1, + [ + { + tokenAddress: mock721.address, + tokenId: 2, + }, + { + tokenAddress: mock721.address, + tokenId: 3, + }, + { + tokenAddress: mock721_2.address, + tokenId: 2, + }, + ], + { from: anyone2 } + ); + + await stakingPoints721.stakeTokens( + 1, + [ + { + tokenAddress: mock721_2.address, + tokenId: 3, + }, + ], + { from: anotherOwner } + ); + await stakingPoints721.stakeTokens( + 1, + [ + { + tokenAddress: mock721.address, + tokenId: 1, + }, + { + tokenAddress: mock721_2.address, + tokenId: 1, + }, + ], + { from: anyone1 } + ); + + await stakingPoints721.unstakeTokens( + 1, + [ + { + tokenAddress: mock721_2.address, + tokenId: 2, + }, + ], + { from: anyone2 } + ); + + let user1 = await stakingPoints721.getStakerDetails(1, anyone1); + let user2 = await stakingPoints721.getStakerDetails(1, anyone2); + assert.equal(0, user1.pointsRedeemed); + assert.equal(0, user2.pointsRedeemed); + + await stakingPoints721.redeemPoints(1, { from: anyone1 }); + await stakingPoints721.redeemPoints(1, { from: anyone2 }); + + let user1Updated = await stakingPoints721.getStakerDetails(1, anyone1); + let user2Updated = await stakingPoints721.getStakerDetails(1, anyone2); + assert.equal(true, user1Updated.pointsRedeemed != 0); + assert.equal(true, user2Updated.pointsRedeemed != 0); + }); }); }); From cc211c286489652e09d789425930dc782ea111bf Mon Sep 17 00:00:00 2001 From: nikki-kiga <42276368+nikki-kiga@users.noreply.github.com> Date: Sat, 29 Apr 2023 10:37:00 -0700 Subject: [PATCH 5/7] update redeem on unstake and mappings --- .../manifold/staking/ERC721StakingPoints.sol | 41 ++- .../manifold/staking/IERC721StakingPoints.sol | 12 +- .../manifold/staking/IStakingPointsCore.sol | 38 ++- .../manifold/staking/StakingPointsCore.sol | 289 ++++++++++++------ test/manifold/stakingpoints721.js | 190 +++++++++--- 5 files changed, 425 insertions(+), 145 deletions(-) diff --git a/contracts/manifold/staking/ERC721StakingPoints.sol b/contracts/manifold/staking/ERC721StakingPoints.sol index 5ec783f1..8ebfbd5d 100644 --- a/contracts/manifold/staking/ERC721StakingPoints.sol +++ b/contracts/manifold/staking/ERC721StakingPoints.sol @@ -17,11 +17,8 @@ import "./IStakingPointsCore.sol"; * @notice logic for Staking Points for ERC721 extension. */ contract ERC721StakingPoints is StakingPointsCore, IERC721StakingPoints { - - uint256 public totalPointsCirculation; - - // { instanceId => { walletAddress => points } } - mapping(uint256 => mapping(address => uint256)) public stakerPoints; + // { creatorContractAddress => {instanceId => uint256 } } + mapping(address => mapping(uint256 => uint256)) public totalPointsClaimed; function supportsInterface(bytes4 interfaceId) public view virtual override(StakingPointsCore, IERC165) returns (bool) { return interfaceId == type(IERC721StakingPoints).interfaceId || super.supportsInterface(interfaceId); @@ -41,12 +38,34 @@ contract ERC721StakingPoints is StakingPointsCore, IERC721StakingPoints { require(version <= 255, "Unsupported contract version"); creatorContractVersion = uint8(version); } catch {} - + StakingPoints storage instance = _stakingPointsInstances[creatorContractAddress][instanceId]; + require(instance.paymentReceiver == address(0), "StakingPoints already initialized"); _initialize(creatorContractAddress, creatorContractVersion, instanceId, stakingPointsParams); emit StakingPointsInitialized(creatorContractAddress, instanceId, msg.sender); } + function updateStakingPoints( + address creatorContractAddress, + uint256 instanceId, + StakingPointsParams calldata stakingPointsParams + ) external creatorAdminRequired(creatorContractAddress) nonReentrant { + // Max uint56 for instanceId + require(instanceId > 0 && instanceId <= MAX_UINT_56, "Invalid instanceId"); + require(stakingPointsParams.paymentReceiver != address(0), "Cannot update without payment receiver"); + + uint8 creatorContractVersion; + try IERC721CreatorCoreVersion(creatorContractAddress).VERSION() returns (uint256 version) { + require(version <= 255, "Unsupported contract version"); + creatorContractVersion = uint8(version); + } catch {} + StakingPoints storage instance = _stakingPointsInstances[creatorContractAddress][instanceId]; + require(instance.stakers.length == (0), "StakingPoints cannot be updated when 1 or more wallets have staked"); + _update(creatorContractAddress, creatorContractVersion, instanceId, stakingPointsParams); + + emit StakingPointsUpdated(creatorContractAddress, instanceId, msg.sender); + } + /** * @dev was originally using safeTransferFrom but was getting a reentrancy error */ @@ -72,8 +91,12 @@ contract ERC721StakingPoints is StakingPointsCore, IERC721StakingPoints { /** * @dev */ - function _redeem(uint256 instanceId, uint256 pointsAmount, address redeemer) internal override { - totalPointsCirculation = SafeMath.add(totalPointsCirculation, pointsAmount); - stakerPoints[instanceId][redeemer]; + function _redeem( + address creatorContractAddress, + uint256 instanceId, + uint256 pointsAmount + ) internal override { + uint256 currTotal = totalPointsClaimed[creatorContractAddress][instanceId]; + totalPointsClaimed[creatorContractAddress][instanceId] = currTotal + pointsAmount; } } diff --git a/contracts/manifold/staking/IERC721StakingPoints.sol b/contracts/manifold/staking/IERC721StakingPoints.sol index 3aa0f6a7..cf6d395c 100644 --- a/contracts/manifold/staking/IERC721StakingPoints.sol +++ b/contracts/manifold/staking/IERC721StakingPoints.sol @@ -8,7 +8,6 @@ import "./IStakingPointsCore.sol"; import "./StakingPointsCore.sol"; interface IERC721StakingPoints is IStakingPointsCore { - /** * @notice initialize a new staking points, emit initialize event * @param creatorContractAddress the address of the creator contract @@ -21,4 +20,15 @@ interface IERC721StakingPoints is IStakingPointsCore { StakingPointsParams calldata stakingPointsParams ) external; + /** + * @notice update existing staking points, emit update event + * @param creatorContractAddress the address of the creator contract + * @param instanceId the instanceId of the staking points for the creator contract + * @param stakingPointsParams the stakingPointsParams object + */ + function updateStakingPoints( + address creatorContractAddress, + uint256 instanceId, + StakingPointsParams calldata stakingPointsParams + ) external; } diff --git a/contracts/manifold/staking/IStakingPointsCore.sol b/contracts/manifold/staking/IStakingPointsCore.sol index cc32517c..d6a21579 100644 --- a/contracts/manifold/staking/IStakingPointsCore.sol +++ b/contracts/manifold/staking/IStakingPointsCore.sol @@ -22,7 +22,12 @@ interface IStakingPointsCore is IERC165, IERC721Receiver { address stakerAddress; uint256 timeStaked; uint256 timeUnstaked; - uint256 stakerTokenIdx; + uint256 tokenIdx; + } + + struct StakedTokenIdx { + uint256 tokenIdx; + address stakerAddress; } struct StakedTokenParams { @@ -32,7 +37,7 @@ interface IStakingPointsCore is IERC165, IERC721Receiver { struct Staker { uint256 pointsRedeemed; - uint256 stakerTokenIdx; + uint256 stakerIdx; StakedToken[] stakersTokens; } @@ -47,6 +52,7 @@ interface IStakingPointsCore is IERC165, IERC721Receiver { address payable paymentReceiver; uint8 contractVersion; StakingRule[] stakingRules; + Staker[] stakers; } struct StakingPointsParams { @@ -55,29 +61,39 @@ interface IStakingPointsCore is IERC165, IERC721Receiver { } event StakingPointsInitialized(address indexed creatorContract, uint256 indexed instanceId, address initializer); - event StakingPointsUpdated(address indexed creatorContract, uint256 indexed instanceId); - event TokensStaked(uint256 indexed instanceId, StakedToken[] stakedTokens, address owner); - event TokensUnstaked(uint256 indexed instanceId, StakedToken[] stakedTokens, address owner); - event PointsDistributed(); + event StakingPointsUpdated(address indexed creatorContract, uint256 indexed instanceId, address updater); + event TokensStaked(address indexed creatorContract, uint256 indexed instanceId, StakedToken[] stakedTokens, address owner); + event TokensUnstaked(address indexed creatorContract, uint256 indexed instanceId, StakedToken[] stakedTokens, address owner); + event PointsDistributed(address indexed creatorContract, uint256 indexed instanceId, address user, uint256 amount); /** * @notice stake tokens - * @param instanceId the instanceId of the staking points for the creator contract + * @param creatorContractAddress the address of the creator contract + * @param instanceId the staking points instanceId for the creator contract * @param stakingTokens a list of tokenIds with token contract addresses */ - function stakeTokens(uint256 instanceId, StakedTokenParams[] calldata stakingTokens) external; + function stakeTokens( + address creatorContractAddress, + uint256 instanceId, + StakedTokenParams[] calldata stakingTokens + ) external; /** * @notice unstake tokens - * @param instanceId the instanceId of the staking points for the creator contract + * @param creatorContractAddress the address of the creator contract + * @param instanceId the staking points instanceId for the creator contract * @param unstakingTokens a list of tokenIds with token contract addresses */ - function unstakeTokens(uint256 instanceId, StakedTokenParams[] calldata unstakingTokens) external; + function unstakeTokens( + address creatorContractAddress, + uint256 instanceId, + StakedTokenParams[] calldata unstakingTokens + ) external; /** * @notice get a staking points instance corresponding to a creator contract and instanceId * @param creatorContractAddress the address of the creator contract - * @param instanceId the instanceId of the staking points for the creator contract + * @param instanceId the staking points instanceId for the creator contract * @return StakingPoints the staking points object */ function getStakingPointsInstance( diff --git a/contracts/manifold/staking/StakingPointsCore.sol b/contracts/manifold/staking/StakingPointsCore.sol index 99eadfcd..a6502a28 100644 --- a/contracts/manifold/staking/StakingPointsCore.sol +++ b/contracts/manifold/staking/StakingPointsCore.sol @@ -29,27 +29,24 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS // { creatorContractAddress => { instanceId => StakingPoints } } mapping(address => mapping(uint256 => StakingPoints)) internal _stakingPointsInstances; - // { instanceId => { walletAddress => Staker } } - mapping(uint256 => mapping(address => Staker)) public stakers; + // { creatorContractAddress => { instanceId => { walletAddress => stakerIdx } } } + mapping(address => mapping(uint256 => mapping(address => uint256))) internal _stakerIdxs; - // { instanceId => { tokenAddress => StakingRule } } - mapping(uint256 => mapping(address => StakingRule)) internal _stakingRules; + // { creatorContractAddress => { instanceId => { tokenAddress => ruleIdx } } } + mapping(address => mapping(uint256 => mapping(address => uint256))) internal _stakingRulesIdxs; - // { walletAddress => { tokenAddress => { tokenId => StakedToken } } } - mapping(address => mapping(address => mapping(uint256 => StakedToken))) public userStakedTokens; + // { walletAddress => { tokenAddress => { tokenId => StakedTokenIdx } } } + mapping(address => mapping(address => mapping(uint256 => StakedTokenIdx))) internal _stakedTokenIdxs; - // { walletAddress => bool} - mapping(address => bool) internal _isStakerIndexed; + // { creatorContractAddress => {instanceId => { walletAddress => bool } } } + mapping(address => mapping(uint256 => mapping(address => bool))) internal _isStakerIndexed; address public manifoldMembershipContract; - address[] public stakersAddresses; - function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165, AdminControl) returns (bool) { return interfaceId == type(IStakingPointsCore).interfaceId || interfaceId == type(IERC721Receiver).interfaceId || - // interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); } @@ -72,17 +69,25 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS uint256 instanceId, StakingPointsParams calldata stakingPointsParams ) internal { - StakingPoints storage stakingPointsInstance = _stakingPointsInstances[creatorContractAddress][instanceId]; - require(stakingPointsInstance.paymentReceiver == address(0), "StakingPoints already initialized"); + _update(creatorContractAddress, creatorContractVersion, instanceId, stakingPointsParams); + } + + /** + * Updates a stakingPionts with params + */ + function _update( + address creatorContractAddress, + uint8 creatorContractVersion, + uint256 instanceId, + StakingPointsParams calldata stakingPointsParams + ) internal { + StakingPoints storage instance = _stakingPointsInstances[creatorContractAddress][instanceId]; _validateStakingPointsParams(stakingPointsParams); - stakingPointsInstance.paymentReceiver = stakingPointsParams.paymentReceiver; - stakingPointsInstance.contractVersion = creatorContractVersion; + instance.paymentReceiver = stakingPointsParams.paymentReceiver; + instance.contractVersion = creatorContractVersion; require(stakingPointsParams.stakingRules.length > 0, "Needs at least one stakingRule"); - _setStakingRules(stakingPointsInstance, instanceId, stakingPointsParams.stakingRules); - - emit StakingPointsInitialized(creatorContractAddress, instanceId, msg.sender); + _setStakingRules(creatorContractAddress, instance, instanceId, stakingPointsParams.stakingRules); } - /** * Abstract helper to transfer tokens. To be implemented by inheriting contracts. @@ -94,45 +99,81 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS */ function _transferBack(address contractAddress, uint256 tokenId, address fromAddress, address toAddress) internal virtual; + /** + * Abstract helper to redeem points. To be implemented by inheriting contracts. + */ + function _redeem( + address creatorContractAddress, + uint256 instanceId, + uint256 pointsAmount + ) internal virtual; + /** STAKING */ - function stakeTokens(uint256 instanceId, StakedTokenParams[] calldata stakingTokens) external nonReentrant { - _stakeTokens(instanceId, stakingTokens); + function stakeTokens( + address creatorContractAddress, + uint256 instanceId, + StakedTokenParams[] calldata stakingTokens + ) external nonReentrant { + _stakeTokens(creatorContractAddress, instanceId, stakingTokens); } - function _stakeTokens(uint256 instanceId, StakedTokenParams[] calldata stakingTokens) private { - bool isIndexed = _isStakerIndexed[msg.sender]; + function _stakeTokens( + address creatorContractAddress, + uint256 instanceId, + StakedTokenParams[] calldata stakingTokens + ) private { + // get instance + StakingPoints storage instance = _getStakingPointsInstance(creatorContractAddress, instanceId); + bool isIndexed = _isStakerIndexed[creatorContractAddress][instanceId][msg.sender]; if (!isIndexed) { - Staker storage newStaker = stakers[instanceId][msg.sender]; - newStaker.stakerTokenIdx = stakersAddresses.length; - stakersAddresses.push(msg.sender); - _isStakerIndexed[msg.sender] = true; + uint256 newStakerIdx = instance.stakers.length; + instance.stakers.push(); + instance.stakers[newStakerIdx].stakerIdx = newStakerIdx; + // add staker to index map + _stakerIdxs[creatorContractAddress][instanceId][msg.sender] = newStakerIdx; + _isStakerIndexed[creatorContractAddress][instanceId][msg.sender] = true; } + StakedToken[] memory newlyStaked = new StakedToken[](stakingTokens.length); - Staker storage user = stakers[instanceId][msg.sender]; - StakedToken[] storage userTokens = user.stakersTokens; + uint256 stakerIdx = _getStakerIdx(creatorContractAddress, instanceId, msg.sender); + StakedToken[] storage userTokens = instance.stakers[stakerIdx].stakersTokens; uint256 length = stakingTokens.length; for (uint256 i = 0; i < length; ) { - StakedToken storage currToken = userStakedTokens[msg.sender][stakingTokens[i].tokenAddress][stakingTokens[i].tokenId]; - require(currToken.stakerAddress == address(0), "Token already staked"); + StakedTokenIdx storage currStakedTokenIdx = _stakedTokenIdxs[msg.sender][stakingTokens[i].tokenAddress][ + stakingTokens[i].tokenId + ]; + require(currStakedTokenIdx.stakerAddress == address(0), "Token already staked"); + StakedToken memory currToken; currToken.tokenId = stakingTokens[i].tokenId; currToken.contractAddress = stakingTokens[i].tokenAddress; currToken.stakerAddress = msg.sender; currToken.timeStaked = block.timestamp; - currToken.stakerTokenIdx = userTokens.length; - userStakedTokens[msg.sender][stakingTokens[i].tokenAddress][stakingTokens[i].tokenId] = currToken; + currToken.tokenIdx = userTokens.length; + + _stakedTokenIdxs[msg.sender][stakingTokens[i].tokenAddress][stakingTokens[i].tokenId] = StakedTokenIdx( + currToken.tokenIdx, + msg.sender + ); userTokens.push(currToken); newlyStaked[i] = currToken; - _stake(instanceId, stakingTokens[i].tokenAddress, stakingTokens[i].tokenId); + + _stake(creatorContractAddress, instanceId, instance, stakingTokens[i].tokenAddress, stakingTokens[i].tokenId); unchecked { ++i; } } - emit TokensStaked(instanceId, newlyStaked, msg.sender); + emit TokensStaked(creatorContractAddress, instanceId, newlyStaked, msg.sender); } - function _stake(uint256 instanceId, address tokenAddress, uint256 tokenId) private { - StakingRule memory ruleExists = _getTokenRule(instanceId, tokenAddress); + function _stake( + address creatorContractAddress, + uint256 instanceId, + StakingPoints storage stakingPointsInstance, + address tokenAddress, + uint256 tokenId + ) private { + StakingRule memory ruleExists = _getTokenRule(creatorContractAddress, instanceId, stakingPointsInstance, tokenAddress); require(ruleExists.tokenAddress == tokenAddress, "Token does not match existing rule"); _transfer(tokenAddress, tokenId, msg.sender, address(this)); @@ -140,75 +181,129 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS /** UNSTAKING */ - function unstakeTokens(uint256 instanceId, StakedTokenParams[] calldata unstakingTokens) external nonReentrant { - _unstakeTokens(instanceId, unstakingTokens); + function unstakeTokens( + address creatorContractAddress, + uint256 instanceId, + StakedTokenParams[] calldata unstakingTokens + ) external nonReentrant { + _unstakeTokens(creatorContractAddress, instanceId, unstakingTokens); } - function _unstakeTokens(uint256 instanceId, StakedTokenParams[] calldata unstakingTokens) private { + function _unstakeTokens( + address creatorContractAddress, + uint256 instanceId, + StakedTokenParams[] calldata unstakingTokens + ) private { require(unstakingTokens.length != 0, "Cannot unstake 0 tokens"); + StakingPoints storage instance = _getStakingPointsInstance(creatorContractAddress, instanceId); StakedToken[] memory unstakedTokens = new StakedToken[](unstakingTokens.length); + uint256 stakerIdx = _getStakerIdx(creatorContractAddress, instanceId, msg.sender); + for (uint256 i = 0; i < unstakingTokens.length; ++i) { - StakedToken storage currToken = userStakedTokens[msg.sender][unstakingTokens[i].tokenAddress][ + StakedTokenIdx storage currStakedTokenIdx = _stakedTokenIdxs[msg.sender][unstakingTokens[i].tokenAddress][ unstakingTokens[i].tokenId ]; + StakedToken storage currToken = instance.stakers[stakerIdx].stakersTokens[currStakedTokenIdx.tokenIdx]; require( msg.sender != address(0) && msg.sender == currToken.stakerAddress, "No sender address or not the original staker" ); currToken.timeUnstaked = block.timestamp; - stakers[instanceId][msg.sender].stakersTokens[currToken.stakerTokenIdx] = currToken; + delete _stakedTokenIdxs[msg.sender][unstakingTokens[i].tokenAddress][unstakingTokens[i].tokenId]; unstakedTokens[i] = currToken; _unstakeToken(unstakingTokens[i].tokenAddress, unstakingTokens[i].tokenId); } - emit TokensUnstaked(instanceId, unstakedTokens, msg.sender); + // add redeem functionality if staker has not redeemed past qualifying amount for unstaking tokens + // ensure that staker is coming from right place + Staker storage staker = instance.stakers[stakerIdx]; + uint256 totalUnstakingTokensPoints = _calculateTotalQualifyingPoints(creatorContractAddress, instanceId, unstakedTokens); + uint256 diffRedeemed = totalUnstakingTokensPoints - staker.pointsRedeemed; + + if (diffRedeemed > 0) { + _redeemPointsAmount(creatorContractAddress, instanceId, diffRedeemed); + } + emit TokensUnstaked(creatorContractAddress, instanceId, unstakedTokens, msg.sender); } + /** + * @dev assumes that fn that calls protects against sender not matching original owner + */ function _unstakeToken(address tokenAddress, uint256 tokenId) private { - StakedToken storage userToken = userStakedTokens[msg.sender][tokenAddress][tokenId]; - require( - msg.sender != address(0) && msg.sender == userToken.stakerAddress, - "No sender address or not the original staker" - ); _transferBack(tokenAddress, tokenId, address(this), msg.sender); } - /** - * Abstract helper to redeem points. To be implemented by inheriting contracts. - */ - function _redeem(uint256 instanceId, uint256 pointsAmount, address redeemer) internal virtual; + function redeemPoints(address creatorContractAddress, uint256 instanceId) external nonReentrant { + _redeemPoints(creatorContractAddress, instanceId); + } + + function _redeemPoints(address creatorContractAddress, uint256 instanceId) private { + StakingPoints storage instance = _getStakingPointsInstance(creatorContractAddress, instanceId); + uint256 stakerIdx = _getStakerIdx(creatorContractAddress, instanceId, msg.sender); - function redeemPoints(uint256 instanceId) external nonReentrant { - // guard against more than calculated - Staker storage staker = stakers[instanceId][msg.sender]; - uint256 totalQualifyingPoints = _calculatePoints(instanceId, staker.stakersTokens); - uint256 diff = SafeMath.sub(totalQualifyingPoints, staker.pointsRedeemed); + Staker storage staker = instance.stakers[stakerIdx]; + uint256 totalQualifyingPoints = _calculateTotalQualifyingPoints( + creatorContractAddress, + instanceId, + staker.stakersTokens + ); + uint256 diff = totalQualifyingPoints - staker.pointsRedeemed; require(totalQualifyingPoints != 0 && diff >= 0, "Need more than zero points"); // compare with pointsRedeemd staker.pointsRedeemed = totalQualifyingPoints; - _redeem(instanceId, diff, msg.sender); + _redeem(creatorContractAddress, instanceId, diff); + emit PointsDistributed(creatorContractAddress, instanceId, msg.sender, diff); + } + + /** + * @dev assumes that the sender is qualified to redeem amount and not in excess of points already redeemed + */ + function _redeemPointsAmount(address creatorContractAddress, uint256 instanceId, uint256 amount) private { + _redeem(creatorContractAddress, instanceId, amount); + emit PointsDistributed(creatorContractAddress, instanceId, msg.sender, amount); + } + + function getPointsForWallet( + address creatorContractAddress, + uint256 instanceId, + address walletAddress + ) external view returns (uint256 totalPoints, uint256 diff) { + return _getPointsForWallet(creatorContractAddress, instanceId, walletAddress); } - function getPoints(uint256 instanceId) external view returns (uint256 totalPoints, uint256 diff) { - Staker storage staker = stakers[instanceId][msg.sender]; - totalPoints = _calculatePoints(instanceId, staker.stakersTokens); - diff = SafeMath.sub(totalPoints, staker.pointsRedeemed); + function _getPointsForWallet( + address creatorContractAddress, + uint256 instanceId, + address walletAddress + ) private view returns (uint256 totalPoints, uint256 diff) { + StakingPoints storage instance = _getStakingPointsInstance(creatorContractAddress, instanceId); + uint256 stakerIdx = _getStakerIdx(creatorContractAddress, instanceId, walletAddress); + Staker storage staker = instance.stakers[stakerIdx]; + totalPoints = _calculateTotalQualifyingPoints(creatorContractAddress, instanceId, staker.stakersTokens); + diff = totalPoints - staker.pointsRedeemed; } /** * @notice assumes points */ - function _calculatePoints(uint256 instanceId, StakedToken[] memory stakingTokens) private view returns (uint256 points) { + function _calculateTotalQualifyingPoints( + address creatorContractAddress, + uint256 instanceId, + StakedToken[] memory stakingTokens + ) private view returns (uint256 points) { uint256 length = stakingTokens.length; + StakingPoints storage instance = _getStakingPointsInstance(creatorContractAddress, instanceId); + StakingRule[] storage rules = instance.stakingRules; for (uint256 i = 0; i < length; ) { - StakingRule storage rule = _stakingRules[instanceId][stakingTokens[i].contractAddress]; + uint256 ruleIdx = _stakingRulesIdxs[creatorContractAddress][instanceId][stakingTokens[i].contractAddress]; + StakingRule storage rule = rules[ruleIdx]; require(rule.startTime >= 0 && rule.endTime >= 0, "Invalid rule values"); uint256 tokenEnd = stakingTokens[i].timeUnstaked == 0 ? block.timestamp : stakingTokens[i].timeUnstaked; uint256 start = Math.max(rule.startTime, stakingTokens[i].timeStaked); uint256 end = Math.min(tokenEnd, rule.endTime); uint256 diff = start - end; - uint256 qualified = _calculateQualifiedPoints(diff, rule.pointsRatePerDay, 86400); + uint256 qualified = _calculateTotalPoints(diff, rule.pointsRatePerDay, 86400); points = points + qualified; unchecked { @@ -219,28 +314,26 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS /** VIEW HELPERS */ - function _calculateQualifiedPoints(uint256 diff, uint256 rate, uint256 div) private pure returns (uint256) { - return (diff * rate) / div; - } - - function getStakerDetails(uint256 instanceId, address staker) external view returns (Staker memory) { - return stakers[instanceId][staker]; - } - - function _getTokenRule(uint256 instanceId, address tokenAddress) internal view returns (StakingRule memory) { - return _stakingRules[instanceId][tokenAddress]; + function getStaker( + address creatorContractAddress, + uint256 instanceId, + address stakerAddress + ) external view returns (Staker memory) { + uint256 stakerIdx = _getStakerIdx(creatorContractAddress, instanceId, stakerAddress); + StakingPoints storage instance = _getStakingPointsInstance(creatorContractAddress, instanceId); + return instance.stakers[stakerIdx]; } - function getUserStakedToken( - address wallet, - address tokenAddress, - uint256 tokenId - ) external view returns (StakedToken memory) { - return userStakedTokens[wallet][tokenAddress][tokenId]; + function _getTokenRule( + address creatorContractAddress, + uint256 instanceId, + StakingPoints storage stakingPointsInstance, + address tokenAddress + ) internal view returns (StakingRule memory) { + uint256 ruleIdx = _stakingRulesIdxs[creatorContractAddress][instanceId][tokenAddress]; + return stakingPointsInstance.stakingRules[ruleIdx]; } - /** HELPERS */ - /** * See {IStakingPointsCore-getStakingPointsInstance}. */ @@ -254,8 +347,12 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS /** * Helper to get Staker */ - function _getStaker(uint256 instanceId, address walletAddress) internal view returns (Staker memory staker) { - staker = stakers[instanceId][walletAddress]; + function _getStakerIdx( + address creatorContractAddress, + uint256 instanceId, + address walletAddress + ) internal view returns (uint256) { + return _stakerIdxs[creatorContractAddress][instanceId][walletAddress]; } /** @@ -269,6 +366,12 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS require(stakingPointsInstance.paymentReceiver != address(0), "Staking points not initialized"); } + /** HELPERS */ + + function _calculateTotalPoints(uint256 diff, uint256 rate, uint256 div) private pure returns (uint256) { + return (diff * rate) / div; + } + /** * @dev See {IStakingPointsCore-recoverERC721}. */ @@ -289,7 +392,9 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS return this.onERC721Received.selector; } - function _onERC721Received(address from, uint256 id, bytes calldata data) private {} + function _onERC721Received(address from, uint256 id, bytes calldata data) private { + /** TODO attempt to stake */ + } /** * @dev See {IstakingPointsCore-setManifoldMembership}. @@ -303,14 +408,18 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS } function _setStakingRules( + address creatorContractAddress, StakingPoints storage stakingPointsInstance, uint256 instanceId, StakingRule[] calldata newStakingRules ) private { - // delete old rules in map StakingRule[] memory oldRules = stakingPointsInstance.stakingRules; - for (uint256 i; i < oldRules.length; ) { - delete _stakingRules[instanceId][oldRules[i].tokenAddress]; + uint256 oldLength = oldRules.length; + for (uint256 i; i < oldLength; ) { + delete _stakingRulesIdxs[creatorContractAddress][instanceId][oldRules[i].tokenAddress]; + unchecked { + ++i; + } } delete stakingPointsInstance.stakingRules; StakingRule[] storage rules = stakingPointsInstance.stakingRules; @@ -320,8 +429,8 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS require(rule.tokenAddress != address(0), "Staking rule: Contract address required"); require((rule.endTime == 0) || (rule.startTime < rule.endTime), "Staking rule: Invalid time range"); require(rule.pointsRatePerDay > 0, "Staking rule: Invalid points rate"); + _stakingRulesIdxs[creatorContractAddress][instanceId][rule.tokenAddress] = rules.length; rules.push(rule); - _stakingRules[instanceId][rule.tokenAddress] = rule; unchecked { ++i; } diff --git a/test/manifold/stakingpoints721.js b/test/manifold/stakingpoints721.js index 438a4d6f..932ad425 100644 --- a/test/manifold/stakingpoints721.js +++ b/test/manifold/stakingpoints721.js @@ -30,9 +30,6 @@ contract("ERC721StakingPoints", function ([...accounts]) { await creator.registerExtension(stakingPoints721.address, { from: owner }); }); - // edge cases - can user withdraw points without unstaking? - // non-creator or owner cannot change rules - // can admin update the rules / rates while there is an affected token(?) it("Admin creates new stakingpoints with rate", async function () { // must be an admin @@ -117,6 +114,121 @@ contract("ERC721StakingPoints", function ([...accounts]) { assert.equal(stakingPointsInstance.stakingRules.length, 1); }); + it("Will update a staking points instance if there are not any stakers", async function () { + await stakingPoints721.initializeStakingPoints( + creator.address, + 1, + { + paymentReceiver: owner, + stakingRules: [ + { + tokenAddress: manifoldMembership.address, + pointsRatePerDay: 2222, + startTime: 1674768875, + endTime: 1682541275, + }, + { + tokenAddress: mock721.address, + pointsRatePerDay: 1111, + startTime: 1674768875, + endTime: 1682541275, + }, + ], + }, + { from: owner } + ); + + await truffleAssert.reverts( + stakingPoints721.updateStakingPoints( + creator.address, + 1, + { + paymentReceiver: owner, + stakingRules: [ + { + tokenAddress: manifoldMembership.address, + pointsRatePerDay: 3333, + startTime: 1674768875, + endTime: 1682541275, + }, + { + tokenAddress: mock721.address, + pointsRatePerDay: 4444, + startTime: 1674768875, + endTime: 1682541275, + }, + ], + }, + { from: anyone2 } + ), + "Wallet is not an admin" + ); + + //update + await stakingPoints721.updateStakingPoints( + creator.address, + 1, + { + paymentReceiver: owner, + stakingRules: [ + { + tokenAddress: manifoldMembership.address, + pointsRatePerDay: 1234, + startTime: 1674768875, + endTime: 1682541275, + }, + { + tokenAddress: mock721.address, + pointsRatePerDay: 125, + startTime: 1674768875, + endTime: 1682541275, + }, + ], + }, + { from: owner } + ); + + // someone stakes + await mock721.setApprovalForAll(stakingPoints721.address, true, { from: anyone2 }); + await stakingPoints721.stakeTokens( + creator.address, + 1, + [ + { + tokenAddress: mock721.address, + tokenId: 2, + }, + ], + { from: anyone2 } + ); + + // unable to update + await truffleAssert.reverts( + stakingPoints721.updateStakingPoints( + creator.address, + 1, + { + paymentReceiver: owner, + stakingRules: [ + { + tokenAddress: manifoldMembership.address, + pointsRatePerDay: 3333, + startTime: 1674768875, + endTime: 1682541275, + }, + { + tokenAddress: mock721.address, + pointsRatePerDay: 4444, + startTime: 1674768875, + endTime: 1682541275, + }, + ], + }, + { from: owner } + ), + "StakingPoints cannot be updated when 1 or more wallets have staked" + ); + }); it("Will not stake if not owned or approved", async function () { await stakingPoints721.initializeStakingPoints( creator.address, @@ -142,6 +254,7 @@ contract("ERC721StakingPoints", function ([...accounts]) { ); await truffleAssert.reverts( stakingPoints721.stakeTokens( + creator.address, 1, [ { @@ -186,6 +299,7 @@ contract("ERC721StakingPoints", function ([...accounts]) { await mock721.setApprovalForAll(stakingPoints721.address, true, { from: anyone2 }); await mock721_2.setApprovalForAll(stakingPoints721.address, true, { from: anyone2 }); await stakingPoints721.stakeTokens( + creator.address, 1, [ { @@ -204,14 +318,14 @@ contract("ERC721StakingPoints", function ([...accounts]) { { from: anyone2 } ); - let stakerDetails1 = await stakingPoints721.getStakerDetails(1, anyone2); + let staker = await stakingPoints721.getStaker(creator.address, 1, anyone2); - assert.equal(stakerDetails1.stakersTokens.length, 3); - assert.equal(stakerDetails1.stakersTokens[0].tokenId, 2); - assert.equal(stakerDetails1.stakersTokens[1].tokenId, 3); - assert.equal(stakerDetails1.stakersTokens[1].contractAddress, mock721.address); - assert.equal(stakerDetails1.stakersTokens[2].tokenId, 2); - assert.equal(stakerDetails1.stakersTokens[2].contractAddress, mock721_2.address); + assert.equal(staker.stakersTokens.length, 3); + assert.equal(staker.stakersTokens[0].tokenId, 2); + assert.equal(staker.stakersTokens[1].tokenId, 3); + assert.equal(staker.stakersTokens[1].contractAddress, mock721.address); + assert.equal(staker.stakersTokens[2].tokenId, 2); + assert.equal(staker.stakersTokens[2].contractAddress, mock721_2.address); }); it("Stakes and unstakes", async function () { await stakingPoints721.initializeStakingPoints( @@ -245,6 +359,7 @@ contract("ERC721StakingPoints", function ([...accounts]) { await mock721.setApprovalForAll(stakingPoints721.address, true, { from: anyone2 }); await mock721_2.setApprovalForAll(stakingPoints721.address, true, { from: anyone2 }); await stakingPoints721.stakeTokens( + creator.address, 1, [ { @@ -263,18 +378,23 @@ contract("ERC721StakingPoints", function ([...accounts]) { { from: anyone2 } ); - let stakerDetails1 = await stakingPoints721.getStakerDetails(1, anyone2); + let staker = await stakingPoints721.getStaker(creator.address, 1, anyone2); - assert.equal(stakerDetails1.stakersTokens.length, 3); - assert.equal(stakerDetails1.stakersTokens[0].tokenId, 2); - assert.equal(stakerDetails1.stakersTokens[1].tokenId, 3); - assert.equal(stakerDetails1.stakersTokens[1].contractAddress, mock721.address); - assert.equal(stakerDetails1.stakersTokens[2].tokenId, 2); - assert.equal(stakerDetails1.stakersTokens[2].contractAddress, mock721_2.address); + assert.equal(staker.stakersTokens.length, 3); + assert.equal(staker.stakersTokens[0].tokenId, 2); + assert.equal(staker.stakersTokens[0].timeUnstaked, 0); + assert.equal(staker.stakersTokens[1].tokenId, 3); + assert.equal(staker.stakersTokens[1].contractAddress, mock721.address); + assert.equal(staker.stakersTokens[1].timeUnstaked, 0); + assert.equal(staker.stakersTokens[2].tokenId, 2); + assert.equal(staker.stakersTokens[2].contractAddress, mock721_2.address); + assert.equal(staker.stakersTokens[2].timeUnstaked, 0); + assert.equal(staker.pointsRedeemed, 0); // unstake #1 await truffleAssert.reverts( stakingPoints721.unstakeTokens( + creator.address, 1, [ { @@ -296,6 +416,7 @@ contract("ERC721StakingPoints", function ([...accounts]) { ); await stakingPoints721.unstakeTokens( + creator.address, 1, [ { @@ -310,17 +431,12 @@ contract("ERC721StakingPoints", function ([...accounts]) { { from: anyone2 } ); - let stakerDetails = await stakingPoints721.getStakerDetails(1, anyone2); - assert.equal(stakerDetails.stakersTokens.length, 3); - let token = await stakingPoints721.getUserStakedToken(anyone2, mock721.address, 2); - assert.equal(token.timeUnstaked !== 0, true); - let token2 = await stakingPoints721.getUserStakedToken(anyone2, mock721.address, 3); - assert.equal(token2.timeUnstaked, 0); - let token3 = await stakingPoints721.getUserStakedToken(anyone2, mock721_2.address, 2); - assert.equal(token3.timeUnstaked !== 0, true); - assert.equal(stakerDetails.stakersTokens[0].timeUnstaked, token.timeUnstaked); - assert.equal(stakerDetails.stakersTokens[1].timeUnstaked, token2.timeUnstaked); - assert.equal(stakerDetails.stakersTokens[2].timeUnstaked, token3.timeUnstaked); + let staker_again = await stakingPoints721.getStaker(creator.address, 1, anyone2); + assert.equal(staker_again.stakersTokens.length, 3); + assert.equal(staker_again.stakersTokens[0].timeUnstaked != 0, true); + assert.equal(staker_again.stakersTokens[1].timeUnstaked, 0); + assert.equal(staker_again.stakersTokens[2].timeUnstaked != 0, true); + assert.equal(staker_again.pointsRedeemed !== 0, true); }); it("Redeems points", async function () { @@ -358,6 +474,7 @@ contract("ERC721StakingPoints", function ([...accounts]) { await mock721_2.setApprovalForAll(stakingPoints721.address, true, { from: anyone1 }); await mock721.setApprovalForAll(stakingPoints721.address, true, { from: anyone1 }); await stakingPoints721.stakeTokens( + creator.address, 1, [ { @@ -377,6 +494,7 @@ contract("ERC721StakingPoints", function ([...accounts]) { ); await stakingPoints721.stakeTokens( + creator.address, 1, [ { @@ -387,6 +505,7 @@ contract("ERC721StakingPoints", function ([...accounts]) { { from: anotherOwner } ); await stakingPoints721.stakeTokens( + creator.address, 1, [ { @@ -402,6 +521,7 @@ contract("ERC721StakingPoints", function ([...accounts]) { ); await stakingPoints721.unstakeTokens( + creator.address, 1, [ { @@ -412,18 +532,20 @@ contract("ERC721StakingPoints", function ([...accounts]) { { from: anyone2 } ); - let user1 = await stakingPoints721.getStakerDetails(1, anyone1); - let user2 = await stakingPoints721.getStakerDetails(1, anyone2); + let user1 = await stakingPoints721.getStaker(creator.address, 1, anyone1); + let user2 = await stakingPoints721.getStaker(creator.address, 1, anyone2); assert.equal(0, user1.pointsRedeemed); assert.equal(0, user2.pointsRedeemed); - await stakingPoints721.redeemPoints(1, { from: anyone1 }); - await stakingPoints721.redeemPoints(1, { from: anyone2 }); + await stakingPoints721.redeemPoints(creator.address, 1, { from: anyone1 }); + await stakingPoints721.redeemPoints(creator.address, 1, { from: anyone2 }); - let user1Updated = await stakingPoints721.getStakerDetails(1, anyone1); - let user2Updated = await stakingPoints721.getStakerDetails(1, anyone2); + let user1Updated = await stakingPoints721.getStaker(creator.address, 1, anyone1); + let user2Updated = await stakingPoints721.getStaker(creator.address, 1, anyone2); assert.equal(true, user1Updated.pointsRedeemed != 0); assert.equal(true, user2Updated.pointsRedeemed != 0); + console.log("user1", user1Updated); + console.log("user2", user2Updated); }); }); }); From 344feee118ffead1def849784da6fb9e7e230f23 Mon Sep 17 00:00:00 2001 From: nikki-kiga <42276368+nikki-kiga@users.noreply.github.com> Date: Thu, 4 May 2023 22:07:08 -0700 Subject: [PATCH 6/7] fix stakerIdx at 0 and update points logic --- .../manifold/staking/ERC721StakingPoints.sol | 9 +- .../manifold/staking/IStakingPointsCore.sol | 1 + .../manifold/staking/StakingPointsCore.sol | 90 ++++--- test/manifold/stakingpoints721.js | 254 +++++++++++++----- 4 files changed, 242 insertions(+), 112 deletions(-) diff --git a/contracts/manifold/staking/ERC721StakingPoints.sol b/contracts/manifold/staking/ERC721StakingPoints.sol index 8ebfbd5d..a69b957c 100644 --- a/contracts/manifold/staking/ERC721StakingPoints.sol +++ b/contracts/manifold/staking/ERC721StakingPoints.sol @@ -17,6 +17,7 @@ import "./IStakingPointsCore.sol"; * @notice logic for Staking Points for ERC721 extension. */ contract ERC721StakingPoints is StakingPointsCore, IERC721StakingPoints { + using SafeMath for uint256; // { creatorContractAddress => {instanceId => uint256 } } mapping(address => mapping(uint256 => uint256)) public totalPointsClaimed; @@ -91,12 +92,8 @@ contract ERC721StakingPoints is StakingPointsCore, IERC721StakingPoints { /** * @dev */ - function _redeem( - address creatorContractAddress, - uint256 instanceId, - uint256 pointsAmount - ) internal override { + function _redeem(address creatorContractAddress, uint256 instanceId, uint256 pointsAmount) internal override { uint256 currTotal = totalPointsClaimed[creatorContractAddress][instanceId]; - totalPointsClaimed[creatorContractAddress][instanceId] = currTotal + pointsAmount; + totalPointsClaimed[creatorContractAddress][instanceId] = currTotal.add(pointsAmount); } } diff --git a/contracts/manifold/staking/IStakingPointsCore.sol b/contracts/manifold/staking/IStakingPointsCore.sol index d6a21579..dfe1bdd5 100644 --- a/contracts/manifold/staking/IStakingPointsCore.sol +++ b/contracts/manifold/staking/IStakingPointsCore.sol @@ -39,6 +39,7 @@ interface IStakingPointsCore is IERC165, IERC721Receiver { uint256 pointsRedeemed; uint256 stakerIdx; StakedToken[] stakersTokens; + address stakerAddress; } struct StakingRule { diff --git a/contracts/manifold/staking/StakingPointsCore.sol b/contracts/manifold/staking/StakingPointsCore.sol index a6502a28..999be6eb 100644 --- a/contracts/manifold/staking/StakingPointsCore.sol +++ b/contracts/manifold/staking/StakingPointsCore.sol @@ -102,11 +102,7 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS /** * Abstract helper to redeem points. To be implemented by inheriting contracts. */ - function _redeem( - address creatorContractAddress, - uint256 instanceId, - uint256 pointsAmount - ) internal virtual; + function _redeem(address creatorContractAddress, uint256 instanceId, uint256 pointsAmount) internal virtual; /** STAKING */ @@ -129,6 +125,7 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS if (!isIndexed) { uint256 newStakerIdx = instance.stakers.length; instance.stakers.push(); + instance.stakers[newStakerIdx].stakerAddress = msg.sender; instance.stakers[newStakerIdx].stakerIdx = newStakerIdx; // add staker to index map _stakerIdxs[creatorContractAddress][instanceId][msg.sender] = newStakerIdx; @@ -136,8 +133,9 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS } StakedToken[] memory newlyStaked = new StakedToken[](stakingTokens.length); - uint256 stakerIdx = _getStakerIdx(creatorContractAddress, instanceId, msg.sender); - StakedToken[] storage userTokens = instance.stakers[stakerIdx].stakersTokens; + int256 stakerIdx = _getStakerIdx(creatorContractAddress, instanceId, msg.sender); + require(stakerIdx > -1, "Error with staker idx"); + StakedToken[] storage userTokens = instance.stakers[uint256(stakerIdx)].stakersTokens; uint256 length = stakingTokens.length; for (uint256 i = 0; i < length; ) { StakedTokenIdx storage currStakedTokenIdx = _stakedTokenIdxs[msg.sender][stakingTokens[i].tokenAddress][ @@ -175,7 +173,10 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS ) private { StakingRule memory ruleExists = _getTokenRule(creatorContractAddress, instanceId, stakingPointsInstance, tokenAddress); require(ruleExists.tokenAddress == tokenAddress, "Token does not match existing rule"); - + require( + block.timestamp > ruleExists.startTime && block.timestamp < ruleExists.endTime, + "Outside staking rule time limit" + ); _transfer(tokenAddress, tokenId, msg.sender, address(this)); } @@ -198,13 +199,15 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS StakingPoints storage instance = _getStakingPointsInstance(creatorContractAddress, instanceId); StakedToken[] memory unstakedTokens = new StakedToken[](unstakingTokens.length); - uint256 stakerIdx = _getStakerIdx(creatorContractAddress, instanceId, msg.sender); + + int256 stakerIdx = _getStakerIdx(creatorContractAddress, instanceId, msg.sender); + require(stakerIdx > -1, "Cannot unstake tokens for someone who has not staked"); for (uint256 i = 0; i < unstakingTokens.length; ++i) { StakedTokenIdx storage currStakedTokenIdx = _stakedTokenIdxs[msg.sender][unstakingTokens[i].tokenAddress][ unstakingTokens[i].tokenId ]; - StakedToken storage currToken = instance.stakers[stakerIdx].stakersTokens[currStakedTokenIdx.tokenIdx]; + StakedToken storage currToken = instance.stakers[uint256(stakerIdx)].stakersTokens[currStakedTokenIdx.tokenIdx]; require( msg.sender != address(0) && msg.sender == currToken.stakerAddress, "No sender address or not the original staker" @@ -217,12 +220,12 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS // add redeem functionality if staker has not redeemed past qualifying amount for unstaking tokens // ensure that staker is coming from right place - Staker storage staker = instance.stakers[stakerIdx]; - uint256 totalUnstakingTokensPoints = _calculateTotalQualifyingPoints(creatorContractAddress, instanceId, unstakedTokens); - uint256 diffRedeemed = totalUnstakingTokensPoints - staker.pointsRedeemed; + Staker storage staker = instance.stakers[uint256(stakerIdx)]; + uint256 totalUnstakingTokensPoints = _calculateTotalQualifyingPoints(creatorContractAddress, instanceId, staker.stakersTokens); + uint256 diffRedeemed = totalUnstakingTokensPoints.sub(staker.pointsRedeemed); if (diffRedeemed > 0) { - _redeemPointsAmount(creatorContractAddress, instanceId, diffRedeemed); + _redeemPoints(creatorContractAddress, instanceId, msg.sender); } emit TokensUnstaked(creatorContractAddress, instanceId, unstakedTokens, msg.sender); } @@ -235,40 +238,33 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS } function redeemPoints(address creatorContractAddress, uint256 instanceId) external nonReentrant { - _redeemPoints(creatorContractAddress, instanceId); + _redeemPoints(creatorContractAddress, instanceId, msg.sender); } - function _redeemPoints(address creatorContractAddress, uint256 instanceId) private { + function _redeemPoints(address creatorContractAddress, uint256 instanceId, address sender) private { StakingPoints storage instance = _getStakingPointsInstance(creatorContractAddress, instanceId); - uint256 stakerIdx = _getStakerIdx(creatorContractAddress, instanceId, msg.sender); + int256 stakerIdx = _getStakerIdx(creatorContractAddress, instanceId, sender); + require(stakerIdx > -1, "Cannot redeem points for someone who has not staked"); - Staker storage staker = instance.stakers[stakerIdx]; + Staker storage staker = instance.stakers[uint256(stakerIdx)]; uint256 totalQualifyingPoints = _calculateTotalQualifyingPoints( creatorContractAddress, instanceId, staker.stakersTokens ); - uint256 diff = totalQualifyingPoints - staker.pointsRedeemed; + uint256 diff = totalQualifyingPoints.sub(staker.pointsRedeemed); require(totalQualifyingPoints != 0 && diff >= 0, "Need more than zero points"); - // compare with pointsRedeemd + // compare with pointsRedeemed staker.pointsRedeemed = totalQualifyingPoints; _redeem(creatorContractAddress, instanceId, diff); emit PointsDistributed(creatorContractAddress, instanceId, msg.sender, diff); } - /** - * @dev assumes that the sender is qualified to redeem amount and not in excess of points already redeemed - */ - function _redeemPointsAmount(address creatorContractAddress, uint256 instanceId, uint256 amount) private { - _redeem(creatorContractAddress, instanceId, amount); - emit PointsDistributed(creatorContractAddress, instanceId, msg.sender, amount); - } - function getPointsForWallet( address creatorContractAddress, uint256 instanceId, address walletAddress - ) external view returns (uint256 totalPoints, uint256 diff) { + ) external view returns (uint256) { return _getPointsForWallet(creatorContractAddress, instanceId, walletAddress); } @@ -276,12 +272,13 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS address creatorContractAddress, uint256 instanceId, address walletAddress - ) private view returns (uint256 totalPoints, uint256 diff) { + ) private view returns (uint256 walletPoints) { StakingPoints storage instance = _getStakingPointsInstance(creatorContractAddress, instanceId); - uint256 stakerIdx = _getStakerIdx(creatorContractAddress, instanceId, walletAddress); - Staker storage staker = instance.stakers[stakerIdx]; - totalPoints = _calculateTotalQualifyingPoints(creatorContractAddress, instanceId, staker.stakersTokens); - diff = totalPoints - staker.pointsRedeemed; + int256 stakerIdx = _getStakerIdx(creatorContractAddress, instanceId, walletAddress); + require(stakerIdx > -1, "Cannot get points for someone who has not staked"); + + Staker storage staker = instance.stakers[uint256(stakerIdx)]; + walletPoints = _calculateTotalQualifyingPoints(creatorContractAddress, instanceId, staker.stakersTokens); } /** @@ -298,13 +295,13 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS for (uint256 i = 0; i < length; ) { uint256 ruleIdx = _stakingRulesIdxs[creatorContractAddress][instanceId][stakingTokens[i].contractAddress]; StakingRule storage rule = rules[ruleIdx]; - require(rule.startTime >= 0 && rule.endTime >= 0, "Invalid rule values"); - uint256 tokenEnd = stakingTokens[i].timeUnstaked == 0 ? block.timestamp : stakingTokens[i].timeUnstaked; + require(rule.startTime > 0 && rule.endTime > 0, "Invalid rule values"); + uint256 tokenEnd = stakingTokens[i].timeUnstaked > 0 ? stakingTokens[i].timeUnstaked : block.timestamp; uint256 start = Math.max(rule.startTime, stakingTokens[i].timeStaked); uint256 end = Math.min(tokenEnd, rule.endTime); - uint256 diff = start - end; + uint256 diff = end.sub(start); uint256 qualified = _calculateTotalPoints(diff, rule.pointsRatePerDay, 86400); - points = points + qualified; + points = points.add(qualified); unchecked { ++i; @@ -319,9 +316,11 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS uint256 instanceId, address stakerAddress ) external view returns (Staker memory) { - uint256 stakerIdx = _getStakerIdx(creatorContractAddress, instanceId, stakerAddress); + int256 stakerIdx = _getStakerIdx(creatorContractAddress, instanceId, stakerAddress); + StakedToken[] memory emptyStakedTokens; + if (stakerIdx < 0) return Staker(0, 0, emptyStakedTokens, ADDRESS_ZERO); StakingPoints storage instance = _getStakingPointsInstance(creatorContractAddress, instanceId); - return instance.stakers[stakerIdx]; + return instance.stakers[uint256(stakerIdx)]; } function _getTokenRule( @@ -346,13 +345,15 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS /** * Helper to get Staker + * returns the index if staker is indexed otherwise -1 */ function _getStakerIdx( address creatorContractAddress, uint256 instanceId, address walletAddress - ) internal view returns (uint256) { - return _stakerIdxs[creatorContractAddress][instanceId][walletAddress]; + ) internal view returns (int256) { + bool isIndexed = _isStakerIndexed[creatorContractAddress][instanceId][walletAddress]; + return isIndexed ? int256(_stakerIdxs[creatorContractAddress][instanceId][walletAddress]) : -1; } /** @@ -369,7 +370,7 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS /** HELPERS */ function _calculateTotalPoints(uint256 diff, uint256 rate, uint256 div) private pure returns (uint256) { - return (diff * rate) / div; + return diff.mul(rate).div(div); } /** @@ -424,10 +425,11 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS delete stakingPointsInstance.stakingRules; StakingRule[] storage rules = stakingPointsInstance.stakingRules; uint256 length = newStakingRules.length; + uint256 timestamp = block.timestamp; for (uint256 i; i < length; ) { StakingRule memory rule = newStakingRules[i]; require(rule.tokenAddress != address(0), "Staking rule: Contract address required"); - require((rule.endTime == 0) || (rule.startTime < rule.endTime), "Staking rule: Invalid time range"); + require((rule.endTime > timestamp) && (rule.startTime < rule.endTime), "Staking rule: Invalid time range"); require(rule.pointsRatePerDay > 0, "Staking rule: Invalid points rate"); _stakingRulesIdxs[creatorContractAddress][instanceId][rule.tokenAddress] = rules.length; rules.push(rule); diff --git a/test/manifold/stakingpoints721.js b/test/manifold/stakingpoints721.js index 932ad425..f57ffb65 100644 --- a/test/manifold/stakingpoints721.js +++ b/test/manifold/stakingpoints721.js @@ -43,8 +43,8 @@ contract("ERC721StakingPoints", function ([...accounts]) { { tokenAddress: manifoldMembership.address, pointsRatePerDay: 1234, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, ], }, @@ -63,8 +63,8 @@ contract("ERC721StakingPoints", function ([...accounts]) { { tokenAddress: manifoldMembership.address, pointsRatePerDay: 0, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, ], }, @@ -83,8 +83,8 @@ contract("ERC721StakingPoints", function ([...accounts]) { { tokenAddress: manifoldMembership.address, pointsRatePerDay: 1234, - startTime: 1682541275, - endTime: 1674768875, + startTime: 1680680461, + endTime: 1582541275, }, ], }, @@ -103,8 +103,8 @@ contract("ERC721StakingPoints", function ([...accounts]) { { tokenAddress: manifoldMembership.address, pointsRatePerDay: 1234, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, ], }, @@ -124,14 +124,14 @@ contract("ERC721StakingPoints", function ([...accounts]) { { tokenAddress: manifoldMembership.address, pointsRatePerDay: 2222, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, { tokenAddress: mock721.address, pointsRatePerDay: 1111, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, ], }, @@ -148,14 +148,14 @@ contract("ERC721StakingPoints", function ([...accounts]) { { tokenAddress: manifoldMembership.address, pointsRatePerDay: 3333, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, { tokenAddress: mock721.address, pointsRatePerDay: 4444, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, ], }, @@ -174,14 +174,14 @@ contract("ERC721StakingPoints", function ([...accounts]) { { tokenAddress: manifoldMembership.address, pointsRatePerDay: 1234, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, { tokenAddress: mock721.address, pointsRatePerDay: 125, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, ], }, @@ -213,14 +213,14 @@ contract("ERC721StakingPoints", function ([...accounts]) { { tokenAddress: manifoldMembership.address, pointsRatePerDay: 3333, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, { tokenAddress: mock721.address, pointsRatePerDay: 4444, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, ], }, @@ -239,14 +239,14 @@ contract("ERC721StakingPoints", function ([...accounts]) { { tokenAddress: manifoldMembership.address, pointsRatePerDay: 1234, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, { tokenAddress: mock721.address, pointsRatePerDay: 125, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, ], }, @@ -277,20 +277,20 @@ contract("ERC721StakingPoints", function ([...accounts]) { { tokenAddress: manifoldMembership.address, pointsRatePerDay: 1234, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, { tokenAddress: mock721.address, pointsRatePerDay: 125, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, { tokenAddress: mock721_2.address, pointsRatePerDay: 125, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, ], }, @@ -318,14 +318,17 @@ contract("ERC721StakingPoints", function ([...accounts]) { { from: anyone2 } ); - let staker = await stakingPoints721.getStaker(creator.address, 1, anyone2); + let staker1 = await stakingPoints721.getStaker(creator.address, 1, anyone2); - assert.equal(staker.stakersTokens.length, 3); - assert.equal(staker.stakersTokens[0].tokenId, 2); - assert.equal(staker.stakersTokens[1].tokenId, 3); - assert.equal(staker.stakersTokens[1].contractAddress, mock721.address); - assert.equal(staker.stakersTokens[2].tokenId, 2); - assert.equal(staker.stakersTokens[2].contractAddress, mock721_2.address); + assert.equal(staker1.stakersTokens.length, 3); + assert.equal(staker1.stakersTokens[0].tokenId, 2); + assert.equal(staker1.stakersTokens[1].tokenId, 3); + assert.equal(staker1.stakersTokens[1].contractAddress, mock721.address); + assert.equal(staker1.stakersTokens[2].tokenId, 2); + assert.equal(staker1.stakersTokens[2].contractAddress, mock721_2.address); + + let staker2 = await stakingPoints721.getStaker(creator.address, 1, anyone1); + assert.equal(staker2.stakersTokens.length, 0); }); it("Stakes and unstakes", async function () { await stakingPoints721.initializeStakingPoints( @@ -337,20 +340,20 @@ contract("ERC721StakingPoints", function ([...accounts]) { { tokenAddress: manifoldMembership.address, pointsRatePerDay: 1234, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, { tokenAddress: mock721.address, pointsRatePerDay: 125, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, { tokenAddress: mock721_2.address, pointsRatePerDay: 125, - startTime: 1674768875, - endTime: 1682541275, + startTime: 1680680461, + endTime: 95625733261, }, ], }, @@ -412,7 +415,7 @@ contract("ERC721StakingPoints", function ([...accounts]) { ], { from: anyone1 } ), - "No sender address or not the original staker" + "Cannot unstake tokens for someone who has not staked" ); await stakingPoints721.unstakeTokens( @@ -448,21 +451,21 @@ contract("ERC721StakingPoints", function ([...accounts]) { stakingRules: [ { tokenAddress: manifoldMembership.address, - pointsRatePerDay: 1234, - startTime: 1674768875, - endTime: 1682541275, + pointsRatePerDay: 1234000, + startTime: 1680680461, + endTime: 95625733261, }, { tokenAddress: mock721.address, - pointsRatePerDay: 125, - startTime: 1674768875, - endTime: 1682541275, + pointsRatePerDay: 12500000, + startTime: 1680680461, + endTime: 95625733261, }, { tokenAddress: mock721_2.address, - pointsRatePerDay: 120, - startTime: 1674768875, - endTime: 1682541275, + pointsRatePerDay: 12000000, + startTime: 1680680461, + endTime: 95625733261, }, ], }, @@ -520,10 +523,71 @@ contract("ERC721StakingPoints", function ([...accounts]) { { from: anyone1 } ); - await stakingPoints721.unstakeTokens( + let user1 = await stakingPoints721.getStaker(creator.address, 1, anyone1); + let user2 = await stakingPoints721.getStaker(creator.address, 1, anyone2); + assert.equal(0, user1.pointsRedeemed); + assert.equal(0, user2.pointsRedeemed); + + function timeout(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + await timeout(1000); + + await stakingPoints721.redeemPoints(creator.address, 1, { from: anyone1 }); + await stakingPoints721.redeemPoints(creator.address, 1, { from: anyone2 }); + + let user1Updated = await stakingPoints721.getStaker(creator.address, 1, anyone1); + let user2Updated = await stakingPoints721.getStaker(creator.address, 1, anyone2); + assert.equal(true, user1Updated.pointsRedeemed != 0); + assert.equal(true, user2Updated.pointsRedeemed != 0); + }); + it("Redeems points at unstaking", async function () { + await stakingPoints721.initializeStakingPoints( + creator.address, + 1, + { + paymentReceiver: owner, + stakingRules: [ + { + tokenAddress: manifoldMembership.address, + pointsRatePerDay: 1234000, + startTime: 1680680461, + endTime: 95625733261, + }, + { + tokenAddress: mock721.address, + pointsRatePerDay: 12500000, + startTime: 1680680461, + endTime: 95625733261, + }, + { + tokenAddress: mock721_2.address, + pointsRatePerDay: 12000000, + startTime: 1680680461, + endTime: 95625733261, + }, + ], + }, + { from: owner } + ); + await mock721.setApprovalForAll(stakingPoints721.address, true, { from: anyone2 }); + await mock721_2.setApprovalForAll(stakingPoints721.address, true, { from: anyone2 }); + await mock721_2.setApprovalForAll(stakingPoints721.address, true, { from: anotherOwner }); + await mock721_2.setApprovalForAll(stakingPoints721.address, true, { from: anyone1 }); + await mock721.setApprovalForAll(stakingPoints721.address, true, { from: anyone1 }); + await stakingPoints721.stakeTokens( creator.address, 1, [ + { + tokenAddress: mock721.address, + tokenId: 2, + }, + { + tokenAddress: mock721.address, + tokenId: 3, + }, { tokenAddress: mock721_2.address, tokenId: 2, @@ -532,20 +596,86 @@ contract("ERC721StakingPoints", function ([...accounts]) { { from: anyone2 } ); + await stakingPoints721.stakeTokens( + creator.address, + 1, + [ + { + tokenAddress: mock721_2.address, + tokenId: 3, + }, + ], + { from: anotherOwner } + ); + await stakingPoints721.stakeTokens( + creator.address, + 1, + [ + { + tokenAddress: mock721.address, + tokenId: 1, + }, + { + tokenAddress: mock721_2.address, + tokenId: 1, + }, + ], + { from: anyone1 } + ); + let user1 = await stakingPoints721.getStaker(creator.address, 1, anyone1); let user2 = await stakingPoints721.getStaker(creator.address, 1, anyone2); assert.equal(0, user1.pointsRedeemed); assert.equal(0, user2.pointsRedeemed); - await stakingPoints721.redeemPoints(creator.address, 1, { from: anyone1 }); - await stakingPoints721.redeemPoints(creator.address, 1, { from: anyone2 }); + function timeout(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + await timeout(1000); + + let points1 = await stakingPoints721.getPointsForWallet(creator.address, 1, anyone2); + assert.equal(points1 > 0, true); + await stakingPoints721.unstakeTokens( + creator.address, + 1, + [ + { + tokenAddress: mock721_2.address, + tokenId: 2, + }, + ], + { from: anyone2 } + ); + await stakingPoints721.unstakeTokens( + creator.address, + 1, + [ + { + tokenAddress: mock721_2.address, + tokenId: 1, + }, + ], + { from: anyone1 } + ); + await stakingPoints721.unstakeTokens( + creator.address, + 1, + [ + { + tokenAddress: mock721.address, + tokenId: 2, + }, + ], + { from: anyone2 } + ); + let points = await stakingPoints721.getPointsForWallet(creator.address, 1, anyone2); let user1Updated = await stakingPoints721.getStaker(creator.address, 1, anyone1); let user2Updated = await stakingPoints721.getStaker(creator.address, 1, anyone2); + assert.equal(points, user2Updated.pointsRedeemed); assert.equal(true, user1Updated.pointsRedeemed != 0); assert.equal(true, user2Updated.pointsRedeemed != 0); - console.log("user1", user1Updated); - console.log("user2", user2Updated); }); }); }); From 010d33e8bf082fd1c23c026ea43478c46a768d27 Mon Sep 17 00:00:00 2001 From: nikki-kiga <42276368+nikki-kiga@users.noreply.github.com> Date: Fri, 5 May 2023 11:23:30 -0700 Subject: [PATCH 7/7] add back redeemPointsAmount + tests --- .../manifold/staking/StakingPointsCore.sol | 17 +++++++++++++---- test/manifold/stakingpoints721.js | 10 +++++----- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/contracts/manifold/staking/StakingPointsCore.sol b/contracts/manifold/staking/StakingPointsCore.sol index 999be6eb..fa91a8b9 100644 --- a/contracts/manifold/staking/StakingPointsCore.sol +++ b/contracts/manifold/staking/StakingPointsCore.sol @@ -225,7 +225,8 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS uint256 diffRedeemed = totalUnstakingTokensPoints.sub(staker.pointsRedeemed); if (diffRedeemed > 0) { - _redeemPoints(creatorContractAddress, instanceId, msg.sender); + staker.pointsRedeemed = totalUnstakingTokensPoints; + _redeemPointsAmount(creatorContractAddress, instanceId, diffRedeemed); } emit TokensUnstaked(creatorContractAddress, instanceId, unstakedTokens, msg.sender); } @@ -238,12 +239,12 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS } function redeemPoints(address creatorContractAddress, uint256 instanceId) external nonReentrant { - _redeemPoints(creatorContractAddress, instanceId, msg.sender); + _redeemPoints(creatorContractAddress, instanceId); } - function _redeemPoints(address creatorContractAddress, uint256 instanceId, address sender) private { + function _redeemPoints(address creatorContractAddress, uint256 instanceId) private { StakingPoints storage instance = _getStakingPointsInstance(creatorContractAddress, instanceId); - int256 stakerIdx = _getStakerIdx(creatorContractAddress, instanceId, sender); + int256 stakerIdx = _getStakerIdx(creatorContractAddress, instanceId, msg.sender); require(stakerIdx > -1, "Cannot redeem points for someone who has not staked"); Staker storage staker = instance.stakers[uint256(stakerIdx)]; @@ -260,6 +261,14 @@ abstract contract StakingPointsCore is ReentrancyGuard, ERC165, AdminControl, IS emit PointsDistributed(creatorContractAddress, instanceId, msg.sender, diff); } + /** + * @dev assumes that the sender is qualified to redeem amount and not in excess of points already redeemed + */ + function _redeemPointsAmount(address creatorContractAddress, uint256 instanceId, uint256 amount) private { + _redeem(creatorContractAddress, instanceId, amount); + emit PointsDistributed(creatorContractAddress, instanceId, msg.sender, amount); + } + function getPointsForWallet( address creatorContractAddress, uint256 instanceId, diff --git a/test/manifold/stakingpoints721.js b/test/manifold/stakingpoints721.js index f57ffb65..25a0c5ba 100644 --- a/test/manifold/stakingpoints721.js +++ b/test/manifold/stakingpoints721.js @@ -632,10 +632,10 @@ contract("ERC721StakingPoints", function ([...accounts]) { return new Promise((resolve) => setTimeout(resolve, ms)); } - await timeout(1000); + await timeout(5000); let points1 = await stakingPoints721.getPointsForWallet(creator.address, 1, anyone2); - assert.equal(points1 > 0, true); + assert.strictEqual(points1.gt(0), true); await stakingPoints721.unstakeTokens( creator.address, @@ -673,9 +673,9 @@ contract("ERC721StakingPoints", function ([...accounts]) { let points = await stakingPoints721.getPointsForWallet(creator.address, 1, anyone2); let user1Updated = await stakingPoints721.getStaker(creator.address, 1, anyone1); let user2Updated = await stakingPoints721.getStaker(creator.address, 1, anyone2); - assert.equal(points, user2Updated.pointsRedeemed); - assert.equal(true, user1Updated.pointsRedeemed != 0); - assert.equal(true, user2Updated.pointsRedeemed != 0); + assert.strictEqual(points.gt(points1), true); + assert.strictEqual(true, user1Updated.pointsRedeemed != 0); + assert.strictEqual(true, user2Updated.pointsRedeemed != 0); }); }); });