-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
Sm 352 seeker staking
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
pragma solidity ^0.8.18; | ||
|
||
import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; | ||
Check warning on line 4 in contracts/mocks/TestSeekers.sol GitHub Actions / build
|
||
|
||
// Basic ERC721 contract with an unrestricted mint function. | ||
// Useful for mimicking the Seekers ERC721 contract for testing | ||
// purposes. | ||
contract TestSeekers is ERC721 { | ||
constructor() ERC721("Seekers", "SEEKERS") {} | ||
|
||
function mint(address to, uint256 tokenId) external { | ||
_safeMint(to, tokenId); | ||
} | ||
|
||
function exists(uint256 tokenId) external view returns (bool) { | ||
return _exists(tokenId); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
pragma solidity ^0.8.18; | ||
|
||
import "./SeekerStatsOracle.sol"; | ||
Check warning on line 4 in contracts/staking/seekers/ISeekerStakingManager.sol GitHub Actions / build
|
||
|
||
interface ISeekerStakingManager { | ||
struct StakedSeeker { | ||
uint256 seekerId; // bridged seeker id in TRN | ||
address node; // sylo node futureverse address | ||
address user; // msg.sender - the seeker owner, futureverse address in TRN | ||
} | ||
|
||
function stakeSeeker( | ||
address node, | ||
SeekerStatsOracle.Seeker calldata seeker, | ||
bytes calldata seekerStatsProof | ||
) external; | ||
|
||
function stakeSeekers( | ||
address node, | ||
SeekerStatsOracle.Seeker[] calldata seekers, | ||
bytes[] calldata seekerStatsProofs | ||
) external; | ||
|
||
function unstakeSeeker(address node, uint256 seekerId) external; | ||
|
||
function getStakedSeekersByNode(address node) external view returns (uint256[] memory); | ||
|
||
function getStakedSeekersByUser(address node) external view returns (uint256[] memory); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
pragma solidity ^0.8.18; | ||
|
||
import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; | ||
Check warning on line 4 in contracts/staking/seekers/SeekerStakingManager.sol GitHub Actions / build
|
||
import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; | ||
Check warning on line 5 in contracts/staking/seekers/SeekerStakingManager.sol GitHub Actions / build
|
||
import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; | ||
Check warning on line 6 in contracts/staking/seekers/SeekerStakingManager.sol GitHub Actions / build
|
||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; | ||
Check warning on line 7 in contracts/staking/seekers/SeekerStakingManager.sol GitHub Actions / build
|
||
|
||
import "./ISeekerStakingManager.sol"; | ||
Check warning on line 9 in contracts/staking/seekers/SeekerStakingManager.sol GitHub Actions / build
|
||
import "./SeekerStatsOracle.sol"; | ||
Check warning on line 10 in contracts/staking/seekers/SeekerStakingManager.sol GitHub Actions / build
|
||
|
||
contract SeekerStakingManager is | ||
ISeekerStakingManager, | ||
Initializable, | ||
Ownable2StepUpgradeable, | ||
ERC165 | ||
{ | ||
/** | ||
* @notice ERC721 contract for bridged Seekers in TRN. Used for verifying ownership | ||
* of a seeker. | ||
*/ | ||
IERC721 public rootSeekers; | ||
|
||
/** | ||
* @notice Holds the Seekers metadata from Ethereum, set manually in TRN | ||
*/ | ||
SeekerStatsOracle public oracle; | ||
|
||
/** | ||
* @notice mapping to track staked seekers by seeker ID | ||
*/ | ||
mapping(uint256 => StakedSeeker) public stakedSeekersById; | ||
|
||
/** | ||
* @notice mapping to track staked seekers by node address | ||
*/ | ||
mapping(address => uint256[]) public stakedSeekersByNode; | ||
|
||
/** | ||
* @notice mapping to track staked seekers by user address | ||
*/ | ||
mapping(address => uint256[]) public stakedSeekersByUser; | ||
|
||
/** | ||
* @notice variable used for comparision with the mapping | ||
* "stakedSeekersById", specificly whether the value for a given | ||
* key has been defined. | ||
*/ | ||
StakedSeeker private defaultStakedSeeker; | ||
|
||
error NodeAddressCannotBeNil(); | ||
error FromNodeAddressCannotBeNil(); | ||
error SeekerProofCannotBeEmpty(); | ||
error RootSeekersCannotBeZeroAddress(); | ||
error SeekerStatsOracleCannotBeNil(); | ||
error SenderMustOwnSeekerId(); | ||
error SeekerNotYetStaked(); | ||
error SeekerNotStakedToNode(); | ||
error SeekerNotStakedBySender(); | ||
|
||
enum StakedErrors { | ||
SEEKER_NOT_YET_STAKED, | ||
SEEKER_NOT_STAKED_TO_NODE, | ||
SEEKER_NOT_STAKED_BY_SENDER, | ||
NIL // No error | ||
} | ||
|
||
function initialize(IERC721 _rootSeekers, SeekerStatsOracle _oracle) external initializer { | ||
if (address(_rootSeekers) == address(0)) { | ||
revert RootSeekersCannotBeZeroAddress(); | ||
} | ||
if (address(_oracle) == address(0)) { | ||
revert SeekerStatsOracleCannotBeNil(); | ||
} | ||
|
||
Ownable2StepUpgradeable.__Ownable2Step_init(); | ||
|
||
rootSeekers = _rootSeekers; | ||
oracle = _oracle; | ||
} | ||
|
||
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { | ||
return interfaceId == type(ISeekerStakingManager).interfaceId; | ||
} | ||
|
||
/** | ||
* @notice Stakes a seeker, this will unstake a seeker if it is | ||
* already staked. | ||
* @param node Address of node to stake seeker against | ||
* @param seeker The object containing the seekers statistics | ||
* @param seekerStatsProof The signature of the seekers proof | ||
* message, signed by the oracle account and only required if | ||
* the seeker is not registered yet. | ||
*/ | ||
function stakeSeeker( | ||
address node, | ||
SeekerStatsOracle.Seeker calldata seeker, | ||
bytes calldata seekerStatsProof | ||
) external { | ||
_stakeSeeker(node, seeker, seekerStatsProof); | ||
} | ||
|
||
/** | ||
* @notice Stake a multiple seekers, this will unstake a seeker if it is already staked. | ||
* @param node Address of node to stake seeker against | ||
* @param seekers A list of objects containing the seekers statistics | ||
* @param seekerStatsProofs A list of seeker proof message | ||
* signatures, signed by the oracle account and only required if | ||
* the seeker is not registered yet. | ||
*/ | ||
function stakeSeekers( | ||
address node, | ||
SeekerStatsOracle.Seeker[] calldata seekers, | ||
bytes[] calldata seekerStatsProofs | ||
) external { | ||
for (uint256 i = 0; i < seekers.length; ++i) { | ||
_stakeSeeker(node, seekers[i], seekerStatsProofs[i]); | ||
} | ||
} | ||
|
||
function _stakeSeeker( | ||
address node, | ||
SeekerStatsOracle.Seeker calldata seeker, | ||
bytes calldata seekerStatsProof | ||
) internal { | ||
if (node == address(0)) { | ||
revert NodeAddressCannotBeNil(); | ||
} | ||
if (rootSeekers.ownerOf(seeker.seekerId) != msg.sender) { | ||
revert SenderMustOwnSeekerId(); | ||
} | ||
// Register a seeker if not registered yet | ||
if (!oracle.isSeekerRegistered(seeker)) { | ||
if (seekerStatsProof.length == 0) { | ||
revert SeekerProofCannotBeEmpty(); | ||
} | ||
oracle.registerSeeker(seeker, seekerStatsProof); | ||
} | ||
|
||
if (_validateStakedSeeker(node, seeker.seekerId) == StakedErrors.NIL) { | ||
_unstakeSeeker(node, seeker.seekerId); | ||
} | ||
|
||
stakedSeekersById[seeker.seekerId] = StakedSeeker({ | ||
seekerId: seeker.seekerId, | ||
node: node, | ||
user: msg.sender | ||
}); | ||
stakedSeekersByNode[node].push(seeker.seekerId); | ||
stakedSeekersByUser[msg.sender].push(seeker.seekerId); | ||
} | ||
|
||
/** | ||
* @notice Unstakes a seeker, will revert with staked error with | ||
* one of four cases. | ||
* 1) Sender does not own seeker | ||
* 2) Seeker is not yet staked | ||
* 3) Seeker is not staked by the sender | ||
* 4) Seeker is not staked to provided node address | ||
* @param node Address of node to unstake seeker from | ||
* @param seekerId Seeker ID of staked seeker | ||
*/ | ||
function unstakeSeeker(address node, uint256 seekerId) external { | ||
if (node == address(0)) { | ||
revert FromNodeAddressCannotBeNil(); | ||
} | ||
if (rootSeekers.ownerOf(seekerId) != msg.sender) { | ||
revert SenderMustOwnSeekerId(); | ||
} | ||
|
||
StakedErrors err = _validateStakedSeeker(node, seekerId); | ||
if (err == StakedErrors.SEEKER_NOT_YET_STAKED) { | ||
revert SeekerNotYetStaked(); | ||
} | ||
if (err == StakedErrors.SEEKER_NOT_STAKED_BY_SENDER) { | ||
revert SeekerNotStakedBySender(); | ||
} | ||
if (err == StakedErrors.SEEKER_NOT_STAKED_TO_NODE) { | ||
revert SeekerNotStakedToNode(); | ||
} | ||
|
||
_unstakeSeeker(node, seekerId); | ||
} | ||
|
||
function _unstakeSeeker(address node, uint256 seekerId) internal { | ||
for (uint256 i = 0; i < stakedSeekersByNode[node].length; ++i) { | ||
if (stakedSeekersByNode[node][i] == seekerId) { | ||
stakedSeekersByNode[node][i] = stakedSeekersByNode[node][ | ||
stakedSeekersByNode[node].length - 1 | ||
]; | ||
|
||
stakedSeekersByNode[node].pop(); | ||
break; | ||
} | ||
} | ||
|
||
for (uint256 i = 0; i < stakedSeekersByUser[msg.sender].length; ++i) { | ||
if (stakedSeekersByUser[msg.sender][i] == seekerId) { | ||
stakedSeekersByUser[msg.sender][i] = stakedSeekersByUser[msg.sender][ | ||
stakedSeekersByUser[msg.sender].length - 1 | ||
]; | ||
|
||
stakedSeekersByUser[msg.sender].pop(); | ||
break; | ||
} | ||
} | ||
|
||
delete stakedSeekersById[seekerId]; | ||
} | ||
|
||
function _validateStakedSeeker( | ||
address node, | ||
uint256 seekerId | ||
) internal view returns (StakedErrors) { | ||
if ( | ||
keccak256(abi.encode(stakedSeekersById[seekerId])) == | ||
keccak256(abi.encode(defaultStakedSeeker)) | ||
) { | ||
return StakedErrors.SEEKER_NOT_YET_STAKED; | ||
} | ||
if (stakedSeekersById[seekerId].user != msg.sender) { | ||
return StakedErrors.SEEKER_NOT_STAKED_BY_SENDER; | ||
} | ||
if (stakedSeekersById[seekerId].node != node) { | ||
return StakedErrors.SEEKER_NOT_STAKED_TO_NODE; | ||
} | ||
return StakedErrors.NIL; | ||
} | ||
|
||
/** | ||
* @notice Get all seekers that were staked to a specific node address | ||
* @param node Address of node seeker is staked against | ||
*/ | ||
function getStakedSeekersByNode(address node) external view returns (uint256[] memory) { | ||
return stakedSeekersByNode[node]; | ||
} | ||
|
||
/** | ||
* @notice Get all staked seekers owned by user address | ||
* @param user Address of user that staked seeker | ||
*/ | ||
function getStakedSeekersByUser(address user) external view returns (uint256[] memory) { | ||
return stakedSeekersByUser[user]; | ||
} | ||
} |