Skip to content

Commit

Permalink
Merge pull request #171 from futureversecom/SM-350-seekers-stats-oracle
Browse files Browse the repository at this point in the history
Sm 350 seekers stats oracle
  • Loading branch information
teinnt authored Jun 13, 2024
2 parents 6c086b8 + 49d1f09 commit 718ad98
Show file tree
Hide file tree
Showing 11 changed files with 1,676 additions and 94 deletions.
105 changes: 12 additions & 93 deletions common/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,11 @@ import * as factories from '../typechain-types';

export const FixedContractNames = {
syloToken: 'SyloToken',
seekers: 'Seekers',
};

export const DeployedContractNames = {
authorizedAccounts: 'AuthorizedAccounts',
registries: 'Registries',
ticketingParameters: 'TicketingParameters',
epochsManager: 'EpochsManager',
stakingManager: 'StakingManager',
rewardsManager: 'RewardsManager',
directory: 'Directory',
syloTicketing: 'SyloTicketing',
seekerPowerOracle: 'SeekerPowerOracle',
syloStakingManager: 'SyloStakingManager',
seekerStatsOracle: 'SeekerStatsOracle',
};

export const ContractNames = {
Expand All @@ -25,32 +17,14 @@ export const ContractNames = {

export type SyloContracts = {
syloToken: factories.contracts.SyloToken;
authorizedAccounts: factories.contracts.AuthorizedAccounts;
registries: factories.contracts.Registries;
ticketingParameters: factories.contracts.payments.ticketing.TicketingParameters;
epochsManager: factories.contracts.epochs.EpochsManager;
stakingManager: factories.contracts.staking.StakingManager;
rewardsManager: factories.contracts.payments.ticketing.RewardsManager;
directory: factories.contracts.staking.Directory;
syloTicketing: factories.contracts.payments.SyloTicketing;
seekerPowerOracle: factories.contracts.SeekerPowerOracle;
seekers: factories.contracts.mocks.TestSeekers;
futurepassRegistrar: factories.contracts.mocks.TestFuturepassRegistrar;
syloStakingManager: factories.contracts.staking.sylo.SyloStakingManager;
seekerStatsOracle: factories.contracts.staking.seekers.SeekerStatsOracle;
};

export type ContractAddresses = {
syloToken: string;
authorizedAccounts: string;
registries: string;
ticketingParameters: string;
epochsManager: string;
stakingManager: string;
rewardsManager: string;
directory: string;
syloTicketing: string;
seekers: string;
seekerPowerOracle: string;
futurepassRegistrar: string;
syloStakingManager: string;
seekerStatsOracle: string;
};

export function connectContracts(
Expand All @@ -62,74 +36,19 @@ export function connectContracts(
provider,
);

const authorizedAccounts = factories.AuthorizedAccounts__factory.connect(
contracts.authorizedAccounts,
const syloStakingManager = factories.SyloStakingManager__factory.connect(
contracts.syloStakingManager,
provider,
);

const registries = factories.Registries__factory.connect(
contracts.registries,
const seekerStatsOracle = factories.SeekerStatsOracle__factory.connect(
contracts.seekerStatsOracle,
provider,
);

const ticketingParameters = factories.TicketingParameters__factory.connect(
contracts.ticketingParameters,
provider,
);

const epochsManager = factories.EpochsManager__factory.connect(
contracts.epochsManager,
provider,
);

const stakingManager = factories.StakingManager__factory.connect(
contracts.stakingManager,
provider,
);

const rewardsManager = factories.RewardsManager__factory.connect(
contracts.rewardsManager,
provider,
);

const directory = factories.Directory__factory.connect(
contracts.directory,
provider,
);

const syloTicketing = factories.SyloTicketing__factory.connect(
contracts.syloTicketing,
provider,
);

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

const seekerPowerOracle = factories.SeekerPowerOracle__factory.connect(
contracts.seekerPowerOracle,
provider,
);

const futurepassRegistrar =
factories.TestFuturepassRegistrar__factory.connect(
contracts.futurepassRegistrar,
provider,
);

return {
syloToken,
authorizedAccounts,
registries,
ticketingParameters,
epochsManager,
stakingManager,
rewardsManager,
directory,
syloTicketing,
seekers,
seekerPowerOracle,
futurepassRegistrar,
syloStakingManager,
seekerStatsOracle,
};
}
10 changes: 10 additions & 0 deletions contracts/SyloToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.18;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract SyloToken is ERC20 {
constructor() ERC20("Sylo", "SYLO") {
_mint(msg.sender, 10_000_000_000 ether);
}
}
25 changes: 25 additions & 0 deletions contracts/staking/seekers/ISeekerStatsOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.18;

interface ISeekerStatsOracle {
struct Seeker {
uint256 seekerId;
uint256 rank;
uint256 attrReactor;
uint256 attrCores;
uint256 attrDurability;
uint256 attrSensors;
uint256 attrStorage;
uint256 attrChip;
}

function setOracle(address _seekerStatsOracleAccount) external;

function createProofMessage(Seeker calldata seeker) external pure returns (bytes memory);

function registerSeekerRestricted(Seeker calldata seeker) external;

function registerSeeker(Seeker calldata seeker, bytes calldata proof) external;

function calculateAttributeCoverage(Seeker[] calldata seekers) external view returns (int256);
}
210 changes: 210 additions & 0 deletions contracts/staking/seekers/SeekerStatsOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// 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/SeekerStatsOracle.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-upgradeable/access/Ownable2StepUpgradeable.sol";

Check warning on line 5 in contracts/staking/seekers/SeekerStatsOracle.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 6 in contracts/staking/seekers/SeekerStatsOracle.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 "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

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

View workflow job for this annotation

GitHub Actions / build

global import of path @openzeppelin/contracts/utils/cryptography/ECDSA.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)
import "solidity-trigonometry/src/Trigonometry.sol";

Check warning on line 8 in contracts/staking/seekers/SeekerStatsOracle.sol

View workflow job for this annotation

GitHub Actions / build

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

import "./ISeekerStatsOracle.sol";

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

View workflow job for this annotation

GitHub Actions / build

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

contract SeekerStatsOracle is ISeekerStatsOracle, Initializable, Ownable2StepUpgradeable, ERC165 {
/**
* @notice The oracle account. This contract accepts any attestations of
* Seeker power that have been signed by this account.
*/
address public oracle;

/**
* @notice Tracks the set of Seeker Stats and Rank with Seeker ID
*/
mapping(uint256 => Seeker) public seekerStats;

/**
* @notice Holds the angle used for coverage calculation in radians
*/
int256 private coverageAngle =
Trigonometry.sin(((Trigonometry.TWO_PI / 6) + Trigonometry.TWO_PI));

event SeekerStatsUpdated(
uint256 indexed seekerId,
uint256 attrReactor,
uint256 attrCores,
uint256 attrDurability,
uint256 attrSensors,
uint256 attrStorage,
uint256 attrChip
);

/** events **/
event OracleUpdated(address oracle);

/** errors **/
error OracleAddressCannotBeNil();
error SeekerProofIsEmpty();
error UnauthorizedRegisterSeekerStats();
error InvalidSignatureForSeekerProof();
error SeekerNotRegistered(uint256 seekerId);

function initialize(address _oracle) external initializer {
if (_oracle == address(0)) {
revert OracleAddressCannotBeNil();
}

Ownable2StepUpgradeable.__Ownable2Step_init();

oracle = _oracle;
}

/**
* @notice Returns true if the contract implements the interface defined by
* `interfaceId` from ERC165.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(ISeekerStatsOracle).interfaceId;
}

/**
* @notice Sets the oracle account.
* @param _oracle The oracle account.
*/
function setOracle(address _oracle) external onlyOwner {
if (_oracle == address(0)) {
revert OracleAddressCannotBeNil();
}
oracle = _oracle;

emit OracleUpdated(_oracle);
}

/**
* @notice Returns true if the oracle account signed the proof message for the given seeker.
* @param seeker The object containing the seekers statistics.
* @param signature The signature of the seekers proof message, signed by the oracle account.
*/
function isSeekerStatsProofValid(
Seeker calldata seeker,
bytes calldata signature
) internal view returns (bool) {
bytes memory proof = _createProofMessage(seeker);
bytes32 ecdsaHash = ECDSA.toEthSignedMessageHash(proof);
address signerAddress = ECDSA.recover(ecdsaHash, signature);
if (signerAddress == oracle) {
return true;
} else {
return false;
}
}

/**
* @notice Creates a unique proofing message for the provided seeker.
* @param seeker The object containing the seekers statistics.
*/
function _createProofMessage(Seeker calldata seeker) internal pure returns (bytes memory) {
return
abi.encodePacked(
seeker.seekerId,
seeker.rank,
seeker.attrChip,
seeker.attrDurability,
seeker.attrSensors,
seeker.attrCores,
seeker.attrStorage,
seeker.attrReactor
);
}

/**
* @notice Creates a proofing message unique to the provided seeker.
* @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();
}

seekerStats[seeker.seekerId] = seeker;
emit SeekerStatsUpdated(
seeker.seekerId,
seeker.attrReactor,
seeker.attrCores,
seeker.attrDurability,
seeker.attrSensors,
seeker.attrStorage,
seeker.attrChip
);
}

/**
* @notice Registers a seeker
* @param seeker The object containing the seekers statistics.
* @param proof The signature of the seekers proof message, signed by the oracle account.
*/
function registerSeeker(Seeker calldata seeker, bytes calldata proof) external {
if (!isSeekerStatsProofValid(seeker, proof)) {
revert InvalidSignatureForSeekerProof();
}

seekerStats[seeker.seekerId] = seeker;
emit SeekerStatsUpdated(
seeker.seekerId,
seeker.attrReactor,
seeker.attrCores,
seeker.attrDurability,
seeker.attrSensors,
seeker.attrStorage,
seeker.attrChip
);
}

/**
* @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
* in attributes of the seekers staked against the node.
* @param seekers A list containing seekers, will revert if any seeker is not registered.
*/
function calculateAttributeCoverage(Seeker[] calldata seekers) external view returns (int256) {
int256 coverage = 0;

int256 totalReactor = 0;
int256 totalCores = 0;
int256 totalDurability = 0;
int256 totalSensors = 0;
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))) {
revert SeekerNotRegistered(seeker.seekerId);
}

totalReactor += int256(registeredSeeker.attrReactor);
totalCores += int256(registeredSeeker.attrCores);
totalDurability += int256(registeredSeeker.attrDurability);
totalSensors += int256(registeredSeeker.attrSensors);
totalStorage += int256(registeredSeeker.attrStorage);
totalChip += int256(registeredSeeker.attrChip);
}

coverage += (int256(totalReactor) * coverageAngle * int256(totalCores)) / 2;
coverage += (int256(totalCores) * coverageAngle * int256(totalDurability)) / 2;
coverage += (int256(totalDurability) * coverageAngle * int256(totalSensors)) / 2;
coverage += (int256(totalSensors) * coverageAngle * int256(totalStorage)) / 2;
coverage += (int256(totalStorage) * coverageAngle * int256(totalChip)) / 2;
coverage += (int256(totalChip) * coverageAngle * int256(totalReactor)) / 2;

return coverage;
}
}
Loading

0 comments on commit 718ad98

Please sign in to comment.