diff --git a/common/contracts.ts b/common/contracts.ts index 55caa4bc..6f1f1356 100644 --- a/common/contracts.ts +++ b/common/contracts.ts @@ -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( @@ -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, }; } diff --git a/contracts/mocks/TestSeekers.sol b/contracts/mocks/TestSeekers.sol new file mode 100644 index 00000000..59c46eda --- /dev/null +++ b/contracts/mocks/TestSeekers.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.18; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +// 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); + } +} diff --git a/contracts/staking/seekers/ISeekerStakingManager.sol b/contracts/staking/seekers/ISeekerStakingManager.sol new file mode 100644 index 00000000..014fe061 --- /dev/null +++ b/contracts/staking/seekers/ISeekerStakingManager.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.18; + +import "./SeekerStatsOracle.sol"; + +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); +} diff --git a/contracts/staking/seekers/ISeekerStatsOracle.sol b/contracts/staking/seekers/ISeekerStatsOracle.sol index 98dc8221..47f3b7a3 100644 --- a/contracts/staking/seekers/ISeekerStatsOracle.sol +++ b/contracts/staking/seekers/ISeekerStatsOracle.sol @@ -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); } diff --git a/contracts/staking/seekers/SeekerStakingManager.sol b/contracts/staking/seekers/SeekerStakingManager.sol new file mode 100644 index 00000000..bf6e27e8 --- /dev/null +++ b/contracts/staking/seekers/SeekerStakingManager.sol @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.18; + +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +import "./ISeekerStakingManager.sol"; +import "./SeekerStatsOracle.sol"; + +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]; + } +} diff --git a/contracts/staking/seekers/SeekerStatsOracle.sol b/contracts/staking/seekers/SeekerStatsOracle.sol index 083344dc..e80026a6 100644 --- a/contracts/staking/seekers/SeekerStatsOracle.sol +++ b/contracts/staking/seekers/SeekerStatsOracle.sol @@ -21,6 +21,13 @@ contract SeekerStatsOracle is ISeekerStatsOracle, Initializable, Ownable2StepUpg */ mapping(uint256 => Seeker) public seekerStats; + /** + * @notice variable used for comparision with the mapping + * "seekerStats", specificly whether the value for a given + * key has been defined. + */ + Seeker public defaultSeeker; + /** * @notice Holds the angle used for coverage calculation in radians */ @@ -42,8 +49,7 @@ contract SeekerStatsOracle is ISeekerStatsOracle, Initializable, Ownable2StepUpg /** errors **/ error OracleAddressCannotBeNil(); - error SeekerProofIsEmpty(); - error UnauthorizedRegisterSeekerStats(); + error SenderMustBeOracelAccount(); error InvalidSignatureForSeekerProof(); error SeekerNotRegistered(uint256 seekerId); @@ -80,7 +86,7 @@ contract SeekerStatsOracle is ISeekerStatsOracle, Initializable, Ownable2StepUpg /** * @notice Returns true if the oracle account signed the proof message for the given seeker. - * @param seeker The object containing the seekers statistics. + * @param seeker The object containing the seeker's statistics. * @param signature The signature of the seekers proof message, signed by the oracle account. */ function isSeekerStatsProofValid( @@ -97,6 +103,14 @@ contract SeekerStatsOracle is ISeekerStatsOracle, Initializable, Ownable2StepUpg } } + /** + * @notice Creates a unique proofing message for the provided seeker. + * @param seeker The object containing the seeker's statistics. + */ + function createProofMessage(Seeker calldata seeker) external pure returns (bytes memory) { + return _createProofMessage(seeker); + } + /** * @notice Creates a unique proofing message for the provided seeker. * @param seeker The object containing the seekers statistics. @@ -116,16 +130,12 @@ contract SeekerStatsOracle is ISeekerStatsOracle, Initializable, Ownable2StepUpg } /** - * @notice Creates a proofing message unique to the provided seeker. + * @notice Registers a seeker - only callable from oracle * @param seeker The object containing the seekers statistics. */ - function createProofMessage(Seeker calldata seeker) external pure returns (bytes memory) { - return _createProofMessage(seeker); - } - function registerSeekerRestricted(Seeker calldata seeker) external { if (msg.sender != oracle) { - revert UnauthorizedRegisterSeekerStats(); + revert SenderMustBeOracelAccount(); } seekerStats[seeker.seekerId] = seeker; @@ -162,6 +172,20 @@ contract SeekerStatsOracle is ISeekerStatsOracle, Initializable, Ownable2StepUpg ); } + /** + * @notice Validates that the contract has registered the given seeker + * @param seeker The object containing the seeker statistics + */ + function isSeekerRegistered(Seeker calldata seeker) external view returns (bool) { + return _isSeekerRegistered(seeker); + } + + function _isSeekerRegistered(Seeker memory seeker) internal view returns (bool) { + return + keccak256(abi.encode(seekerStats[seeker.seekerId])) != + keccak256(abi.encode(defaultSeeker)); + } + /** * @notice Calculates the coverage score for the given seekers. This score is used by * nodes to determine the staking capacity and is a reflection of the diversity @@ -178,15 +202,13 @@ contract SeekerStatsOracle is ISeekerStatsOracle, Initializable, Ownable2StepUpg int256 totalStorage = 0; int256 totalChip = 0; - Seeker memory defaultSeeker; - for (uint256 i = 0; i < seekers.length; i++) { Seeker memory seeker = seekers[i]; Seeker memory registeredSeeker = seekerStats[seeker.seekerId]; // We validate the seeker has been registered by checking if it is // not equal to the default, empty-value Seeker. - if (keccak256(abi.encode(registeredSeeker)) == keccak256(abi.encode(defaultSeeker))) { + if (!_isSeekerRegistered(registeredSeeker)) { revert SeekerNotRegistered(seeker.seekerId); } @@ -207,4 +229,12 @@ contract SeekerStatsOracle is ISeekerStatsOracle, Initializable, Ownable2StepUpg return coverage; } + + /** + * @notice Get registered seeker statistics for given seeker ID + * @param seekerId Id of the seekers statistics to retrieve + */ + function getSeekerStats(uint256 seekerId) external view returns (Seeker memory) { + return seekerStats[seekerId]; + } } diff --git a/test/seekerStats/stakingStats.test.ts b/test/seekerStats/stakingStats.test.ts index 9ac51e8c..f3435a85 100644 --- a/test/seekerStats/stakingStats.test.ts +++ b/test/seekerStats/stakingStats.test.ts @@ -5,7 +5,7 @@ import { Signer } from 'ethers'; import { expect, assert } from 'chai'; import { deployContracts, getInterfaceId } from '../utils'; -class Seeker { +export class Seeker { constructor( public seekerId: number, public rank: number, @@ -82,7 +82,7 @@ describe('Seeker Stats', () => { }); it('can register seeker', async () => { - const seeker = new Seeker(10, 2, 10, 20, 30, 40, 50, 60); + const seeker = createRandomSeeker(); const proofMessage = await seekerStatsOracle.createProofMessage(seeker); const signature = await accounts[19].signMessage( @@ -91,32 +91,48 @@ describe('Seeker Stats', () => { await expect(seekerStatsOracle.registerSeeker(seeker, signature)) .to.emit(seekerStatsOracle, 'SeekerStatsUpdated') - .withArgs(10n, 10n, 20n, 30n, 40n, 50n, 60n); + .withArgs( + seeker.seekerId, + seeker.attrReactor, + seeker.attrCores, + seeker.attrDurability, + seeker.attrSensors, + seeker.attrStorage, + seeker.attrChip, + ); }); it('can register seeker restricted', async () => { - const seeker = new Seeker(10, 2, 10, 20, 30, 40, 50, 60); + const seeker = createRandomSeeker(); await expect( seekerStatsOracle.connect(accounts[19]).registerSeekerRestricted(seeker), ) .to.emit(seekerStatsOracle, 'SeekerStatsUpdated') - .withArgs(10n, 10n, 20n, 30n, 40n, 50n, 60n); + .withArgs( + seeker.seekerId, + seeker.attrReactor, + seeker.attrCores, + seeker.attrDurability, + seeker.attrSensors, + seeker.attrStorage, + seeker.attrChip, + ); }); it('cannot register seeker restricted from non-oracle account', async () => { - const seeker = new Seeker(10, 2, 10, 20, 30, 40, 50, 60); + const seeker = createRandomSeeker(); await expect( seekerStatsOracle.registerSeekerRestricted(seeker), ).to.be.revertedWithCustomError( seekerStatsOracle, - 'UnauthorizedRegisterSeekerStats', + 'SenderMustBeOracelAccount', ); }); it('cannot register seeker from non-oracle account', async () => { - const seeker = new Seeker(20, 2, 10, 20, 30, 40, 50, 60); + const seeker = createRandomSeeker(); const proofMessage = await seekerStatsOracle.createProofMessage(seeker); const signature = await accounts[18].signMessage( @@ -131,8 +147,10 @@ describe('Seeker Stats', () => { }); it('can update registered seeker', async () => { - const seeker = new Seeker(10, 2, 1, 1, 1, 1, 1, 1); - const seekerTwo = new Seeker(10, 2, 10, 10, 10, 10, 10, 10); + const seeker = createRandomSeeker(); + const seekerTwo = createRandomSeeker(); + seekerTwo.seekerId = seeker.seekerId; + seekerTwo.rank = seeker.rank; const proofMessage = await seekerStatsOracle.createProofMessage(seeker); const proofMessageTwo = await seekerStatsOracle.createProofMessage( @@ -148,7 +166,9 @@ describe('Seeker Stats', () => { await seekerStatsOracle.registerSeeker(seeker, signature); - const fetchedSeeker = await seekerStatsOracle.seekerStats(10); + const fetchedSeeker = await seekerStatsOracle.getSeekerStats( + seeker.seekerId, + ); const newSeekerOne = new Seeker( Number(fetchedSeeker[0]), Number(fetchedSeeker[1]), @@ -160,11 +180,13 @@ describe('Seeker Stats', () => { Number(fetchedSeeker[7]), ); - assert.equal(Number(fetchedSeeker.seekerId), 10); + assert.equal(Number(fetchedSeeker.seekerId), seeker.seekerId); await seekerStatsOracle.registerSeeker(seekerTwo, signatureTwo); - const fetchedSeekerTwo = await seekerStatsOracle.seekerStats(10); + const fetchedSeekerTwo = await seekerStatsOracle.getSeekerStats( + seeker.seekerId, + ); const newSeekerTwo = new Seeker( Number(fetchedSeekerTwo[0]), Number(fetchedSeekerTwo[1]), @@ -180,7 +202,7 @@ describe('Seeker Stats', () => { }); it('cannot register seeker from with invalid proof', async () => { - const seeker = new Seeker(20, 2, 10, 20, 30, 40, 50, 60); + const seeker = createRandomSeeker(); const proofMessage = 'invalid message'; const signature = await accounts[18].signMessage( @@ -195,7 +217,7 @@ describe('Seeker Stats', () => { }); it('cannot calculate converage with unregistered seeker', async () => { - const seeker = new Seeker(30, 2, 10, 20, 30, 40, 50, 60); + const seeker = createRandomSeeker(); await expect(seekerStatsOracle.calculateAttributeCoverage([seeker])) .to.be.revertedWithCustomError(seekerStatsOracle, 'SeekerNotRegistered') @@ -211,8 +233,8 @@ describe('Seeker Stats', () => { const formatedCoverage = ethers.formatEther(attributeCoverage); assert.equal( - Number(formatedCoverage).toFixed(1), - attributeConverageExpected.toFixed(1), + Number(formatedCoverage).toFixed(0), + attributeConverageExpected.toFixed(0), ); }); @@ -237,6 +259,8 @@ describe('Seeker Stats', () => { 'function registerSeekerRestricted((uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256) calldata seeker) external', 'function registerSeeker((uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256) calldata seeker, bytes calldata signature) external', 'function calculateAttributeCoverage((uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)[] calldata seekersList) external view returns (int256)', + 'function isSeekerRegistered((uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256) calldata seeker) external view returns (bool)', + 'function getSeekerStats(uint256 seekerId) external view returns ((uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256) memory)', ]; const interfaceId = getInterfaceId(abi); @@ -307,16 +331,7 @@ describe('Seeker Stats', () => { async function createAndRegisterSeeker(amount: number): Promise { const seekerList: Seeker[] = []; for (let i = 0; i < amount; i++) { - const newSeeker = new Seeker( - i, - i, - i + 10, - i + 20, - i + 30, - i + 40, - i + 50, - i + 60, - ); + const newSeeker = createRandomSeeker(); const proofMessage = await seekerStatsOracle.createProofMessage( newSeeker, ); @@ -343,3 +358,31 @@ describe('Seeker Stats', () => { ); } }); + +function getRandomInt(min: number, max: number): number { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1)) + min; // The maximum is inclusive and the minimum is inclusive +} + +export function createRandomSeeker(): Seeker { + const seekerId = getRandomInt(1, 60000); + const rank = getRandomInt(1, 100); + const attrReactor = getRandomInt(1, 30); + const attrCores = getRandomInt(1, 30); + const attrDurability = getRandomInt(1, 30); + const attrSensors = getRandomInt(1, 30); + const attrStorage = getRandomInt(1, 30); + const attrChip = getRandomInt(1, 30); + + return new Seeker( + seekerId, + rank, + attrReactor, + attrCores, + attrDurability, + attrSensors, + attrStorage, + attrChip, + ); +} diff --git a/test/staking/seekerStakingManager.test.ts b/test/staking/seekerStakingManager.test.ts new file mode 100644 index 00000000..659ec7c2 --- /dev/null +++ b/test/staking/seekerStakingManager.test.ts @@ -0,0 +1,674 @@ +import { ethers } from 'hardhat'; +import { + SeekerStakingManager, + TestSeekers, + SeekerStatsOracle, +} from '../../typechain-types'; +import { SyloContracts } from '../../common/contracts'; +import { Signer } from 'ethers'; +import { expect, assert } from 'chai'; +import { deployContracts } from '../utils'; +import { getInterfaceId } from '../utils'; +import { createRandomSeeker } from '../seekerStats/stakingStats.test'; + +class StakedSeeker { + constructor( + public seekerId: number, + public node: string, + public user: string, + ) {} +} + +describe('Seeker Staking Manager', () => { + let accounts: Signer[]; + let contracts: SyloContracts; + let seekerStakingManager: SeekerStakingManager; + let testSeekers: TestSeekers; + let seekerStatsOracle: SeekerStatsOracle; + let seekerOwner: Signer; + let nonSeekerOwner: Signer; + let nodeOne: Signer; + let nodeTwo: Signer; + let oracleAccount: Signer; + + beforeEach(async () => { + accounts = await ethers.getSigners(); + contracts = await deployContracts(); + seekerStakingManager = contracts.seekerStakingManager; + testSeekers = contracts.seekers; + seekerStatsOracle = contracts.seekerStatsOracle; + nodeOne = accounts[10]; + nodeTwo = accounts[11]; + seekerOwner = accounts[0]; + nonSeekerOwner = accounts[1]; + + oracleAccount = accounts[19]; + await seekerStatsOracle.setOracle(await accounts[19].getAddress()); + }); + + it('cannot initialize seeker staking manager with invalid arguemnts', async () => { + const factory = await ethers.getContractFactory('SeekerStakingManager'); + const seekerStakingManagerTemp = await factory.deploy(); + + await expect( + seekerStakingManagerTemp.initialize( + ethers.ZeroAddress, + ethers.ZeroAddress, + ), + ).to.be.revertedWithCustomError( + seekerStakingManagerTemp, + 'RootSeekersCannotBeZeroAddress', + ); + + await expect( + seekerStakingManagerTemp.initialize( + await testSeekers.getAddress(), + ethers.ZeroAddress, + ), + ).to.be.revertedWithCustomError( + seekerStakingManagerTemp, + 'SeekerStatsOracleCannotBeNil', + ); + }); + + it('cannot initialize seeker staking manager more than once', async () => { + await expect( + seekerStakingManager.initialize( + await testSeekers.getAddress(), + await seekerStatsOracle.getAddress(), + ), + ).to.be.revertedWith('Initializable: contract is already initialized'); + }); + + it('cannot stake seeker without node address', async () => { + const seeker = createRandomSeeker(); + + await expect( + seekerStakingManager.stakeSeeker( + ethers.ZeroAddress, + seeker, + Buffer.from(''), + ), + ).to.be.revertedWithCustomError( + seekerStakingManager, + 'NodeAddressCannotBeNil', + ); + }); + + it('tx sender must own seeker id to stake', async () => { + const seeker = createRandomSeeker(); + + await testSeekers.mint(seekerOwner, seeker.seekerId); + + await expect( + seekerStakingManager + .connect(nonSeekerOwner) + .stakeSeeker(nodeOne, seeker, Buffer.from('')), + ).to.be.revertedWithCustomError( + seekerStakingManager, + 'SenderMustOwnSeekerId', + ); + }); + + it('cannot stake unregistered seeker with empty proof', async () => { + const seeker = createRandomSeeker(); + + await testSeekers.mint(seekerOwner, seeker.seekerId); + + await expect( + seekerStakingManager.stakeSeeker(nodeOne, seeker, Buffer.from('')), + ).to.be.revertedWithCustomError( + seekerStakingManager, + 'SeekerProofCannotBeEmpty', + ); + }); + + it('stake seeker emits event', async () => { + const seeker = createRandomSeeker(); + + await testSeekers.mint(seekerOwner, seeker.seekerId); + + const proofMessage = await seekerStatsOracle.createProofMessage(seeker); + const signature = await oracleAccount.signMessage( + Buffer.from(proofMessage.slice(2), 'hex'), + ); + + await expect(seekerStakingManager.stakeSeeker(nodeOne, seeker, signature)) + .to.emit(seekerStatsOracle, 'SeekerStatsUpdated') + .withArgs( + seeker.seekerId, + seeker.attrReactor, + seeker.attrCores, + seeker.attrDurability, + seeker.attrSensors, + seeker.attrStorage, + seeker.attrChip, + ); + }); + + it('can stake unregistered seeker', async () => { + const seeker = createRandomSeeker(); + + await testSeekers.mint(seekerOwner, seeker.seekerId); + + const expectedStakedSeeker = new StakedSeeker( + seeker.seekerId, + await nodeOne.getAddress(), + await seekerOwner.getAddress(), + ); + + const proofMessage = await seekerStatsOracle.createProofMessage(seeker); + const signature = await oracleAccount.signMessage( + Buffer.from(proofMessage.slice(2), 'hex'), + ); + + const stakedSeekerByIdBefore = await seekerStakingManager.stakedSeekersById( + seeker.seekerId, + ); + const stakedSeekerByNodeBefore = + await seekerStakingManager.getStakedSeekersByNode(nodeOne); + const stakedSeekerByUserBefore = + await seekerStakingManager.getStakedSeekersByUser(seekerOwner); + + compareStakedSeekers( + new StakedSeeker(0, ethers.ZeroAddress, ethers.ZeroAddress), + new StakedSeeker( + Number(stakedSeekerByIdBefore[0]), + stakedSeekerByIdBefore[1], + stakedSeekerByIdBefore[2], + ), + ); + assert.equal(stakedSeekerByNodeBefore.length, 0); + assert.equal(stakedSeekerByUserBefore.length, 0); + + await seekerStakingManager.stakeSeeker(nodeOne, seeker, signature); + + const stakedSeekerByIdAfter = await seekerStakingManager.stakedSeekersById( + seeker.seekerId, + ); + const stakedSeekerByNodeAfter = + await seekerStakingManager.getStakedSeekersByNode(nodeOne); + const stakedSeekerByUserAfter = + await seekerStakingManager.getStakedSeekersByUser(seekerOwner); + + compareStakedSeekers( + expectedStakedSeeker, + new StakedSeeker( + Number(stakedSeekerByIdAfter[0]), + stakedSeekerByIdAfter[1], + stakedSeekerByIdAfter[2], + ), + ); + assert.equal(stakedSeekerByNodeAfter.length, 1); + assert.equal(Number(stakedSeekerByNodeAfter[0]), seeker.seekerId); + assert.equal(stakedSeekerByUserAfter.length, 1); + assert.equal(Number(stakedSeekerByUserAfter[0]), seeker.seekerId); + }); + + it('can stake registered seeker', async () => { + const seeker = createRandomSeeker(); + + await testSeekers.mint(seekerOwner, seeker.seekerId); + + const expectedStakedSeeker = new StakedSeeker( + seeker.seekerId, + await nodeOne.getAddress(), + await seekerOwner.getAddress(), + ); + + const proofMessage = await seekerStatsOracle.createProofMessage(seeker); + const signature = await oracleAccount.signMessage( + Buffer.from(proofMessage.slice(2), 'hex'), + ); + await seekerStatsOracle.registerSeeker(seeker, signature); + + const stakedSeekerByIdBefore = await seekerStakingManager.stakedSeekersById( + seeker.seekerId, + ); + const stakedSeekerByNodeBefore = + await seekerStakingManager.getStakedSeekersByNode(nodeOne); + const stakedSeekerByUserBefore = + await seekerStakingManager.getStakedSeekersByUser(seekerOwner); + + compareStakedSeekers( + new StakedSeeker(0, ethers.ZeroAddress, ethers.ZeroAddress), + new StakedSeeker( + Number(stakedSeekerByIdBefore[0]), + stakedSeekerByIdBefore[1], + stakedSeekerByIdBefore[2], + ), + ); + assert.equal(stakedSeekerByNodeBefore.length, 0); + assert.equal(stakedSeekerByUserBefore.length, 0); + + await seekerStakingManager.stakeSeeker(nodeOne, seeker, Buffer.from('')); + + const stakedSeekerByIdAfter = await seekerStakingManager.stakedSeekersById( + seeker.seekerId, + ); + const stakedSeekerByNodeAfter = + await seekerStakingManager.getStakedSeekersByNode(nodeOne); + const stakedSeekerByUserAfter = + await seekerStakingManager.getStakedSeekersByUser(seekerOwner); + + compareStakedSeekers( + expectedStakedSeeker, + new StakedSeeker( + Number(stakedSeekerByIdAfter[0]), + stakedSeekerByIdAfter[1], + stakedSeekerByIdAfter[2], + ), + ); + assert.equal(stakedSeekerByNodeAfter.length, 1); + assert.equal(Number(stakedSeekerByNodeAfter[0]), seeker.seekerId); + assert.equal(stakedSeekerByUserAfter.length, 1); + assert.equal(Number(stakedSeekerByUserAfter[0]), seeker.seekerId); + }); + + it('multiple calls to stakeSeeker does not duplicate seeker stake', async () => { + const seeker = createRandomSeeker(); + + await testSeekers.mint(seekerOwner, seeker.seekerId); + + const expectedStakedSeeker = new StakedSeeker( + seeker.seekerId, + await nodeOne.getAddress(), + await seekerOwner.getAddress(), + ); + + const proofMessage = await seekerStatsOracle.createProofMessage(seeker); + const signature = await oracleAccount.signMessage( + Buffer.from(proofMessage.slice(2), 'hex'), + ); + + const stakedSeekerByIdBefore = await seekerStakingManager.stakedSeekersById( + seeker.seekerId, + ); + const stakedSeekerByNodeBefore = + await seekerStakingManager.getStakedSeekersByNode(nodeOne); + const stakedSeekerByUserBefore = + await seekerStakingManager.getStakedSeekersByUser(seekerOwner); + + compareStakedSeekers( + new StakedSeeker(0, ethers.ZeroAddress, ethers.ZeroAddress), + new StakedSeeker( + Number(stakedSeekerByIdBefore[0]), + stakedSeekerByIdBefore[1], + stakedSeekerByIdBefore[2], + ), + ); + assert.equal(stakedSeekerByNodeBefore.length, 0); + assert.equal(stakedSeekerByUserBefore.length, 0); + + await seekerStakingManager.stakeSeeker(nodeOne, seeker, signature); + + const stakedSeekerByIdAfter = await seekerStakingManager.stakedSeekersById( + seeker.seekerId, + ); + const stakedSeekerByNodeAfter = + await seekerStakingManager.getStakedSeekersByNode(nodeOne); + const stakedSeekerByUserAfter = + await seekerStakingManager.getStakedSeekersByUser(seekerOwner); + + compareStakedSeekers( + expectedStakedSeeker, + new StakedSeeker( + Number(stakedSeekerByIdAfter[0]), + stakedSeekerByIdAfter[1], + stakedSeekerByIdAfter[2], + ), + ); + assert.equal(stakedSeekerByNodeAfter.length, 1); + assert.equal(Number(stakedSeekerByNodeAfter[0]), seeker.seekerId); + assert.equal(stakedSeekerByUserAfter.length, 1); + assert.equal(Number(stakedSeekerByUserAfter[0]), seeker.seekerId); + + await seekerStakingManager.stakeSeeker(nodeOne, seeker, signature); + + const stakedSeekerByIdAfterTwo = + await seekerStakingManager.stakedSeekersById(seeker.seekerId); + const stakedSeekerByNodeAfterTwo = + await seekerStakingManager.getStakedSeekersByNode(nodeOne); + const stakedSeekerByUserAfterTwo = + await seekerStakingManager.getStakedSeekersByUser(seekerOwner); + + compareStakedSeekers( + expectedStakedSeeker, + new StakedSeeker( + Number(stakedSeekerByIdAfterTwo[0]), + stakedSeekerByIdAfterTwo[1], + stakedSeekerByIdAfterTwo[2], + ), + ); + assert.equal(stakedSeekerByNodeAfterTwo.length, 1); + assert.equal(Number(stakedSeekerByNodeAfterTwo[0]), seeker.seekerId); + assert.equal(stakedSeekerByUserAfterTwo.length, 1); + assert.equal(Number(stakedSeekerByUserAfterTwo[0]), seeker.seekerId); + }); + + it('can stake multiple registered seeker', async () => { + const seeker = createRandomSeeker(); + const seekerTwo = createRandomSeeker(); + const seekerThree = createRandomSeeker(); + + await testSeekers.mint(seekerOwner, seeker.seekerId); + await testSeekers.mint(seekerOwner, seekerTwo.seekerId); + await testSeekers.mint(seekerOwner, seekerThree.seekerId); + + const expectedStakedSeeker = new StakedSeeker( + seeker.seekerId, + await nodeOne.getAddress(), + await seekerOwner.getAddress(), + ); + const expectedStakedSeekerTwo = new StakedSeeker( + seekerTwo.seekerId, + await nodeTwo.getAddress(), + await seekerOwner.getAddress(), + ); + const expectedStakedSeekerThree = new StakedSeeker( + seekerThree.seekerId, + await nodeOne.getAddress(), + await seekerOwner.getAddress(), + ); + + const proofMessage = await seekerStatsOracle.createProofMessage(seeker); + const proofMessageTwo = await seekerStatsOracle.createProofMessage( + seekerTwo, + ); + const proofMessageThree = await seekerStatsOracle.createProofMessage( + seekerThree, + ); + + const signature = await oracleAccount.signMessage( + Buffer.from(proofMessage.slice(2), 'hex'), + ); + const signatureTwo = await oracleAccount.signMessage( + Buffer.from(proofMessageTwo.slice(2), 'hex'), + ); + const signatureThree = await oracleAccount.signMessage( + Buffer.from(proofMessageThree.slice(2), 'hex'), + ); + + const stakedSeekerByIdBefore = await seekerStakingManager.stakedSeekersById( + seeker.seekerId, + ); + const stakedSeekerByIdBeforeTwo = + await seekerStakingManager.stakedSeekersById(seekerTwo.seekerId); + const stakedSeekerByIdBeforeThree = + await seekerStakingManager.stakedSeekersById(seekerThree.seekerId); + const stakedSeekerByNodeBefore = + await seekerStakingManager.getStakedSeekersByNode(nodeOne); + const stakedSeekerByNodeTwoBefore = + await seekerStakingManager.getStakedSeekersByNode(nodeTwo); + const stakedSeekerByUserBefore = + await seekerStakingManager.getStakedSeekersByUser(seekerOwner); + + compareStakedSeekers( + new StakedSeeker(0, ethers.ZeroAddress, ethers.ZeroAddress), + new StakedSeeker( + Number(stakedSeekerByIdBefore[0]), + stakedSeekerByIdBefore[1], + stakedSeekerByIdBefore[2], + ), + ); + compareStakedSeekers( + new StakedSeeker(0, ethers.ZeroAddress, ethers.ZeroAddress), + new StakedSeeker( + Number(stakedSeekerByIdBeforeTwo[0]), + stakedSeekerByIdBeforeTwo[1], + stakedSeekerByIdBeforeTwo[2], + ), + ); + compareStakedSeekers( + new StakedSeeker(0, ethers.ZeroAddress, ethers.ZeroAddress), + new StakedSeeker( + Number(stakedSeekerByIdBeforeThree[0]), + stakedSeekerByIdBeforeThree[1], + stakedSeekerByIdBeforeThree[2], + ), + ); + + assert.equal(stakedSeekerByNodeBefore.length, 0); + assert.equal(stakedSeekerByNodeTwoBefore.length, 0); + assert.equal(stakedSeekerByUserBefore.length, 0); + + await seekerStakingManager.stakeSeekers( + nodeOne, + [seeker, seekerThree], + [signature, signatureThree], + ); + + await seekerStakingManager.stakeSeeker(nodeTwo, seekerTwo, signatureTwo); + + const stakedSeekerByIdAfter = await seekerStakingManager.stakedSeekersById( + seeker.seekerId, + ); + const stakedSeekerByIdAfterTwo = + await seekerStakingManager.stakedSeekersById(seekerTwo.seekerId); + const stakedSeekerByIdAfterThree = + await seekerStakingManager.stakedSeekersById(seekerThree.seekerId); + const stakedSeekerByNodeAfter = + await seekerStakingManager.getStakedSeekersByNode(nodeOne); + const stakedSeekerByNodeTwoAfter = + await seekerStakingManager.getStakedSeekersByNode(nodeTwo); + const stakedSeekerByUserAfter = + await seekerStakingManager.getStakedSeekersByUser(seekerOwner); + + compareStakedSeekers( + expectedStakedSeeker, + new StakedSeeker( + Number(stakedSeekerByIdAfter[0]), + stakedSeekerByIdAfter[1], + stakedSeekerByIdAfter[2], + ), + ); + compareStakedSeekers( + expectedStakedSeekerTwo, + new StakedSeeker( + Number(stakedSeekerByIdAfterTwo[0]), + stakedSeekerByIdAfterTwo[1], + stakedSeekerByIdAfterTwo[2], + ), + ); + compareStakedSeekers( + expectedStakedSeekerThree, + new StakedSeeker( + Number(stakedSeekerByIdAfterThree[0]), + stakedSeekerByIdAfterThree[1], + stakedSeekerByIdAfterThree[2], + ), + ); + + assert.equal(stakedSeekerByNodeAfter.length, 2); + assert.equal(Number(stakedSeekerByNodeAfter[0]), seeker.seekerId); + assert.equal(Number(stakedSeekerByNodeAfter[1]), seekerThree.seekerId); + + assert.equal(stakedSeekerByNodeTwoAfter.length, 1); + assert.equal(Number(stakedSeekerByNodeTwoAfter[0]), seekerTwo.seekerId); + + assert.equal(stakedSeekerByUserAfter.length, 3); + assert.equal(Number(stakedSeekerByUserAfter[0]), seeker.seekerId); + assert.equal(Number(stakedSeekerByUserAfter[2]), seekerTwo.seekerId); + assert.equal(Number(stakedSeekerByUserAfter[1]), seekerThree.seekerId); + }); + + it('cannot unstake seeker from zero node address', async () => { + await expect( + seekerStakingManager.unstakeSeeker(ethers.ZeroAddress, 0), + ).to.be.revertedWithCustomError( + seekerStakingManager, + 'FromNodeAddressCannotBeNil', + ); + }); + + it('tx sender must own seeker to transfer', async () => { + await testSeekers.mint(seekerOwner, 10); + await expect( + seekerStakingManager.connect(nonSeekerOwner).unstakeSeeker(nodeOne, 10), + ).to.be.revertedWithCustomError( + seekerStakingManager, + 'SenderMustOwnSeekerId', + ); + }); + + it('seeker must be staked to unstake', async () => { + await testSeekers.mint(seekerOwner, 10); + await expect( + seekerStakingManager.unstakeSeeker(nodeOne, 10), + ).to.be.revertedWithCustomError(seekerStakingManager, 'SeekerNotYetStaked'); + }); + + it('unstake tx sender must be seeker staker', async () => { + const seeker = createRandomSeeker(); + + await testSeekers.mint(seekerOwner, seeker.seekerId); + + const proofMessage = await seekerStatsOracle.createProofMessage(seeker); + const signature = await oracleAccount.signMessage( + Buffer.from(proofMessage.slice(2), 'hex'), + ); + await seekerStakingManager.stakeSeeker(nodeOne, seeker, signature); + + await testSeekers.transferFrom( + seekerOwner, + nonSeekerOwner, + seeker.seekerId, + ); + + await expect( + seekerStakingManager + .connect(nonSeekerOwner) + .unstakeSeeker(nodeOne, seeker.seekerId), + ).to.be.revertedWithCustomError( + seekerStakingManager, + 'SeekerNotStakedBySender', + ); + }); + + it('seeker must be unstaked from staked node', async () => { + const seeker = createRandomSeeker(); + + await testSeekers.mint(seekerOwner, seeker.seekerId); + + const proofMessage = await seekerStatsOracle.createProofMessage(seeker); + const signature = await oracleAccount.signMessage( + Buffer.from(proofMessage.slice(2), 'hex'), + ); + await seekerStakingManager.stakeSeeker(nodeOne, seeker, signature); + + await expect( + seekerStakingManager.unstakeSeeker(nodeTwo, seeker.seekerId), + ).to.be.revertedWithCustomError( + seekerStakingManager, + 'SeekerNotStakedToNode', + ); + }); + + it('can unstake seeker', async () => { + const seeker = createRandomSeeker(); + + await testSeekers.mint(seekerOwner, seeker.seekerId); + + const stakedSeekerBefore = new StakedSeeker( + seeker.seekerId, + await nodeOne.getAddress(), + await seekerOwner.getAddress(), + ); + + const proofMessage = await seekerStatsOracle.createProofMessage(seeker); + const signature = await oracleAccount.signMessage( + Buffer.from(proofMessage.slice(2), 'hex'), + ); + await seekerStakingManager.stakeSeeker(nodeOne, seeker, signature); + + const stakedSeekerByNodeBefore = + await seekerStakingManager.getStakedSeekersByNode(nodeOne); + const stakedSeekerByUserBefore = + await seekerStakingManager.getStakedSeekersByUser(seekerOwner); + const stakedSeekerByIdBefore = await seekerStakingManager.stakedSeekersById( + seeker.seekerId, + ); + + assert.equal(stakedSeekerByNodeBefore.length, 1); + assert.equal(Number(stakedSeekerByNodeBefore[0]), seeker.seekerId); + assert.equal(stakedSeekerByUserBefore.length, 1); + assert.equal(Number(stakedSeekerByUserBefore[0]), seeker.seekerId); + + compareStakedSeekers( + stakedSeekerBefore, + new StakedSeeker( + Number(stakedSeekerByIdBefore[0]), + stakedSeekerByIdBefore[1], + stakedSeekerByIdBefore[2], + ), + ); + + await seekerStakingManager.unstakeSeeker(nodeOne, seeker.seekerId); + + const stakedSeekerByNodeAfter = + await seekerStakingManager.getStakedSeekersByNode(nodeOne); + const stakedSeekerByUserAfter = + await seekerStakingManager.getStakedSeekersByUser(seekerOwner); + const stakedSeekerByIdAfter = await seekerStakingManager.stakedSeekersById( + seeker.seekerId, + ); + + assert.equal(stakedSeekerByNodeAfter.length, 0); + assert.equal(stakedSeekerByUserAfter.length, 0); + + compareStakedSeekers( + new StakedSeeker(0, ethers.ZeroAddress, ethers.ZeroAddress), + new StakedSeeker( + Number(stakedSeekerByIdAfter[0]), + stakedSeekerByIdAfter[1], + stakedSeekerByIdAfter[2], + ), + ); + }); + + it('supports only seeker staking manager interface', async () => { + const abi = [ + 'function stakeSeeker(address node, (uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256) calldata seeker, bytes calldata seekerStatsProof) external', + 'function stakeSeekers(address node, (uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)[] 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)', + ]; + + const interfaceId = getInterfaceId(abi); + + const supports = await seekerStakingManager.supportsInterface(interfaceId); + + assert.equal( + supports, + true, + 'Expected seeker staking manager to support correct interface', + ); + + const invalidAbi = ['function foo(uint256 duration) external']; + + const invalidAbiInterfaceId = getInterfaceId(invalidAbi); + + const invalid = await seekerStakingManager.supportsInterface( + invalidAbiInterfaceId, + ); + + assert.equal( + invalid, + false, + 'Expected seeker staking manager to not support incorrect interface', + ); + }); + + function compareStakedSeekers( + seeker1: StakedSeeker, + seeker2: StakedSeeker, + ): void { + assert.equal( + seeker1.seekerId === seeker2.seekerId && + seeker1.node === seeker2.node && + seeker1.user === seeker2.user, + true, + ); + } +}); diff --git a/test/staking/syloStakingManager.test.ts b/test/staking/syloStakingManager.test.ts index 7a77064b..f1fc7826 100644 --- a/test/staking/syloStakingManager.test.ts +++ b/test/staking/syloStakingManager.test.ts @@ -19,12 +19,12 @@ describe('Sylo Staking', () => { it('cannot initialize sylo staking manager with invalid arguemnts', async () => { const factory = await ethers.getContractFactory('SyloStakingManager'); - const syloStakingManager = await factory.deploy(); + const syloStakingManagerTemp = await factory.deploy(); await expect( - syloStakingManager.initialize(ethers.ZeroAddress, 100n), + syloStakingManagerTemp.initialize(ethers.ZeroAddress, 100n), ).to.be.revertedWithCustomError( - syloStakingManager, + syloStakingManagerTemp, 'SyloAddressCannotBeNil', ); }); diff --git a/test/utils.ts b/test/utils.ts index 33cfef48..c7f7bf58 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -14,41 +14,57 @@ export type DeploymentOptions = { export async function deployContracts( opts: DeploymentOptions = {}, ): Promise { + // Sylo Token const SyloTokenFactory = await ethers.getContractFactory('SyloToken'); const syloToken = await SyloTokenFactory.deploy(); + // Sylo Staking Manager const syloStakingManagerOpts = { unlockDuration: opts.syloStakingManager?.unlockDuration ?? 10, }; - const SyloStakingManagerFactory = await ethers.getContractFactory( 'SyloStakingManager', ); const syloStakingManager = await SyloStakingManagerFactory.deploy(); - await syloStakingManager.initialize( - await syloToken.getAddress(), - syloStakingManagerOpts.unlockDuration, - ); - + // Seeker Stats Oracle const seekerStatsOracleOpts = { oracleAccount: opts.seekerStatsOralce?.oracleAccount ?? '0xd9D6945dfe8c1C7aFaFcDF8bf1D1c5beDfeccABF', }; - const seekerStatsOracleFactory = await ethers.getContractFactory( 'SeekerStatsOracle', ); - const seekerStatsOracle = await seekerStatsOracleFactory.deploy(); + // Seekers + const SeekersFactory = await ethers.getContractFactory('TestSeekers'); + const seekers = await SeekersFactory.deploy(); + + // Seeker Staking Manager + const seekerStakingManagerFactor = await ethers.getContractFactory( + 'SeekerStakingManager', + ); + const seekerStakingManager = await seekerStakingManagerFactor.deploy(); + + // Initliaze + await syloStakingManager.initialize( + await syloToken.getAddress(), + syloStakingManagerOpts.unlockDuration, + ); await seekerStatsOracle.initialize(seekerStatsOracleOpts.oracleAccount); + await seekerStakingManager.initialize( + await seekers.getAddress(), + await seekerStatsOracle.getAddress(), + ); return { syloToken, syloStakingManager, seekerStatsOracle, + seekerStakingManager, + seekers, }; }