-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
246 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 GitHub Actions / build
|
||
import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; | ||
Check warning on line 5 in contracts/Directory.sol GitHub Actions / build
|
||
import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; | ||
Check warning on line 6 in contracts/Directory.sol GitHub Actions / build
|
||
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters