Skip to content

Commit

Permalink
implement Directory interface
Browse files Browse the repository at this point in the history
  • Loading branch information
Nick95550 committed Jun 26, 2024
1 parent a43e8c9 commit ff59bae
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 1 deletion.
11 changes: 10 additions & 1 deletion common/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export const DeployedContractNames = {
authorizedAccounts: 'AuthorizedAccounts',
rewardsManager: 'RewardsManager',
ticketing: 'Ticketing',
StakingOrchestrator: 'StakingOrchestrator',
stakingOrchestrator: 'StakingOrchestrator',
direcrory: 'Directory',
};

export const ContractNames = {
Expand All @@ -34,6 +35,7 @@ export type SyloContracts = {
rewardsManager: factories.contracts.RewardsManager;
ticketing: factories.contracts.Ticketing;
stakingOrchestrator: factories.contracts.staking.StakingOrchestrator;
directory: factories.contracts.Directory;
};

export type ContractAddresses = {
Expand All @@ -48,6 +50,7 @@ export type ContractAddresses = {
rewardsManager: string;
ticketing: string;
stakingOrchestator: string;
directory: string;
};

export function connectContracts(
Expand Down Expand Up @@ -109,6 +112,11 @@ export function connectContracts(
provider,
);

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

return {
syloToken,
syloStakingManager,
Expand All @@ -121,5 +129,6 @@ export function connectContracts(
rewardsManager,
ticketing,
stakingOrchestrator,
directory,
};
}
135 changes: 135 additions & 0 deletions contracts/Directory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.18;

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

Check warning on line 4 in contracts/Directory.sol

View workflow job for this annotation

GitHub Actions / build

global import of path @openzeppelin/contracts/token/ERC20/utils/SafeERC20.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/Directory.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/utils/introspection/ERC165.sol";

Check warning on line 6 in contracts/Directory.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 "./staking/StakingOrchestrator.sol";
import "./ProtocolTimeManager.sol";

import "./IDirectory.sol";

contract Directory is IDirectory, Initializable, Ownable2StepUpgradeable, ERC165 {
StakingOrchestrator public stakingOrchestrator;
ProtocolTimeManager public protocolTimeManager;

/**
* @notice Tracks each directory, these directories are apart of
* each staking period for each reward cycle
*/
mapping(uint256 => mapping(uint256 => Directory)) public directories;

error CannotInitialiseWithZeroStakingOrchestratorAddress();
error CannotInitialiseWithZeroProtocolTimeManagerAddress();
error CannotJoinDirectoryWithZeroStake();
error StakeeAlreadyJoinedEpoch();

function initialize(
StakingOrchestrator _stakingOrchestrator,
ProtocolTimeManager _protocolTimeManager
) external initializer {
Ownable2StepUpgradeable.__Ownable2Step_init();

if (address(_stakingOrchestrator) == address(0)) {
revert CannotInitialiseWithZeroStakingOrchestratorAddress();
}
if (address(_protocolTimeManager) == address(0)) {
revert CannotInitialiseWithZeroProtocolTimeManagerAddress();
}

stakingOrchestrator = _stakingOrchestrator;
protocolTimeManager = _protocolTimeManager;
}

/**
* @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(IDirectory).interfaceId || super.supportsInterface(interfaceId);
}

function scan(uint128 point) external view returns (address) {
uint256 currentStakingPeriod = protocolTimeManager.getCurrentPeriod();
return _scan(point, currentStakingPeriod);
}

function scanWithStakingPeriod(
uint128 point,
uint256 stakingPeriodId
) external view returns (address) {
return _scan(point, stakingPeriodId);
}

/**
* @notice Call this to perform a stake-weighted scan to find the Node assigned
* to the given point of the requested directory (internal).
* @dev The current implementation will perform a binary search through
* the directory. This can allow gas costs to be low if this needs to be
* used in a transaction.
* @param point The point, which will usually be a hash of a public key.
* @param stakingPeriodId The epoch id associated with the directory to scan.
*/
function _scan(uint128 point, uint256 stakingPeriodId) internal view returns (address stakee) {
uint256 currentRewardCycle = protocolTimeManager.getCurrentCycle();
uint256 entryLength = directories[currentRewardCycle][stakingPeriodId].entries.length;

if (entryLength == 0) {
return address(0);
}

// Staking all the Sylo would only be 94 bits, so multiplying this with
// a uint128 cannot overflow a uint256.
uint256 expectedVal = (directories[currentRewardCycle][stakingPeriodId].totalStake *
uint256(point)) >> 128;

uint256 left;
uint256 right = entryLength - 1;

// perform a binary search through the directory
uint256 lower;
uint256 upper;
uint256 index;

while (left <= right) {
index = (left + right) >> 1;

lower = index == 0
? 0
: directories[currentRewardCycle][stakingPeriodId].entries[index - 1].boundary;
upper = directories[currentRewardCycle][stakingPeriodId].entries[index].boundary;

if (expectedVal >= lower && expectedVal < upper) {
return directories[currentRewardCycle][stakingPeriodId].entries[index].stakee;
} else if (expectedVal < lower) {
right = index - 1;
} else {
// expectedVal >= upper
left = index + 1;
}
}
}

function joinNextDirectory() external {
uint256 currentRewardCycle = protocolTimeManager.getCurrentCycle();
uint256 currentStakingPeriod = protocolTimeManager.getCurrentPeriod();

uint256 nodeStake = stakingOrchestrator.getNodeCurrentStake(msg.sender);
if (nodeStake == 0) {
revert CannotJoinDirectoryWithZeroStake();
}

if (directories[currentRewardCycle][currentStakingPeriod].stakes[msg.sender] > 0) {
revert StakeeAlreadyJoinedEpoch();
}

uint256 nextBoundary = directories[currentRewardCycle][currentStakingPeriod].totalStake +
nodeStake;

directories[currentRewardCycle][currentStakingPeriod].entries.push(
DirectoryEntry(msg.sender, nextBoundary)
);
directories[currentRewardCycle][currentStakingPeriod].stakes[msg.sender] = nodeStake;
directories[currentRewardCycle][currentStakingPeriod].totalStake = nextBoundary;
}
}
36 changes: 36 additions & 0 deletions contracts/IDirectory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.18;

interface IDirectory {
/**
* @dev A DirectoryEntry will be stored for every node that joins the
* network in a specific period. The entry will contain the stakee's
* address, and a boundary value which is a sum of the current directory's
* total stake, and the current stakee's total stake.
*/
struct DirectoryEntry {
address stakee;
uint256 boundary;
}

/**
* @dev An EpochDirectory will be stored for every period. The
* directory will be constructed piece by piece as Nodes join,
* each adding their own directory entry based on their current
* stake value.
*/
struct Directory {
DirectoryEntry[] entries;
mapping(address => uint256) stakes;
uint256 totalStake;
}

function scan(uint128 point) external returns (address);

function scanWithStakingPeriod(
uint128 point,
uint256 stakingPeriodId
) external returns (address);

function joinNextDirectory() external;
}
53 changes: 53 additions & 0 deletions test/Directory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ethers } from 'hardhat';
import { deployContracts } from './utils';
import { expect } from 'chai';
import { SyloContracts } from '../common/contracts';
import {
Directory,
StakingOrchestrator,
ProtocolTimeManager,
} from '../typechain-types';

describe.only('Directory', () => {
let contracts: SyloContracts;
let directory: Directory;
let stakingOrchestator: StakingOrchestrator;
let protocolTimeManager: ProtocolTimeManager;

beforeEach(async () => {
contracts = await deployContracts();
directory = contracts.directory;
stakingOrchestator = contracts.stakingOrchestrator;
protocolTimeManager = contracts.protocolTimeManager;
});

it('cannot initialize directory with empty StakingOrchestrator address', async () => {
const directoryFactory = await ethers.getContractFactory('Directory');
const directoryTemp = await directoryFactory.deploy();

await expect(
directory.initialize(
ethers.ZeroAddress,
await protocolTimeManager.getAddress(),
),
).to.be.revertedWithCustomError(
directoryTemp,
'CannotInitialiseWithZeroStakingOrchestratorAddress',
);
});

it('cannot initialize directory with empty ProtocolTimeManager address', async () => {
const directoryFactory = await ethers.getContractFactory('Directory');
const directoryTemp = await directoryFactory.deploy();

await expect(
directory.initialize(
await stakingOrchestator.getAddress(),
ethers.ZeroAddress,
),
).to.be.revertedWithCustomError(
directoryTemp,
'CannotInitialiseWithZeroStakingOrchestratorAddress',
);
});
});
12 changes: 12 additions & 0 deletions test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export async function deployContracts(
'RewardsManager',
);
const ticketingFactory = await ethers.getContractFactory('Ticketing');
const stakingOrchestratorFactory = await ethers.getContractFactory(
'StakingOrchestrator',
);
const directoryFactory = await ethers.getContractFactory('Directory');

// Deploy
const syloToken = await syloTokenFactory.deploy();
Expand All @@ -56,6 +60,8 @@ export async function deployContracts(
const authorizedAccounts = await authorizedAccountsFactory.deploy();
const rewardsManager = await rewardsManagerFactory.deploy();
const ticketing = await ticketingFactory.deploy();
const stakingOrchestrator = await stakingOrchestratorFactory.deploy();
const directory = await directoryFactory.deploy();

// Options
const syloStakingManagerOpts = {
Expand Down Expand Up @@ -88,6 +94,10 @@ export async function deployContracts(
await authorizedAccounts.initialize();
await ticketing.initialize(await rewardsManager.getAddress());
await rewardsManager.initialize(await registries.getAddress(), ticketing);
await directory.initialize(
await stakingOrchestrator.getAddress(),
await protocolTimeManager.getAddress(),
);
await protocolTimeManager.initialize(
protocolTimeManagerOpts.cycleDuration,
protocolTimeManagerOpts.periodDuration,
Expand All @@ -104,6 +114,8 @@ export async function deployContracts(
authorizedAccounts,
rewardsManager,
ticketing,
stakingOrchestrator,
directory,
};
}

Expand Down

0 comments on commit ff59bae

Please sign in to comment.