Skip to content

Commit

Permalink
Merge pull request #172 from futureversecom/SM-352-seeker-staking
Browse files Browse the repository at this point in the history
Sm 352 seeker staking
  • Loading branch information
teinnt authored Jun 21, 2024
2 parents 718ad98 + 48b1d53 commit 7cd0e99
Show file tree
Hide file tree
Showing 10 changed files with 1,127 additions and 50 deletions.
16 changes: 16 additions & 0 deletions common/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@ export type SyloContracts = {
syloToken: factories.contracts.SyloToken;
syloStakingManager: factories.contracts.staking.sylo.SyloStakingManager;
seekerStatsOracle: factories.contracts.staking.seekers.SeekerStatsOracle;
seekerStakingManager: factories.contracts.staking.seekers.SeekerStakingManager;
seekers: factories.contracts.mocks.TestSeekers;
};

export type ContractAddresses = {
syloToken: string;
syloStakingManager: string;
seekerStatsOracle: string;
seekerStakingManager: string;
seekers: string;
};

export function connectContracts(
Expand All @@ -46,9 +50,21 @@ export function connectContracts(
provider,
);

const seekerStakingManager = factories.SeekerStakingManager__factory.connect(
contracts.seekerStakingManager,
provider,
);

const seekers = factories.TestSeekers__factory.connect(
contracts.seekers,
provider,
);

return {
syloToken,
syloStakingManager,
seekerStatsOracle,
seekerStakingManager,
seekers,
};
}
19 changes: 19 additions & 0 deletions contracts/mocks/TestSeekers.sol
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

View workflow job for this annotation

GitHub Actions / build

global import of path @openzeppelin/contracts/token/ERC721/ERC721.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)

// 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") {}

Check warning on line 10 in contracts/mocks/TestSeekers.sol

View workflow job for this annotation

GitHub Actions / build

Code contains empty blocks

function mint(address to, uint256 tokenId) external {
_safeMint(to, tokenId);
}

function exists(uint256 tokenId) external view returns (bool) {
return _exists(tokenId);
}
}
30 changes: 30 additions & 0 deletions contracts/staking/seekers/ISeekerStakingManager.sol
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

View workflow job for this annotation

GitHub Actions / build

global import of path ./SeekerStatsOracle.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)

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);
}
4 changes: 4 additions & 0 deletions contracts/staking/seekers/ISeekerStatsOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ interface ISeekerStatsOracle {
function registerSeeker(Seeker calldata seeker, bytes calldata proof) external;

function calculateAttributeCoverage(Seeker[] calldata seekers) external view returns (int256);

function isSeekerRegistered(Seeker calldata seeker) external view returns (bool);

function getSeekerStats(uint256 seekerId) external view returns (Seeker memory);
}
245 changes: 245 additions & 0 deletions contracts/staking/seekers/SeekerStakingManager.sol
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

View workflow job for this annotation

GitHub Actions / build

global import of path @openzeppelin/contracts/utils/introspection/ERC165.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";

Check warning on line 5 in contracts/staking/seekers/SeekerStakingManager.sol

View workflow job for this annotation

GitHub Actions / build

global import of path @openzeppelin/contracts/token/ERC721/IERC721.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)
import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";

Check warning on line 6 in contracts/staking/seekers/SeekerStakingManager.sol

View workflow job for this annotation

GitHub Actions / build

global import of path @openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

Check warning on line 7 in contracts/staking/seekers/SeekerStakingManager.sol

View workflow job for this annotation

GitHub Actions / build

global import of path @openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)

import "./ISeekerStakingManager.sol";

Check warning on line 9 in contracts/staking/seekers/SeekerStakingManager.sol

View workflow job for this annotation

GitHub Actions / build

global import of path ./ISeekerStakingManager.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)
import "./SeekerStatsOracle.sol";

Check warning on line 10 in contracts/staking/seekers/SeekerStakingManager.sol

View workflow job for this annotation

GitHub Actions / build

global import of path ./SeekerStatsOracle.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)

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];
}
}
Loading

0 comments on commit 7cd0e99

Please sign in to comment.