From bf498e4e64e18d8e000824c0ac5b3fc0cee2a626 Mon Sep 17 00:00:00 2001 From: John Carlo San Pedro Date: Mon, 15 Jan 2024 15:19:35 +1300 Subject: [PATCH 01/10] implement calculateCapacityFromSeekerPower and use when joining directory --- contracts/epochs/EpochsManager.sol | 2 +- contracts/interfaces/staking/IDirectory.sol | 2 +- .../interfaces/staking/IStakingManager.sol | 2 ++ contracts/staking/Directory.sol | 18 ++++++++++-- contracts/staking/StakingManager.sol | 29 +++++++++++++++++++ 5 files changed, 49 insertions(+), 4 deletions(-) diff --git a/contracts/epochs/EpochsManager.sol b/contracts/epochs/EpochsManager.sol index cc636b2a..ceec1217 100644 --- a/contracts/epochs/EpochsManager.sol +++ b/contracts/epochs/EpochsManager.sol @@ -232,7 +232,7 @@ contract EpochsManager is IEpochsManager, Initializable, Ownable2StepUpgradeable activeSeekers[nextEpoch][seekerId] = msg.sender; _directory._rewardsManager().initializeNextRewardPool(msg.sender); - _directory.joinNextDirectory(msg.sender); + _directory.joinNextDirectory(msg.sender, seekerId); emit EpochJoined(nextEpoch, msg.sender, seekerId); } diff --git a/contracts/interfaces/staking/IDirectory.sol b/contracts/interfaces/staking/IDirectory.sol index 39b9f316..940163d9 100644 --- a/contracts/interfaces/staking/IDirectory.sol +++ b/contracts/interfaces/staking/IDirectory.sol @@ -27,7 +27,7 @@ interface IDirectory { function setCurrentDirectory(uint256 epochId) external; - function joinNextDirectory(address stakee) external; + function joinNextDirectory(address stakee, uint256 seekerId) external; function scan(uint128 point) external view returns (address stakee); diff --git a/contracts/interfaces/staking/IStakingManager.sol b/contracts/interfaces/staking/IStakingManager.sol index ca38405e..32791e1e 100644 --- a/contracts/interfaces/staking/IStakingManager.sol +++ b/contracts/interfaces/staking/IStakingManager.sol @@ -49,6 +49,8 @@ interface IStakingManager { function cancelUnlocking(uint256 amount, address stakee) external; + function calculateCapacityFromSeekerPower(uint256 seekerId) external view returns (uint256); + function calculateMaxAdditionalDelegatedStake(address stakee) external view returns (uint256); function getTotalManagedStake() external view returns (uint256); diff --git a/contracts/staking/Directory.sol b/contracts/staking/Directory.sol index 56145703..aac2b035 100644 --- a/contracts/staking/Directory.sol +++ b/contracts/staking/Directory.sol @@ -81,7 +81,12 @@ contract Directory is IDirectory, Initializable, Manageable, IERC165 { } /** - * @notice This function is called by a node as a prerequisite to participate in the next epoch. + * @notice This function is called by the epochs manager as a prerequisite to when the node joins the next epoch. + * @param stakee The address of the node. + * @param seekerId The seekerId of the Seeker that the node is + * registered with when joining the epoch. It is used to determine the nodes + * staking capacity based on its seeker power. + * * @dev This will construct the directory as nodes join. The directory is constructed * by creating a boundary value which is a sum of the current directory's total stake, and * the current stakee's total stake, and pushing the new boundary into the entries array. @@ -100,7 +105,7 @@ contract Directory is IDirectory, Initializable, Manageable, IERC165 { * |-----------|------|----------------|--------| * Alice/20 Bob/30 Carl/70 Dave/95 */ - function joinNextDirectory(address stakee) external onlyManager { + function joinNextDirectory(address stakee, uint256 seekerId) external onlyManager { if (stakee == address(0)) { revert StakeeCannotBeZeroAddress(); } @@ -113,6 +118,15 @@ contract Directory is IDirectory, Initializable, Manageable, IERC165 { } uint256 currentStake = _stakingManager.getCurrentStakerAmount(stakee, stakee); + + uint256 seekerStakingCapacity = _stakingManager.calculateCapacityFromSeekerPower(seekerId); + + // we take the minimum value between the currentStake and the current + // staking capacity + if (currentStake > seekerStakingCapacity) { + currentStake = seekerStakingCapacity; + } + uint16 ownedStakeProportion = SyloUtils.asPerc( SafeCast.toUint128(currentStake), totalStake diff --git a/contracts/staking/StakingManager.sol b/contracts/staking/StakingManager.sol index bc5dd21f..dd74fade 100644 --- a/contracts/staking/StakingManager.sol +++ b/contracts/staking/StakingManager.sol @@ -9,6 +9,7 @@ import "../libraries/SyloUtils.sol"; import "../epochs/EpochsManager.sol"; import "../payments/ticketing/RewardsManager.sol"; import "../interfaces/staking/IStakingManager.sol"; +import "../interfaces/ISeekerPowerOracle.sol"; /** * @notice Manages stakes and delegated stakes for Nodes. Holding @@ -28,6 +29,8 @@ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeab EpochsManager public _epochsManager; + ISeekerPowerOracle public _seekerPowerOracle; + /** * @notice Tracks the managed stake for every Node. */ @@ -70,11 +73,13 @@ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeab error CannotCancelUnlockZeroAmount(); error CannotUnlockMoreThanStaked(uint256 stakeAmount, uint256 unlockAmount); error StakeCapacityReached(uint256 maxCapacity, uint256 currentCapacity); + error SeekerPowerNotRegistered(uint256 seekerId); function initialize( IERC20 token, RewardsManager rewardsManager, EpochsManager epochsManager, + ISeekerPowerOracle seekerPowerOracle, uint256 _unlockDuration, uint16 _minimumStakeProportion ) external initializer { @@ -94,6 +99,12 @@ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeab type(IEpochsManager).interfaceId ); + SyloUtils.validateContractInterface( + "SeekerPowerOracle", + address(seekerPowerOracle), + type(ISeekerPowerOracle).interfaceId + ); + if (_unlockDuration == 0) { revert UnlockDurationCannotBeZero(); } @@ -103,6 +114,7 @@ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeab _token = token; _rewardsManager = rewardsManager; _epochsManager = epochsManager; + _seekerPowerOracle = seekerPowerOracle; unlockDuration = _unlockDuration; minimumStakeProportion = _minimumStakeProportion; } @@ -301,6 +313,23 @@ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeab _addStake(amount, stakee); } + /** + * @notice This function determines the staking capacity of + * a Seeker based on its power level. The method will revert if + * the Seeker's power level has not been registered with the oracle. + * + * Currently the algorithm is as follows: + * staking_capacity = seeker_power^2 + */ + function calculateCapacityFromSeekerPower(uint256 seekerId) external view returns (uint256) { + uint256 seekerPower = _seekerPowerOracle.getSeekerPower(seekerId); + if (seekerPower == 0) { + revert SeekerPowerNotRegistered(seekerId); + } + + return seekerId ** 2; + } + /** * @notice This function should be called by clients to determine how much * additional delegated stake can be allocated to a Node via an addStake or From eaf0d270d7a4a157cab65f37b833c0bbdaf28dae Mon Sep 17 00:00:00 2001 From: John Carlo San Pedro Date: Tue, 16 Jan 2024 14:23:06 +1300 Subject: [PATCH 02/10] add test for seeker staking capacity --- contracts/SeekerPowerOracle.sol | 11 +- contracts/staking/Directory.sol | 10 +- contracts/staking/StakingManager.sol | 14 +- test/payments/multiReceiverTicketing.test.ts | 57 ++- test/payments/ticketing.test.ts | 352 ++++++++++++++++--- test/payments/utils.ts | 3 + test/registries.test.ts | 9 +- test/staking.test.ts | 207 ++++++++--- test/utils.ts | 14 +- 9 files changed, 569 insertions(+), 108 deletions(-) diff --git a/contracts/SeekerPowerOracle.sol b/contracts/SeekerPowerOracle.sol index 6ed036b9..fda4bd1d 100644 --- a/contracts/SeekerPowerOracle.sol +++ b/contracts/SeekerPowerOracle.sol @@ -5,6 +5,7 @@ import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import "./interfaces/ISeekerPowerOracle.sol"; @@ -13,7 +14,7 @@ import "./interfaces/ISeekerPowerOracle.sol"; * a Seeker's power level via a restricted oracle account call. Seeker Power can also * be set by any account if the correct Oracle signature proof is provided. */ -contract SeekerPowerOracle is ISeekerPowerOracle, Initializable, Ownable2StepUpgradeable { +contract SeekerPowerOracle is ISeekerPowerOracle, Initializable, Ownable2StepUpgradeable, ERC165 { /** * @notice The oracle account. This contract accepts any attestations of * Seeker power that have been signed by this account. @@ -43,6 +44,14 @@ contract SeekerPowerOracle is ISeekerPowerOracle, Initializable, Ownable2StepUpg 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(ISeekerPowerOracle).interfaceId; + } + /** * @notice Sets the oracle account. * @param _oracle The oracle account. diff --git a/contracts/staking/Directory.sol b/contracts/staking/Directory.sol index aac2b035..0f0ea878 100644 --- a/contracts/staking/Directory.sol +++ b/contracts/staking/Directory.sol @@ -117,16 +117,16 @@ contract Directory is IDirectory, Initializable, Manageable, IERC165 { revert NoStakeToJoinEpoch(); } - uint256 currentStake = _stakingManager.getCurrentStakerAmount(stakee, stakee); - uint256 seekerStakingCapacity = _stakingManager.calculateCapacityFromSeekerPower(seekerId); - // we take the minimum value between the currentStake and the current + // we take the minimum value between the total stake and the current // staking capacity - if (currentStake > seekerStakingCapacity) { - currentStake = seekerStakingCapacity; + if (totalStake > seekerStakingCapacity) { + totalStake = seekerStakingCapacity; } + uint256 currentStake = _stakingManager.getCurrentStakerAmount(stakee, stakee); + uint16 ownedStakeProportion = SyloUtils.asPerc( SafeCast.toUint128(currentStake), totalStake diff --git a/contracts/staking/StakingManager.sol b/contracts/staking/StakingManager.sol index dd74fade..5265b031 100644 --- a/contracts/staking/StakingManager.sol +++ b/contracts/staking/StakingManager.sol @@ -18,6 +18,10 @@ import "../interfaces/ISeekerPowerOracle.sol"; * and delegated stakers are rewarded on a pro-rata basis. */ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeable, ERC165 { + // The maximum possible SYLO that exists in the network. Naturally + // represents the maximum possible SYLO that can be staked. + uint256 internal constant MAX_SYLO = 10_000_000_000 ether; + /** ERC 20 compatible token we are dealing with */ IERC20 public _token; @@ -327,7 +331,15 @@ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeab revert SeekerPowerNotRegistered(seekerId); } - return seekerId ** 2; + // If the Seeker Power is already + // at the maximum sylo, then we just return the max sylo value directly. + if (seekerPower >= MAX_SYLO) { + return MAX_SYLO; + } + + uint256 capacity = seekerPower ** 2 * 1 ether; + + return capacity > MAX_SYLO ? MAX_SYLO : capacity; } /** diff --git a/test/payments/multiReceiverTicketing.test.ts b/test/payments/multiReceiverTicketing.test.ts index 1cbb5e4c..5d21924a 100644 --- a/test/payments/multiReceiverTicketing.test.ts +++ b/test/payments/multiReceiverTicketing.test.ts @@ -5,6 +5,7 @@ import { EpochsManager, Registries, RewardsManager, + SeekerPowerOracle, StakingManager, SyloTicketing, SyloToken, @@ -34,6 +35,7 @@ describe('MultiReceiverTicketing', () => { let registries: Registries; let stakingManager: StakingManager; let seekers: TestSeekers; + let seekerPowerOracle: SeekerPowerOracle; let futurepassRegistrar: TestFuturepassRegistrar; before(async () => { @@ -57,6 +59,7 @@ describe('MultiReceiverTicketing', () => { registries = contracts.registries; stakingManager = contracts.stakingManager; seekers = contracts.seekers; + seekerPowerOracle = contracts.seekerPowerOracle; futurepassRegistrar = contracts.futurepassRegistrar; await token.approve(await stakingManager.getAddress(), toSOLOs(10000000)); @@ -65,7 +68,14 @@ describe('MultiReceiverTicketing', () => { it('can redeem multi receiver ticket', async () => { await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -115,7 +125,14 @@ describe('MultiReceiverTicketing', () => { await ticketingParameters.setTicketDuration(10000000000); await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -295,7 +312,14 @@ describe('MultiReceiverTicketing', () => { await ticketingParameters.setBaseLiveWinProb(0); await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -355,7 +379,14 @@ describe('MultiReceiverTicketing', () => { it('cannot redeem multi receiver ticket if node has not joined epoch', async () => { await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.initializeEpoch(); @@ -391,7 +422,14 @@ describe('MultiReceiverTicketing', () => { it('can not redeem for non valid receiver', async () => { await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -428,7 +466,14 @@ describe('MultiReceiverTicketing', () => { it('can not redeem for the same user more than once', async () => { await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); diff --git a/test/payments/ticketing.test.ts b/test/payments/ticketing.test.ts index 35d3054b..d7d80f2d 100644 --- a/test/payments/ticketing.test.ts +++ b/test/payments/ticketing.test.ts @@ -1,6 +1,6 @@ import { ethers } from 'hardhat'; import { assert, expect } from 'chai'; -import { Signer, Wallet } from 'ethers'; +import { MaxUint256, Signer, Wallet } from 'ethers'; import { BigNumber } from '@ethersproject/bignumber'; import { AuthorizedAccounts, @@ -15,6 +15,7 @@ import { TicketingParameters, ISyloTicketing__factory, TestFuturepassRegistrar, + SeekerPowerOracle, } from '../../typechain-types'; import crypto from 'crypto'; import { @@ -46,6 +47,7 @@ describe('Ticketing', () => { let stakingManager: StakingManager; let seekers: TestSeekers; let authorizedAccounts: AuthorizedAccounts; + let seekerPowerOracle: SeekerPowerOracle; let futurepassRegistrar: TestFuturepassRegistrar; enum Permission { @@ -85,6 +87,7 @@ describe('Ticketing', () => { stakingManager = contracts.stakingManager; seekers = contracts.seekers; authorizedAccounts = contracts.authorizedAccounts; + seekerPowerOracle = contracts.seekerPowerOracle; futurepassRegistrar = contracts.futurepassRegistrar; await token.approve(await stakingManager.getAddress(), toSOLOs(10000000)); @@ -558,7 +561,14 @@ describe('Ticketing', () => { it('should be able to initialize next reward pool', async () => { await stakingManager.addStake(30, owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); const currentBlock = await ethers.provider.getBlockNumber(); @@ -580,12 +590,26 @@ describe('Ticketing', () => { it('can not initialize reward pool more than once', async () => { await stakingManager.addStake(30, owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); // change the seeker but node should still be prevented from // initializing the reward pool again - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 2); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 2, + ); await expect(epochsManager.joinNextEpoch()).to.be.revertedWithCustomError( rewardsManager, 'RewardPoolAlreadyExist', @@ -594,7 +618,14 @@ describe('Ticketing', () => { it('can not initialize reward pool more than once for the same seekers', async () => { await stakingManager.addStake(30, owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await expect(epochsManager.joinNextEpoch()) @@ -603,7 +634,14 @@ describe('Ticketing', () => { }); it('should not be able to initialize next reward pool without stake', async () => { - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await expect(epochsManager.joinNextEpoch()).to.be.revertedWithCustomError( rewardsManager, 'NoStakeToCreateRewardPool', @@ -716,7 +754,14 @@ describe('Ticketing', () => { it('cannot redeem ticket using sender delegated account to sign without permission', async () => { await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -750,7 +795,14 @@ describe('Ticketing', () => { it('cannot redeem ticket using receiver delegated account to sign without permission', async () => { await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -803,7 +855,14 @@ describe('Ticketing', () => { it('cannot redeem ticket using sender delegated account to sign after unauthorizing account', async () => { await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -875,7 +934,14 @@ describe('Ticketing', () => { await ticketingParameters.setDecayRate(1); await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -932,7 +998,14 @@ describe('Ticketing', () => { await ticketingParameters.setDecayRate(1); await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -986,7 +1059,14 @@ describe('Ticketing', () => { it('can redeem ticket using sender authorized account to sign with valid permission', async () => { await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -1035,7 +1115,14 @@ describe('Ticketing', () => { it('can redeem ticket using sender and receiver authorized accounts to sign with valid permission', async () => { await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -1176,7 +1263,14 @@ describe('Ticketing', () => { }); it('cannot redeem ticket if node has not joined directory', async () => { - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.initializeEpoch(); @@ -1204,10 +1298,18 @@ describe('Ticketing', () => { it('cannot redeem ticket if node has not initialized reward pool', async () => { await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); + await seekerPowerOracle.registerSeekerPowerRestricted(1, MaxUint256); await directory.addManager(owner); - await directory.joinNextDirectory(owner); + await directory.joinNextDirectory(owner, 1); await epochsManager.initializeEpoch(); @@ -1232,7 +1334,14 @@ describe('Ticketing', () => { it('cannot redeem invalid ticket', async () => { await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -1337,6 +1446,7 @@ describe('Ticketing', () => { await utils.setSeekerRegistry( contracts.registries, contracts.seekers, + contracts.seekerPowerOracle, accounts[0], accounts[1], 1, @@ -1382,7 +1492,14 @@ describe('Ticketing', () => { it('can redeem winning ticket', async () => { await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -1426,7 +1543,14 @@ describe('Ticketing', () => { it('cannot redeem ticket more than once', async () => { await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -1465,7 +1589,14 @@ describe('Ticketing', () => { it('burns penalty on insufficient escrow', async () => { await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -1541,7 +1672,14 @@ describe('Ticketing', () => { it('can claim ticketing rewards', async () => { await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -1593,7 +1731,14 @@ describe('Ticketing', () => { }); it('delegated stakers should be able to claim rewards', async () => { - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); const { proportions } = await addStakes(token, stakingManager, owner, [ { account: accounts[0], stake: 3 }, @@ -1639,7 +1784,14 @@ describe('Ticketing', () => { }); it('should have rewards be automatically removed from pending when stake is updated', async () => { - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await addStakes(token, stakingManager, owner, [ { account: accounts[0], stake: 10000 }, @@ -1733,7 +1885,14 @@ describe('Ticketing', () => { }); it('can calculate staker claim if reward total is 0', async () => { - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await addStakes(token, stakingManager, owner, [ { account: accounts[0], stake: 3 }, @@ -1782,7 +1941,14 @@ describe('Ticketing', () => { it('can not claim reward more than once for the same epoch', async () => { await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -1822,7 +1988,14 @@ describe('Ticketing', () => { }); it('should be able to correctly calculate staking rewards for multiple epochs when managed stake is the same', async () => { - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); const { proportions } = await addStakes(token, stakingManager, owner, [ { account: accounts[0], stake: 1000 }, @@ -1892,7 +2065,14 @@ describe('Ticketing', () => { await syloTicketing.depositEscrow(toSOLOs(500000), alice.address); await syloTicketing.depositPenalty(toSOLOs(50), alice.address); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -1944,7 +2124,14 @@ describe('Ticketing', () => { }); it('should be able to correctly calculate staking rewards for multiple epochs when managed stake increases', async () => { - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await addStakes(token, stakingManager, owner, [ { account: accounts[0], stake: 1000 }, @@ -2043,7 +2230,14 @@ describe('Ticketing', () => { }); it('should be able to correctly calculate staking rewards for multiple epochs when managed stake decreases', async () => { - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await addStakes(token, stakingManager, owner, [ { account: accounts[0], stake: 1000 }, @@ -2134,7 +2328,14 @@ describe('Ticketing', () => { // to the Rewards contract calculation is made. // TODO: Create script to spin up new test network to run this test locally or for CI automatically. it('should calculate updated stake and rewards over several ticket redemptions without significant precision loss [ @skip-on-coverage ]', async () => { - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); const { proportions } = await addStakes(token, stakingManager, owner, [ { account: accounts[0], stake: 1000 }, @@ -2203,6 +2404,7 @@ describe('Ticketing', () => { await utils.setSeekerRegistry( contracts.registries, contracts.seekers, + contracts.seekerPowerOracle, accounts[0], accounts[1], 1, @@ -2243,7 +2445,14 @@ describe('Ticketing', () => { }); it('should be able to correctly calculate staker rewards if node was not active for multiple epochs', async () => { - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); const { proportions } = await addStakes(token, stakingManager, owner, [ { account: accounts[0], stake: 1000 }, @@ -2311,7 +2520,14 @@ describe('Ticketing', () => { { account: accounts[1], stake: 1 }, ]); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -2369,7 +2585,14 @@ describe('Ticketing', () => { { account: accounts[1], stake: 1 }, ]); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -2441,7 +2664,14 @@ describe('Ticketing', () => { it('can claim staking rewards again after previous ended', async () => { await stakingManager.addStake(toSOLOs(1), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -2495,7 +2725,14 @@ describe('Ticketing', () => { }); it('can claim staking rewards if node already joined next epoch', async () => { - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); const { proportions } = await addStakes(token, stakingManager, owner, [ { account: accounts[0], stake: 1000 }, @@ -2573,7 +2810,14 @@ describe('Ticketing', () => { }); it('can claim staking rewards if node already joined next epoch but skipped the current epoch', async () => { - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); const { proportions } = await addStakes(token, stakingManager, owner, [ { account: accounts[0], stake: 1000 }, @@ -2653,7 +2897,14 @@ describe('Ticketing', () => { } await stakingManager.addStake(toSOLOs(1000), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); // have account 2, 3 and 4 as delegated stakers with varying levels of stake await stakingManager.connect(accounts[2]).addStake(toSOLOs(250), owner); @@ -2704,7 +2955,14 @@ describe('Ticketing', () => { it('can retrieve total staking rewards for an epoch', async () => { await stakingManager.addStake(toSOLOs(1000), owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); const alice = Wallet.createRandom(); const bob = Wallet.createRandom(); @@ -2778,7 +3036,14 @@ describe('Ticketing', () => { await ticketingParameters.setFaceValue(ethers.parseEther('10000')); await stakingManager.addStake(50, owner); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); @@ -2809,7 +3074,14 @@ describe('Ticketing', () => { // set up the node's stake and registry await stakingManager.addStake(toSOLOs(1), node); - await setSeekerRegistry(seekers, registries, accounts[0], accounts[1], 1); + await setSeekerRegistry( + seekers, + registries, + seekerPowerOracle, + accounts[0], + accounts[1], + 1, + ); await epochsManager.joinNextEpoch(); await epochsManager.initializeEpoch(); diff --git a/test/payments/utils.ts b/test/payments/utils.ts index a3621a2b..7b9b3fdf 100644 --- a/test/payments/utils.ts +++ b/test/payments/utils.ts @@ -9,6 +9,7 @@ import { SyloTicketing, SyloToken, TestSeekers, + SeekerPowerOracle, } from '../../typechain-types'; import * as contractTypes from '../../typechain-types'; import web3 from 'web3'; @@ -119,6 +120,7 @@ export const checkAfterRedeem = async ( export async function setSeekerRegistry( seekers: TestSeekers, registries: Registries, + seekerPowerOracle: SeekerPowerOracle, account: Signer, seekerAccount: Signer, tokenId: number, @@ -126,6 +128,7 @@ export async function setSeekerRegistry( await utils.setSeekerRegistry( registries, seekers, + seekerPowerOracle, account, seekerAccount, tokenId, diff --git a/test/registries.test.ts b/test/registries.test.ts index 114e32de..6171254c 100644 --- a/test/registries.test.ts +++ b/test/registries.test.ts @@ -1,6 +1,6 @@ import { ethers } from 'hardhat'; import { Signer } from 'ethers'; -import { Registries, TestSeekers } from '../typechain-types'; +import { Registries, SeekerPowerOracle, TestSeekers } from '../typechain-types'; import { assert, expect } from 'chai'; import utils from './utils'; import { randomBytes } from 'crypto'; @@ -11,6 +11,7 @@ describe('Registries', () => { let registries: Registries; let seekers: TestSeekers; + let seekerPowerOracle: SeekerPowerOracle; before(async () => { accounts = await ethers.getSigners(); @@ -27,6 +28,7 @@ describe('Registries', () => { }); registries = contracts.registries; seekers = contracts.seekers; + seekerPowerOracle = contracts.seekerPowerOracle; }); it('registries cannot initialize twice', async () => { @@ -192,6 +194,7 @@ describe('Registries', () => { await utils.setSeekerRegistry( registries, seekers, + seekerPowerOracle, accounts[0], accounts[1], 1, @@ -295,6 +298,7 @@ describe('Registries', () => { await utils.setSeekerRegistry( registries, seekers, + seekerPowerOracle, accounts[0], accounts[1], 1, @@ -311,6 +315,7 @@ describe('Registries', () => { await utils.setSeekerRegistry( registries, seekers, + seekerPowerOracle, accounts[0], accounts[1], 1, @@ -342,6 +347,7 @@ describe('Registries', () => { await utils.setSeekerRegistry( registries, seekers, + seekerPowerOracle, accountOne, seekerAccount, tokenID, @@ -350,6 +356,7 @@ describe('Registries', () => { await utils.setSeekerRegistry( registries, seekers, + seekerPowerOracle, accountTwo, seekerAccount, tokenID, diff --git a/test/staking.test.ts b/test/staking.test.ts index 85c3b7cb..b0ca7a33 100644 --- a/test/staking.test.ts +++ b/test/staking.test.ts @@ -1,10 +1,11 @@ import { ethers } from 'hardhat'; -import { Signer } from 'ethers'; +import { MaxUint256, Signer } from 'ethers'; import { Directory, EpochsManager, Registries, RewardsManager, + SeekerPowerOracle, StakingManager, SyloToken, TestSeekers, @@ -28,9 +29,12 @@ describe('Staking', () => { let stakingManager: StakingManager; let registries: Registries; let seekers: TestSeekers; + let seekerPowerOracle: SeekerPowerOracle; const epochId = 1; + const defaultSeekerId = 1; + before(async () => { accounts = await ethers.getSigners(); // first account is implicitly used as deployer of contracts in hardhat @@ -48,8 +52,15 @@ describe('Staking', () => { stakingManager = contracts.stakingManager; registries = contracts.registries; seekers = contracts.seekers; + seekerPowerOracle = contracts.seekerPowerOracle; await token.approve(await stakingManager.getAddress(), 100000); + + // set the seeker power to max for all tests by default + await seekerPowerOracle.registerSeekerPowerRestricted( + defaultSeekerId, + MaxUint256, + ); }); it('staking manager cannot be intialized twice', async () => { @@ -59,6 +70,7 @@ describe('Staking', () => { ethers.ZeroAddress, ethers.ZeroAddress, ethers.ZeroAddress, + ethers.ZeroAddress, 0, ), ).to.be.revertedWith('Initializable: contract is already initialized'); @@ -74,6 +86,7 @@ describe('Staking', () => { ethers.ZeroAddress, ethers.ZeroAddress, ethers.ZeroAddress, + ethers.ZeroAddress, 0, ), ).to.be.revertedWithCustomError(stakingManager, 'TokenCannotBeZeroAddress'); @@ -82,6 +95,7 @@ describe('Staking', () => { stakingManager.initialize( await token.getAddress(), await epochsManager.getAddress(), // correct contract is rewards manager + await seekerPowerOracle.getAddress(), ethers.ZeroAddress, ethers.ZeroAddress, 0, @@ -98,6 +112,7 @@ describe('Staking', () => { await token.getAddress(), await rewardsManager.getAddress(), await epochsManager.getAddress(), + await seekerPowerOracle.getAddress(), 0, 0, ), @@ -495,14 +510,14 @@ describe('Staking', () => { it('should not allow directory to be joined with no stake', async () => { await directory.addManager(owner); await expect( - directory.joinNextDirectory(owner), + directory.joinNextDirectory(owner, defaultSeekerId), ).to.be.revertedWithCustomError(directory, 'NoStakeToJoinEpoch'); }); it('cannot join directory with invalid arguments', async () => { await directory.addManager(owner); await expect( - directory.joinNextDirectory(ethers.ZeroAddress), + directory.joinNextDirectory(ethers.ZeroAddress, 1), ).to.be.revertedWithCustomError(directory, 'StakeeCannotBeZeroAddress'); }); @@ -513,7 +528,7 @@ describe('Staking', () => { await directory.addManager(owner); await expect( - directory.joinNextDirectory(owner), + directory.joinNextDirectory(owner, defaultSeekerId), ).to.be.revertedWithCustomError(directory, 'NoStakeToJoinEpoch'); }); @@ -539,7 +554,7 @@ describe('Staking', () => { await stakingManager.unlockStake(80, owner); await directory.addManager(owner); - await directory.joinNextDirectory(owner); + await directory.joinNextDirectory(owner, defaultSeekerId); // the node now only owns 10% of the stake, which 50% of the // minimum stake proportion @@ -566,7 +581,7 @@ describe('Staking', () => { await directory.addManager(owner); await expect( - directory.joinNextDirectory(owner), + directory.joinNextDirectory(owner, defaultSeekerId), ).to.be.revertedWithCustomError(directory, 'NoJoiningStakeToJoinEpoch'); }); @@ -655,10 +670,10 @@ describe('Staking', () => { it('should not be able to join the next epoch more than once', async () => { await stakingManager.addStake(1, owner); await directory.addManager(owner); - await directory.joinNextDirectory(owner); + await directory.joinNextDirectory(owner, defaultSeekerId); await expect( - directory.joinNextDirectory(owner), + directory.joinNextDirectory(owner, defaultSeekerId), ).to.be.revertedWithCustomError(directory, 'StakeeAlreadyJoinedEpoch'); }); @@ -887,7 +902,7 @@ describe('Staking', () => { it('can not call functions that onlyManager constraint', async () => { await expect( - directory.joinNextDirectory(owner), + directory.joinNextDirectory(owner, defaultSeekerId), ).to.be.revertedWithCustomError(directory, 'OnlyManagers'); }); @@ -994,6 +1009,135 @@ describe('Staking', () => { ); }); + it('can validate contract interface', async () => { + const TestSyloUtils = await ethers.getContractFactory('TestSyloUtils'); + const testSyloUtils = await TestSyloUtils.deploy(); + + await expect( + testSyloUtils.validateContractInterface( + '', + await rewardsManager.getAddress(), + '0x3db12b5a', + ), + ).to.be.revertedWithCustomError(testSyloUtils, 'ContractNameCannotBeEmpty'); + + await expect( + testSyloUtils.validateContractInterface( + 'RewardsManager', + ethers.ZeroAddress, + '0x3db12b5a', + ), + ).to.be.revertedWithCustomError( + testSyloUtils, + 'TargetContractCannotBeZeroAddress', + ); + + await expect( + testSyloUtils.validateContractInterface( + 'RewardsManager', + await rewardsManager.getAddress(), + '0x00000000', + ), + ).to.be.revertedWithCustomError( + testSyloUtils, + 'InterfaceIdCannotBeZeroBytes', + ); + + await expect( + testSyloUtils.validateContractInterface( + 'RewardsManager', + await rewardsManager.getAddress(), + '0x11111111', + ), + ).to.be.revertedWithCustomError(testSyloUtils, 'TargetNotSupportInterface'); + }); + + it('reverts if seeker power has not been registered', async () => { + await expect( + stakingManager.calculateCapacityFromSeekerPower(111), + ).to.revertedWithCustomError(stakingManager, 'SeekerPowerNotRegistered'); + }); + + it('correctly calculates seeker staking capacity from power', async () => { + const seekerPowers = [ + { seekerId: 10, power: 100, expectedSyloCapacity: 10000 }, + { seekerId: 11, power: 222, expectedSyloCapacity: 49284 }, + { seekerId: 12, power: 432, expectedSyloCapacity: 186624 }, + { seekerId: 13, power: 1000, expectedSyloCapacity: 1000000 }, + { seekerId: 14, power: 2000, expectedSyloCapacity: 4000000 }, + { seekerId: 15, power: 100, expectedSyloCapacity: 10000 }, + ]; + + for (const sp of seekerPowers) { + await seekerPowerOracle.registerSeekerPowerRestricted( + sp.seekerId, + sp.power, + ); + + const capacity = await stakingManager.calculateCapacityFromSeekerPower( + sp.seekerId, + ); + + const expectedSylo = ethers.parseEther( + sp.expectedSyloCapacity.toString(), + ); + + expect(capacity).to.equal(expectedSylo); + } + }); + + it('returns maximum SYLO amount if seeker power is very large', async () => { + const maxSylo = ethers.parseEther('10000000000'); + + // 1 more than the maximum sylo + await seekerPowerOracle.registerSeekerPowerRestricted(111, maxSylo + 1n); + + const capacityOne = await stakingManager.calculateCapacityFromSeekerPower( + 111, + ); + + expect(capacityOne).to.equal(maxSylo); + + // seeker_power ** 2 > maximum_sylo + await seekerPowerOracle.registerSeekerPowerRestricted( + 222, + Math.sqrt(parseInt(maxSylo.toString())) + 1, + ); + + const capacityTwo = await stakingManager.calculateCapacityFromSeekerPower( + 222, + ); + + expect(capacityTwo).to.equal(maxSylo); + }); + + it('reverts when joining directory without seeker power registered', async () => { + await stakingManager.addStake(100, owner); + + await directory.addManager(owner); + await expect( + directory.joinNextDirectory(owner, 111), // unregistered seeker + ).to.be.revertedWithCustomError(stakingManager, 'SeekerPowerNotRegistered'); + }); + + it('joins directory with stake where maximum is dependent on seeker power', async () => { + const stakeToAdd = ethers.parseEther('1000000'); + + await token.approve(stakingManager.getAddress(), stakeToAdd); + await stakingManager.addStake(stakeToAdd, owner); + + // the added stake is 1,000,000 SYLO, but the seeker power capacity + // is 490,000 SYLO. + await seekerPowerOracle.registerSeekerPowerRestricted(111, 700); + + await directory.addManager(owner); + await directory.joinNextDirectory(owner, 111); + + const joinedStake = await directory.getTotalStakeForStakee(1, owner); + + expect(joinedStake).to.equal(ethers.parseEther('490000')); + }); + async function setSeekeRegistry( account: Signer, seekerAccount: Signer, @@ -1002,6 +1146,7 @@ describe('Staking', () => { await utils.setSeekerRegistry( registries, seekers, + seekerPowerOracle, account, seekerAccount, tokenId, @@ -1022,6 +1167,7 @@ describe('Staking', () => { .addStake(amount, await account.getAddress()); await setSeekeRegistry(account, accounts[9], seekerId); await epochsManager.connect(account).joinNextEpoch(); + await seekerPowerOracle.registerSeekerPowerRestricted(seekerId, MaxUint256); } async function testScanResults(iterations: number, expectedResults: Results) { @@ -1098,47 +1244,4 @@ describe('Staking', () => { return points; } - - it('can validate contract interface', async () => { - const TestSyloUtils = await ethers.getContractFactory('TestSyloUtils'); - const testSyloUtils = await TestSyloUtils.deploy(); - - await expect( - testSyloUtils.validateContractInterface( - '', - await rewardsManager.getAddress(), - '0x3db12b5a', - ), - ).to.be.revertedWithCustomError(testSyloUtils, 'ContractNameCannotBeEmpty'); - - await expect( - testSyloUtils.validateContractInterface( - 'RewardsManager', - ethers.ZeroAddress, - '0x3db12b5a', - ), - ).to.be.revertedWithCustomError( - testSyloUtils, - 'TargetContractCannotBeZeroAddress', - ); - - await expect( - testSyloUtils.validateContractInterface( - 'RewardsManager', - await rewardsManager.getAddress(), - '0x00000000', - ), - ).to.be.revertedWithCustomError( - testSyloUtils, - 'InterfaceIdCannotBeZeroBytes', - ); - - await expect( - testSyloUtils.validateContractInterface( - 'RewardsManager', - await rewardsManager.getAddress(), - '0x11111111', - ), - ).to.be.revertedWithCustomError(testSyloUtils, 'TargetNotSupportInterface'); - }); }); diff --git a/test/utils.ts b/test/utils.ts index 08d83f7b..e78ff13f 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,7 +1,12 @@ import { ethers } from 'hardhat'; -import { BigNumberish, Signer } from 'ethers'; +import { BigNumberish, MaxUint256, Signer } from 'ethers'; import { toWei } from 'web3-utils'; -import { Registries, SyloToken, TestSeekers } from '../typechain-types'; +import { + Registries, + SeekerPowerOracle, + SyloToken, + TestSeekers, +} from '../typechain-types'; import { randomBytes } from 'crypto'; import { SyloContracts } from '../common/contracts'; @@ -99,6 +104,7 @@ const initializeContracts = async function ( tokenAddress, await rewardsManager.getAddress(), await epochsManager.getAddress(), + await seekerPowerOracle.getAddress(), unlockDuration, minimumStakeProportion, { from: deployer }, @@ -183,6 +189,7 @@ const advanceBlock = async function (i: number): Promise { async function setSeekerRegistry( registries: Registries, seekers: TestSeekers, + seekerPowerOracle: SeekerPowerOracle, account: Signer, seekerAccount: Signer, tokenId: number, @@ -214,6 +221,9 @@ async function setSeekerRegistry( nonce, signature, ); + + // set the seeker power default to the max value + await seekerPowerOracle.registerSeekerPowerRestricted(tokenId, MaxUint256); } export default { From 76baca51225d97a005529e3a6424f58d29c468d2 Mon Sep 17 00:00:00 2001 From: John Carlo San Pedro Date: Wed, 17 Jan 2024 22:14:19 +1300 Subject: [PATCH 03/10] update deployment script to deploy SeekerPowerOracle --- common/contracts.ts | 8 +++ deploy/001_deploy_contracts.ts | 12 +++- deployments/ganache_deployment_phase_two.json | 27 ++++---- deployments/genesis.config.ts | 61 ++++++------------- .../ganache-with-protocol/deploy_contracts.sh | 4 +- ...{init_network.ts => init_local_network.ts} | 0 scripts/utils.ts | 6 +- 7 files changed, 58 insertions(+), 60 deletions(-) rename scripts/{init_network.ts => init_local_network.ts} (100%) diff --git a/common/contracts.ts b/common/contracts.ts index 52130220..8b5710bd 100644 --- a/common/contracts.ts +++ b/common/contracts.ts @@ -15,6 +15,7 @@ export const DeployedContractNames = { rewardsManager: 'RewardsManager', directory: 'Directory', syloTicketing: 'SyloTicketing', + seekerPowerOracle: 'SeekerPowerOracle', }; export const ContractNames = { @@ -48,6 +49,7 @@ export type ContractAddresses = { directory: string; syloTicketing: string; seekers: string; + seekerPowerOracle: string; futurepassRegistrar: string; }; @@ -105,6 +107,11 @@ export function connectContracts( provider, ); + const seekerPowerOracle = factories.SeekerPowerOracle__factory.connect( + contracts.seekerPowerOracle, + provider, + ); + const futurepassRegistrar = factories.TestFuturepassRegistrar__factory.connect( contracts.futurepassRegistrar, @@ -122,6 +129,7 @@ export function connectContracts( directory, syloTicketing, seekers, + seekerPowerOracle, futurepassRegistrar, }; } diff --git a/deploy/001_deploy_contracts.ts b/deploy/001_deploy_contracts.ts index bf7c94ef..fa3a57cf 100644 --- a/deploy/001_deploy_contracts.ts +++ b/deploy/001_deploy_contracts.ts @@ -30,6 +30,10 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const contracts: ContractMap = {}; + console.log( + `Deploying Sylo Protocol Contracts with deployer: ${deployer.address}...`, + ); + // DEPLOY CONTRACTS if (config.SyloToken == '') { config.SyloToken = ( @@ -97,6 +101,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { config.SyloToken, contracts[ContractNames.rewardsManager].address, contracts[ContractNames.epochsManager].address, + contracts[ContractNames.seekerPowerOracle].address, config.StakingManager.unlockDuration, config.StakingManager.minimumStakeProportion, ], @@ -130,6 +135,10 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { config.Ticketing.unlockDuration, ], }, + { + name: ContractNames.seekerPowerOracle, + args: [config.SeekerPowerOracle.oracleAccount], + }, ]; for (const { name, args } of initializeParams) { await initializeContract(name, args, deployer.address, execute); @@ -167,8 +176,6 @@ function getConfig(networkName: string): configs.ContractParameters { switch (networkName) { case 'porcini-dev': return configs.PorciniDevParameters; - case 'porcini-testing': - return configs.PorciniTestingParameters; case 'locahost': return configs.GanacheTestnetParameters; default: @@ -264,6 +271,7 @@ async function saveContracts( directory: contracts[ContractNames.directory].address, syloTicketing: contracts[ContractNames.syloTicketing].address, seekers: config.Seekers, + seekerPowerOracle: contracts[ContractNames.seekerPowerOracle].address, futurepassRegistrar: config.FuturepassRegistrar, }; diff --git a/deployments/ganache_deployment_phase_two.json b/deployments/ganache_deployment_phase_two.json index 74535761..275caf59 100644 --- a/deployments/ganache_deployment_phase_two.json +++ b/deployments/ganache_deployment_phase_two.json @@ -1,14 +1,15 @@ { - "deployer": "0x835dF5fE77D479695a616F79A3FC3a25310eb7c6", - "syloToken": "0xc4db8fD3209c98290AB32693F5155c596B97Eabe", - "authorizedAccounts": "0x075EEeD1215982b78A2e05cD2213b5f53A718a9A", - "registries": "0xFB87c433852Bb2917B37b0471DFA5B369e75083A", - "ticketingParameters": "0x65A9be6e97eD2F417250A819e93BCb359b1140d0", - "epochsManager": "0x7bFCE7796fdE3Ba0F2052959d506bdA480518edA", - "stakingManager": "0xa4dE4FEA5e961e5C130013CfE207f7C08148A73C", - "rewardsManager": "0x4aa109E0DB0223ed2d9085679F450EfDf188CFf6", - "directory": "0x7E7C762176eaa1662d372399265760d4600CCf28", - "syloTicketing": "0x943E7031A7Ed0FC236173f05a3084104b81Aa480", - "seekers": "0x49C537a88016186Ef41713239799Fc975F9e9aFA", - "futurepassRegistrar": "0x7DBf77bb534997892e5Bcfbdc79Dd82E71C35245" -} + "deployer": "0x835dF5fE77D479695a616F79A3FC3a25310eb7c6", + "syloToken": "0xc4db8fD3209c98290AB32693F5155c596B97Eabe", + "authorizedAccounts": "0x075EEeD1215982b78A2e05cD2213b5f53A718a9A", + "registries": "0xFB87c433852Bb2917B37b0471DFA5B369e75083A", + "ticketingParameters": "0x65A9be6e97eD2F417250A819e93BCb359b1140d0", + "epochsManager": "0x7bFCE7796fdE3Ba0F2052959d506bdA480518edA", + "stakingManager": "0xa4dE4FEA5e961e5C130013CfE207f7C08148A73C", + "rewardsManager": "0x4aa109E0DB0223ed2d9085679F450EfDf188CFf6", + "directory": "0x7E7C762176eaa1662d372399265760d4600CCf28", + "syloTicketing": "0x943E7031A7Ed0FC236173f05a3084104b81Aa480", + "seekers": "0x49C537a88016186Ef41713239799Fc975F9e9aFA", + "seekerPowerOracle": "0xca7efb9aA54e70F7a8e7efb878A48BaefA34F4AC", + "futurepassRegistrar": "0x7DBf77bb534997892e5Bcfbdc79Dd82E71C35245" +} \ No newline at end of file diff --git a/deployments/genesis.config.ts b/deployments/genesis.config.ts index 3a09efe1..57c38c4c 100644 --- a/deployments/genesis.config.ts +++ b/deployments/genesis.config.ts @@ -36,6 +36,10 @@ type ContractParameters = { unlockDuration: BigNumberish; minimumStakeProportion: number; }; + + SeekerPowerOracle: { + oracleAccount: string; + }; }; const GenesisParameters: ContractParameters = { @@ -80,6 +84,10 @@ const GenesisParameters: ContractParameters = { unlockDuration: 8000, minimumStakeProportion: 3000, }, + + SeekerPowerOracle: { + oracleAccount: '0x835dF5fE77D479695a616F79A3FC3a25310eb7c6', + }, }; const GanacheTestnetParameters: ContractParameters = { @@ -114,6 +122,10 @@ const GanacheTestnetParameters: ContractParameters = { unlockDuration: 30, // 30 * 4 = 120 seconds = 2 minutes minimumStakeProportion: 3000, }, + + SeekerPowerOracle: { + oracleAccount: '', + }, }; const PorciniDevParameters: ContractParameters = { @@ -125,7 +137,7 @@ const PorciniDevParameters: ContractParameters = { EpochsManager: { initialEpoch: 0, - epochDuration: 30, + epochDuration: 17280, // 1 day }, Registries: { @@ -133,10 +145,10 @@ const PorciniDevParameters: ContractParameters = { }, TicketingParameters: { - faceValue: hre.ethers.parseEther('10000'), - baseLiveWinProb: 2n ** 128n - 1n, + faceValue: hre.ethers.parseEther('1000'), + baseLiveWinProb: 2n ** 128n - 1n / 10n, expiredWinProb: 2n ** 128n - 1n, - ticketDuration: 100, + ticketDuration: 17280, decayRate: 8000, }, @@ -148,47 +160,12 @@ const PorciniDevParameters: ContractParameters = { unlockDuration: 10, minimumStakeProportion: 3000, }, -}; - -const PorciniTestingParameters: ContractParameters = { - SyloToken: '0xCCcCCcCC00000C64000000000000000000000000', - - Seekers: '0xAAAAAAAA00001864000000000000000000000000', - - FuturepassRegistrar: '0x000000000000000000000000000000000000FFFF', - - EpochsManager: { - initialEpoch: 0, - epochDuration: 17280, // 1 day - }, - Registries: { - defaultPayoutPercentage: 5000, - }, - - TicketingParameters: { - faceValue: hre.ethers.parseEther('10000'), - baseLiveWinProb: 2n ** 128n - 1n, - expiredWinProb: 2n ** 128n - 1n, - ticketDuration: 100, - decayRate: 8000, - }, - - Ticketing: { - unlockDuration: 120, - }, - - StakingManager: { - unlockDuration: 120, - minimumStakeProportion: 3000, + SeekerPowerOracle: { + oracleAccount: '0x448c8e9e1816300Dd052e77D2A44c990A2807D15', }, }; -export { - GenesisParameters, - GanacheTestnetParameters, - PorciniDevParameters, - PorciniTestingParameters, -}; +export { GenesisParameters, GanacheTestnetParameters, PorciniDevParameters }; export type { ContractParameters }; diff --git a/docker/ganache-with-protocol/deploy_contracts.sh b/docker/ganache-with-protocol/deploy_contracts.sh index 2e6b7f09..1f34a66b 100755 --- a/docker/ganache-with-protocol/deploy_contracts.sh +++ b/docker/ganache-with-protocol/deploy_contracts.sh @@ -13,14 +13,14 @@ ganache \ ganache_pid=`echo $!` echo "waiting for ganache to start up" -sleep 5 +sleep 15 echo "deploying contracts to local node" npx hardhat --network localhost deploy mv ./deployments/localhost_deployment_phase_two.json deployment/addresses.json echo "initialzing network" -npx hardhat --network localhost run scripts/init_network.ts +npx hardhat --network localhost run scripts/init_local_network.ts echo "shutting down ganache" kill $ganache_pid diff --git a/scripts/init_network.ts b/scripts/init_local_network.ts similarity index 100% rename from scripts/init_network.ts rename to scripts/init_local_network.ts diff --git a/scripts/utils.ts b/scripts/utils.ts index 69cf5044..4257db4a 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -1,4 +1,4 @@ -import { ethers } from 'ethers'; +import { MaxUint256, ethers } from 'ethers'; import * as Contracts from '../common/contracts'; import contractAddress from '../deployments/ganache_deployment_phase_two.json'; import { randomBytes } from 'crypto'; @@ -96,6 +96,10 @@ export async function setSeekerRegistry( signature, { gasLimit: 1_000_000 }, ); + + await contracts.seekerPowerOracle + .connect(seekerAccount) + .registerSeekerPowerRestricted(tokenId, MaxUint256); } export async function depositTicketing( From a385e7678214a5efaa5f9780e8bbb164fa006d07 Mon Sep 17 00:00:00 2001 From: John Carlo San Pedro Date: Fri, 16 Feb 2024 15:53:25 +1300 Subject: [PATCH 04/10] update capacity calculation to use simple multiplier --- contracts/libraries/SyloUtils.sol | 5 ++++ contracts/staking/Directory.sol | 5 ++++ contracts/staking/StakingManager.sol | 30 +++++++++++-------- test/staking.test.ts | 44 ++++++++++++++++------------ test/utils.ts | 5 ++++ 5 files changed, 58 insertions(+), 31 deletions(-) diff --git a/contracts/libraries/SyloUtils.sol b/contracts/libraries/SyloUtils.sol index 7e7dc058..ceeaab54 100644 --- a/contracts/libraries/SyloUtils.sol +++ b/contracts/libraries/SyloUtils.sol @@ -10,6 +10,11 @@ error TargetContractCannotBeZeroAddress(string name); error TargetNotSupportInterface(string name, bytes4 interfaceId); library SyloUtils { + /** + * @dev The maximum possible SYLO that exists in the network. + */ + uint256 public constant MAX_SYLO = 10_000_000_000 ether; + /** * @dev Percentages are expressed as a ratio where 10000 is the denominator. * A large denominator allows for more precision, e.g representing 12.5% diff --git a/contracts/staking/Directory.sol b/contracts/staking/Directory.sol index 0f0ea878..aaf26062 100644 --- a/contracts/staking/Directory.sol +++ b/contracts/staking/Directory.sol @@ -104,6 +104,11 @@ contract Directory is IDirectory, Initializable, Manageable, IERC165 { * * |-----------|------|----------------|--------| * Alice/20 Bob/30 Carl/70 Dave/95 + * + * The amount of stake will join a directory with is dependent on its + * capacity. We first consider the capacity based on the it's Seeker power. + * We then consider its capacity based on the minimum stake proportion + * factor. The final staking amount will not exceed either capacities. */ function joinNextDirectory(address stakee, uint256 seekerId) external onlyManager { if (stakee == address(0)) { diff --git a/contracts/staking/StakingManager.sol b/contracts/staking/StakingManager.sol index 5265b031..c988cac9 100644 --- a/contracts/staking/StakingManager.sol +++ b/contracts/staking/StakingManager.sol @@ -6,10 +6,10 @@ import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; import "../SyloToken.sol"; import "../libraries/SyloUtils.sol"; +import "../SeekerPowerOracle.sol"; import "../epochs/EpochsManager.sol"; import "../payments/ticketing/RewardsManager.sol"; import "../interfaces/staking/IStakingManager.sol"; -import "../interfaces/ISeekerPowerOracle.sol"; /** * @notice Manages stakes and delegated stakes for Nodes. Holding @@ -18,10 +18,6 @@ import "../interfaces/ISeekerPowerOracle.sol"; * and delegated stakers are rewarded on a pro-rata basis. */ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeable, ERC165 { - // The maximum possible SYLO that exists in the network. Naturally - // represents the maximum possible SYLO that can be staked. - uint256 internal constant MAX_SYLO = 10_000_000_000 ether; - /** ERC 20 compatible token we are dealing with */ IERC20 public _token; @@ -33,7 +29,7 @@ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeab EpochsManager public _epochsManager; - ISeekerPowerOracle public _seekerPowerOracle; + SeekerPowerOracle public _seekerPowerOracle; /** * @notice Tracks the managed stake for every Node. @@ -64,6 +60,12 @@ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeab */ uint16 public minimumStakeProportion; + /** + * @notice The multiplier used in determining a Seeker's staking + * capacity based on its power level. + */ + uint256 public seekerPowerMultiplier; + event UnlockDurationUpdated(uint256 unlockDuration); event MinimumStakeProportionUpdated(uint256 minimumStakeProportion); @@ -83,9 +85,10 @@ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeab IERC20 token, RewardsManager rewardsManager, EpochsManager epochsManager, - ISeekerPowerOracle seekerPowerOracle, + SeekerPowerOracle seekerPowerOracle, uint256 _unlockDuration, - uint16 _minimumStakeProportion + uint16 _minimumStakeProportion, + uint256 _seekerPowerMultiplier ) external initializer { if (address(token) == address(0)) { revert TokenCannotBeZeroAddress(); @@ -121,6 +124,7 @@ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeab _seekerPowerOracle = seekerPowerOracle; unlockDuration = _unlockDuration; minimumStakeProportion = _minimumStakeProportion; + seekerPowerMultiplier = _seekerPowerMultiplier; } /** @@ -323,7 +327,7 @@ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeab * the Seeker's power level has not been registered with the oracle. * * Currently the algorithm is as follows: - * staking_capacity = seeker_power^2 + * staking_capacity = seeker_power * seeker_power_multiplier; */ function calculateCapacityFromSeekerPower(uint256 seekerId) external view returns (uint256) { uint256 seekerPower = _seekerPowerOracle.getSeekerPower(seekerId); @@ -333,13 +337,13 @@ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeab // If the Seeker Power is already // at the maximum sylo, then we just return the max sylo value directly. - if (seekerPower >= MAX_SYLO) { - return MAX_SYLO; + if (seekerPower >= SyloUtils.MAX_SYLO) { + return SyloUtils.MAX_SYLO; } - uint256 capacity = seekerPower ** 2 * 1 ether; + uint256 capacity = seekerPower * seekerPowerMultiplier; - return capacity > MAX_SYLO ? MAX_SYLO : capacity; + return capacity > SyloUtils.MAX_SYLO ? SyloUtils.MAX_SYLO : capacity; } /** diff --git a/test/staking.test.ts b/test/staking.test.ts index b0ca7a33..eba0b042 100644 --- a/test/staking.test.ts +++ b/test/staking.test.ts @@ -18,6 +18,8 @@ import crypto from 'crypto'; type Results = { [key: string]: number }; +const MAX_SYLO_STAKE = ethers.parseEther('10000000000'); + describe('Staking', () => { let accounts: Signer[]; let owner: string; @@ -72,6 +74,7 @@ describe('Staking', () => { ethers.ZeroAddress, ethers.ZeroAddress, 0, + 0, ), ).to.be.revertedWith('Initializable: contract is already initialized'); }); @@ -88,6 +91,7 @@ describe('Staking', () => { ethers.ZeroAddress, ethers.ZeroAddress, 0, + 0, ), ).to.be.revertedWithCustomError(stakingManager, 'TokenCannotBeZeroAddress'); @@ -99,6 +103,7 @@ describe('Staking', () => { ethers.ZeroAddress, ethers.ZeroAddress, 0, + 0, ), ) .to.be.revertedWithCustomError( @@ -115,6 +120,7 @@ describe('Staking', () => { await seekerPowerOracle.getAddress(), 0, 0, + 0, ), ).to.be.revertedWithCustomError( stakingManager, @@ -1059,13 +1065,14 @@ describe('Staking', () => { }); it('correctly calculates seeker staking capacity from power', async () => { + // default multiplier is 1M const seekerPowers = [ - { seekerId: 10, power: 100, expectedSyloCapacity: 10000 }, - { seekerId: 11, power: 222, expectedSyloCapacity: 49284 }, - { seekerId: 12, power: 432, expectedSyloCapacity: 186624 }, - { seekerId: 13, power: 1000, expectedSyloCapacity: 1000000 }, - { seekerId: 14, power: 2000, expectedSyloCapacity: 4000000 }, - { seekerId: 15, power: 100, expectedSyloCapacity: 10000 }, + { seekerId: 10, power: 100, expectedSyloCapacity: 100000000 }, + { seekerId: 11, power: 222, expectedSyloCapacity: 222000000 }, + { seekerId: 12, power: 432, expectedSyloCapacity: 432000000 }, + { seekerId: 13, power: 3, expectedSyloCapacity: 3000000 }, + { seekerId: 14, power: 4, expectedSyloCapacity: 4000000 }, + { seekerId: 15, power: 8, expectedSyloCapacity: 8000000 }, ]; for (const sp of seekerPowers) { @@ -1087,28 +1094,29 @@ describe('Staking', () => { }); it('returns maximum SYLO amount if seeker power is very large', async () => { - const maxSylo = ethers.parseEther('10000000000'); - // 1 more than the maximum sylo - await seekerPowerOracle.registerSeekerPowerRestricted(111, maxSylo + 1n); + await seekerPowerOracle.registerSeekerPowerRestricted( + 111, + MAX_SYLO_STAKE + 1n, + ); const capacityOne = await stakingManager.calculateCapacityFromSeekerPower( 111, ); - expect(capacityOne).to.equal(maxSylo); + expect(capacityOne).to.equal(MAX_SYLO_STAKE); - // seeker_power ** 2 > maximum_sylo + // seeker_power * multiplier > maximum_sylo await seekerPowerOracle.registerSeekerPowerRestricted( 222, - Math.sqrt(parseInt(maxSylo.toString())) + 1, + MAX_SYLO_STAKE / 2n, ); const capacityTwo = await stakingManager.calculateCapacityFromSeekerPower( 222, ); - expect(capacityTwo).to.equal(maxSylo); + expect(capacityTwo).to.equal(MAX_SYLO_STAKE); }); it('reverts when joining directory without seeker power registered', async () => { @@ -1121,21 +1129,21 @@ describe('Staking', () => { }); it('joins directory with stake where maximum is dependent on seeker power', async () => { - const stakeToAdd = ethers.parseEther('1000000'); + const stakeToAdd = ethers.parseEther('10000000'); await token.approve(stakingManager.getAddress(), stakeToAdd); await stakingManager.addStake(stakeToAdd, owner); - // the added stake is 1,000,000 SYLO, but the seeker power capacity - // is 490,000 SYLO. - await seekerPowerOracle.registerSeekerPowerRestricted(111, 700); + // the added stake is 10,000,000 SYLO, but the seeker power capacity + // is 4,000,000 + await seekerPowerOracle.registerSeekerPowerRestricted(111, 4); await directory.addManager(owner); await directory.joinNextDirectory(owner, 111); const joinedStake = await directory.getTotalStakeForStakee(1, owner); - expect(joinedStake).to.equal(ethers.parseEther('490000')); + expect(joinedStake).to.equal(ethers.parseEther('4000000')); }); async function setSeekeRegistry( diff --git a/test/utils.ts b/test/utils.ts index e78ff13f..941497fa 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -20,6 +20,7 @@ type Options = { initialEpoch?: number; epochDuration?: number; minimumStakeProportion?: number; + seekerPowerMultiplier?: BigNumberish; unlockDuration?: number; seekerPowerOracleAccount?: string; }; @@ -43,6 +44,9 @@ const initializeContracts = async function ( const minimumStakeProportion = opts.minimumStakeProportion ?? 2000; + const seekerPowerMultiplier = + opts.seekerPowerMultiplier ?? ethers.parseEther('1000000'); + const seekerPowerOracleAccount = opts.seekerPowerOracleAccount ?? deployer; const tokenAddress = await syloToken.getAddress(); @@ -107,6 +111,7 @@ const initializeContracts = async function ( await seekerPowerOracle.getAddress(), unlockDuration, minimumStakeProportion, + seekerPowerMultiplier, { from: deployer }, ); await rewardsManager.initialize( From d37e56476f48ceb4366f408a4c245097ba2edb22 Mon Sep 17 00:00:00 2001 From: John Carlo San Pedro Date: Fri, 16 Feb 2024 16:23:44 +1300 Subject: [PATCH 05/10] simplify directory stake capacity algorithm --- contracts/staking/Directory.sol | 42 +++++++++++++--------------- contracts/staking/StakingManager.sol | 23 +++++++++++---- test/staking.test.ts | 31 ++++++++++++++++++++ 3 files changed, 67 insertions(+), 29 deletions(-) diff --git a/contracts/staking/Directory.sol b/contracts/staking/Directory.sol index aaf26062..422fd230 100644 --- a/contracts/staking/Directory.sol +++ b/contracts/staking/Directory.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.18; +import "@openzeppelin/contracts/utils/math/Math.sol"; + import "./StakingManager.sol"; import "../libraries/SyloUtils.sol"; import "../libraries/Manageable.sol"; @@ -105,10 +107,12 @@ contract Directory is IDirectory, Initializable, Manageable, IERC165 { * |-----------|------|----------------|--------| * Alice/20 Bob/30 Carl/70 Dave/95 * - * The amount of stake will join a directory with is dependent on its - * capacity. We first consider the capacity based on the it's Seeker power. - * We then consider its capacity based on the minimum stake proportion - * factor. The final staking amount will not exceed either capacities. + * The amount of stake that a node will join a directory with is dependent on its + * different capacity values. There are two distinct capacity values, one + * calculated from the seeker power, and another from the minimum stake + * proportion. The final staking amount will not exceed either capacities, + * and in the case that the current total stake exceeds both, then the final + * will be the minimum of the two values. */ function joinNextDirectory(address stakee, uint256 seekerId) external onlyManager { if (stakee == address(0)) { @@ -122,31 +126,23 @@ contract Directory is IDirectory, Initializable, Manageable, IERC165 { revert NoStakeToJoinEpoch(); } + // staking capacity based on seeker power uint256 seekerStakingCapacity = _stakingManager.calculateCapacityFromSeekerPower(seekerId); - // we take the minimum value between the total stake and the current - // staking capacity - if (totalStake > seekerStakingCapacity) { - totalStake = seekerStakingCapacity; - } - - uint256 currentStake = _stakingManager.getCurrentStakerAmount(stakee, stakee); - - uint16 ownedStakeProportion = SyloUtils.asPerc( - SafeCast.toUint128(currentStake), - totalStake - ); - - uint16 minimumStakeProportion = _stakingManager.minimumStakeProportion(); + // staking capacity based on the min staking proportion constant + uint256 minProportionStakingCapacity = _stakingManager.calculateCapacityFromMinStakingProportion(stakee); uint256 joiningStake; - if (ownedStakeProportion >= minimumStakeProportion) { + if (totalStake > seekerStakingCapacity && totalStake > minProportionStakingCapacity) { + joiningStake = Math.min(seekerStakingCapacity, minProportionStakingCapacity); + } else if (totalStake > seekerStakingCapacity) { + joiningStake = seekerStakingCapacity; + } else if (totalStake > minProportionStakingCapacity) { + joiningStake = minProportionStakingCapacity; + } else { // uncapped joiningStake = totalStake; - } else { - // if the node is below the minimum stake proportion, then we reduce - // the stake used to join the epoch proportionally - joiningStake = (totalStake * ownedStakeProportion) / minimumStakeProportion; } + if (joiningStake == 0) { revert NoJoiningStakeToJoinEpoch(); } diff --git a/contracts/staking/StakingManager.sol b/contracts/staking/StakingManager.sol index c988cac9..a14556db 100644 --- a/contracts/staking/StakingManager.sol +++ b/contracts/staking/StakingManager.sol @@ -347,13 +347,11 @@ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeab } /** - * @notice This function should be called by clients to determine how much - * additional delegated stake can be allocated to a Node via an addStake or - * cancelUnlocking call. This is useful to avoid a revert due to - * the minimum stake proportion requirement not being met from the additional stake. + * @notice This function can be used to a determine a Node's staking capacity, + * based on the minimum stake proportion constant. * @param stakee The address of the staked Node. */ - function calculateMaxAdditionalDelegatedStake(address stakee) external view returns (uint256) { + function calculateCapacityFromMinStakingProportion(address stakee) public view returns (uint256) { if (stakee == address(0)) { revert StakeeCannotBeZeroAddress(); } @@ -361,8 +359,21 @@ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeab Stake storage stake = stakes[stakee]; uint256 currentlyOwnedStake = stake.stakeEntries[stakee].amount; - uint256 totalMaxStake = (currentlyOwnedStake * SyloUtils.PERCENTAGE_DENOMINATOR) / + return (currentlyOwnedStake * SyloUtils.PERCENTAGE_DENOMINATOR) / minimumStakeProportion; + } + + /** + * @notice This function should be called by clients to determine how much + * additional delegated stake can be allocated to a Node via an addStake or + * cancelUnlocking call. This is useful to avoid a revert due to + * the minimum stake proportion requirement not being met from the additional stake. + * @param stakee The address of the staked Node. + */ + function calculateMaxAdditionalDelegatedStake(address stakee) external view returns (uint256) { + uint256 totalMaxStake = calculateCapacityFromMinStakingProportion(stakee); + + Stake storage stake = stakes[stakee]; if (totalMaxStake < stake.totalManagedStake) { revert StakeCapacityReached(totalMaxStake, stake.totalManagedStake); diff --git a/test/staking.test.ts b/test/staking.test.ts index eba0b042..b0dd7ad4 100644 --- a/test/staking.test.ts +++ b/test/staking.test.ts @@ -1146,6 +1146,37 @@ describe('Staking', () => { expect(joinedStake).to.equal(ethers.parseEther('4000000')); }); + it('joins directory with stake where maximum neither exceeds seeker power capacity or proportion capacity', async () => { + const stakeToAdd = ethers.parseEther('100000'); + + await token.approve(stakingManager.getAddress(), stakeToAdd); + await stakingManager.addStake(stakeToAdd, owner); + + await seekerPowerOracle.registerSeekerPowerRestricted(111, 4); + + // delegated stake added causes the minimum proportion to be exceeded + const delegatedStakeToAdd = ethers.parseEther('1000000'); + + await token.transfer(accounts[2], delegatedStakeToAdd); + await token + .connect(accounts[2]) + .approve(stakingManager.getAddress(), delegatedStakeToAdd); + await stakingManager + .connect(accounts[2]) + .addStake(delegatedStakeToAdd, owner); + + await directory.addManager(owner); + await directory.joinNextDirectory(owner, 111); + + const joinedStake = await directory.getTotalStakeForStakee(1, owner); + + // The seeker staking capacity is 1M, and the proportion capacity is 500k. + // In this case the total stake exceeds both, and the joined stake should be + // the lesser of the two capacities. + + expect(joinedStake).to.equal(ethers.parseEther('500000')); + }); + async function setSeekeRegistry( account: Signer, seekerAccount: Signer, From 85f1369900c4ec481ac17d34e80f0358a7d2e443 Mon Sep 17 00:00:00 2001 From: John Carlo San Pedro Date: Fri, 16 Feb 2024 16:30:10 +1300 Subject: [PATCH 06/10] update percentage denominator to 100000 --- contracts/interfaces/staking/IStakingManager.sol | 2 +- contracts/libraries/SyloUtils.sol | 12 ++++++------ contracts/mocks/TestSyloUtils.sol | 4 ++-- contracts/staking/StakingManager.sol | 8 ++++---- deployments/genesis.config.ts | 4 ++-- test/utils.ts | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/contracts/interfaces/staking/IStakingManager.sol b/contracts/interfaces/staking/IStakingManager.sol index 32791e1e..66424b59 100644 --- a/contracts/interfaces/staking/IStakingManager.sol +++ b/contracts/interfaces/staking/IStakingManager.sol @@ -39,7 +39,7 @@ interface IStakingManager { function setUnlockDuration(uint256 _unlockDuration) external; - function setMinimumStakeProportion(uint16 _minimumStakeProportion) external; + function setMinimumStakeProportion(uint32 _minimumStakeProportion) external; function addStake(uint256 amount, address stakee) external; diff --git a/contracts/libraries/SyloUtils.sol b/contracts/libraries/SyloUtils.sol index ceeaab54..c37a6ff4 100644 --- a/contracts/libraries/SyloUtils.sol +++ b/contracts/libraries/SyloUtils.sol @@ -16,11 +16,11 @@ library SyloUtils { uint256 public constant MAX_SYLO = 10_000_000_000 ether; /** - * @dev Percentages are expressed as a ratio where 10000 is the denominator. + * @dev Percentages are expressed as a ratio where 100000 is the denominator. * A large denominator allows for more precision, e.g representing 12.5% - * can be done as 1250 / 10000 + * can be done as 12500 / 100000 */ - uint16 public constant PERCENTAGE_DENOMINATOR = 10000; + uint32 public constant PERCENTAGE_DENOMINATOR = 100000; /** * @dev Multiply a value by a given percentage. Converts the provided @@ -28,7 +28,7 @@ library SyloUtils { * @param value The value to multiply. * @param percentage The percentage, as a ratio of 10000. */ - function percOf(uint128 value, uint16 percentage) internal pure returns (uint256) { + function percOf(uint128 value, uint32 percentage) internal pure returns (uint256) { return (uint256(value) * percentage) / PERCENTAGE_DENOMINATOR; } @@ -39,8 +39,8 @@ library SyloUtils { * @param denominator The denominator. * @return The percentage, as a ratio of 10000. */ - function asPerc(uint128 numerator, uint256 denominator) internal pure returns (uint16) { - return SafeCast.toUint16((uint256(numerator) * PERCENTAGE_DENOMINATOR) / denominator); + function asPerc(uint128 numerator, uint256 denominator) internal pure returns (uint32) { + return SafeCast.toUint32((uint256(numerator) * PERCENTAGE_DENOMINATOR) / denominator); } /** diff --git a/contracts/mocks/TestSyloUtils.sol b/contracts/mocks/TestSyloUtils.sol index 6a901bfe..72cd2c73 100644 --- a/contracts/mocks/TestSyloUtils.sol +++ b/contracts/mocks/TestSyloUtils.sol @@ -4,11 +4,11 @@ pragma solidity ^0.8.18; import "../libraries/SyloUtils.sol"; contract TestSyloUtils { - function percOf(uint128 value, uint16 percentage) public pure returns (uint256) { + function percOf(uint128 value, uint32 percentage) public pure returns (uint256) { return SyloUtils.percOf(value, percentage); } - function asPerc(uint128 numerator, uint256 denominator) public pure returns (uint16) { + function asPerc(uint128 numerator, uint256 denominator) public pure returns (uint32) { return SyloUtils.asPerc(numerator, denominator); } diff --git a/contracts/staking/StakingManager.sol b/contracts/staking/StakingManager.sol index a14556db..3b7800b7 100644 --- a/contracts/staking/StakingManager.sol +++ b/contracts/staking/StakingManager.sol @@ -58,7 +58,7 @@ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeab * represented as a percentage of the Node's total stake, where * the value is a ratio of 10000. */ - uint16 public minimumStakeProportion; + uint32 public minimumStakeProportion; /** * @notice The multiplier used in determining a Seeker's staking @@ -87,7 +87,7 @@ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeab EpochsManager epochsManager, SeekerPowerOracle seekerPowerOracle, uint256 _unlockDuration, - uint16 _minimumStakeProportion, + uint32 _minimumStakeProportion, uint256 _seekerPowerMultiplier ) external initializer { if (address(token) == address(0)) { @@ -154,7 +154,7 @@ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeab * the owner. * @param _minimumStakeProportion The minimum stake proportion in SOLO. */ - function setMinimumStakeProportion(uint16 _minimumStakeProportion) external onlyOwner { + function setMinimumStakeProportion(uint32 _minimumStakeProportion) external onlyOwner { minimumStakeProportion = _minimumStakeProportion; emit MinimumStakeProportionUpdated(_minimumStakeProportion); } @@ -436,7 +436,7 @@ contract StakingManager is IStakingManager, Initializable, Ownable2StepUpgradeab Stake storage stake = stakes[stakee]; uint256 currentlyOwnedStake = stake.stakeEntries[stakee].amount; - uint16 ownedStakeProportion = SyloUtils.asPerc( + uint32 ownedStakeProportion = SyloUtils.asPerc( SafeCast.toUint128(currentlyOwnedStake), stake.totalManagedStake ); diff --git a/deployments/genesis.config.ts b/deployments/genesis.config.ts index 57c38c4c..e50a79ef 100644 --- a/deployments/genesis.config.ts +++ b/deployments/genesis.config.ts @@ -82,7 +82,7 @@ const GenesisParameters: ContractParameters = { StakingManager: { unlockDuration: 8000, - minimumStakeProportion: 3000, + minimumStakeProportion: 20000, }, SeekerPowerOracle: { @@ -158,7 +158,7 @@ const PorciniDevParameters: ContractParameters = { StakingManager: { unlockDuration: 10, - minimumStakeProportion: 3000, + minimumStakeProportion: 20000, }, SeekerPowerOracle: { diff --git a/test/utils.ts b/test/utils.ts index 941497fa..05ba558a 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -42,7 +42,7 @@ const initializeContracts = async function ( const unlockDuration = opts.unlockDuration ?? 10; - const minimumStakeProportion = opts.minimumStakeProportion ?? 2000; + const minimumStakeProportion = opts.minimumStakeProportion ?? 20000; const seekerPowerMultiplier = opts.seekerPowerMultiplier ?? ethers.parseEther('1000000'); From 708bc7985dca8c3ec1adb1de05255fcb4dee2e04 Mon Sep 17 00:00:00 2001 From: John Carlo San Pedro Date: Fri, 16 Feb 2024 16:35:15 +1300 Subject: [PATCH 07/10] add seeker power multipler argument to staking manager deployment --- deploy/001_deploy_contracts.ts | 1 + deployments/genesis.config.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/deploy/001_deploy_contracts.ts b/deploy/001_deploy_contracts.ts index fa3a57cf..c1a0fc4d 100644 --- a/deploy/001_deploy_contracts.ts +++ b/deploy/001_deploy_contracts.ts @@ -104,6 +104,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { contracts[ContractNames.seekerPowerOracle].address, config.StakingManager.unlockDuration, config.StakingManager.minimumStakeProportion, + config.StakingManager.seekerPowerMultiplier, ], }, { diff --git a/deployments/genesis.config.ts b/deployments/genesis.config.ts index e50a79ef..19c256eb 100644 --- a/deployments/genesis.config.ts +++ b/deployments/genesis.config.ts @@ -35,6 +35,7 @@ type ContractParameters = { StakingManager: { unlockDuration: BigNumberish; minimumStakeProportion: number; + seekerPowerMultiplier: BigNumberish; }; SeekerPowerOracle: { @@ -83,6 +84,7 @@ const GenesisParameters: ContractParameters = { StakingManager: { unlockDuration: 8000, minimumStakeProportion: 20000, + seekerPowerMultiplier: hre.ethers.parseEther('1000000'), }, SeekerPowerOracle: { @@ -120,7 +122,8 @@ const GanacheTestnetParameters: ContractParameters = { StakingManager: { unlockDuration: 30, // 30 * 4 = 120 seconds = 2 minutes - minimumStakeProportion: 3000, + minimumStakeProportion: 20000, + seekerPowerMultiplier: hre.ethers.parseEther('1000000'), }, SeekerPowerOracle: { @@ -159,6 +162,7 @@ const PorciniDevParameters: ContractParameters = { StakingManager: { unlockDuration: 10, minimumStakeProportion: 20000, + seekerPowerMultiplier: hre.ethers.parseEther('1000000'), }, SeekerPowerOracle: { From b40bd31edbde923a3d4175119ac405517f518d1b Mon Sep 17 00:00:00 2001 From: John Carlo San Pedro Date: Fri, 16 Feb 2024 21:59:30 +1300 Subject: [PATCH 08/10] update default payout perc and decay rate param types --- contracts/Registries.sol | 18 +++++++++--------- contracts/epochs/EpochsManager.sol | 2 +- contracts/interfaces/IRegistries.sol | 6 +++--- contracts/interfaces/epochs/IEpochsManager.sol | 4 ++-- .../ticketing/ITicketingParameters.sol | 4 ++-- .../payments/ticketing/TicketingParameters.sol | 12 ++++++------ deployments/genesis.config.ts | 12 ++++++------ test/payments/ticketing.test.ts | 2 +- test/registries.test.ts | 8 ++++---- test/utils.ts | 6 ++++-- 10 files changed, 38 insertions(+), 36 deletions(-) diff --git a/contracts/Registries.sol b/contracts/Registries.sol index 01fa99bd..719407b7 100644 --- a/contracts/Registries.sol +++ b/contracts/Registries.sol @@ -51,13 +51,13 @@ contract Registries is IRegistries, Initializable, Ownable2StepUpgradeable, IERC * that will be allocated to the Node's stakers. This is global, and is * currently set for all Nodes. */ - uint16 public defaultPayoutPercentage; + uint32 public defaultPayoutPercentage; - event DefaultPayoutPercentageUpdated(uint16 defaultPayoutPercentage); + event DefaultPayoutPercentageUpdated(uint32 defaultPayoutPercentage); error NonceCannotBeReused(); error EndMustBeGreaterThanStart(); - error PercentageCannotExceed10000(); + error PercentageCannotExceed100000(); error PublicEndpointCannotBeEmpty(); error SeekerAccountMustOwnSeekerId(); error SeekerAccountMustBeMsgSender(); @@ -68,13 +68,13 @@ contract Registries is IRegistries, Initializable, Ownable2StepUpgradeable, IERC function initialize( IERC721 rootSeekers, - uint16 _defaultPayoutPercentage + uint32 _defaultPayoutPercentage ) external initializer { if (address(rootSeekers) == address(0)) { revert RootSeekersCannotBeZeroAddress(); } - if (_defaultPayoutPercentage > 10000) { - revert PercentageCannotExceed10000(); + if (_defaultPayoutPercentage > 100000) { + revert PercentageCannotExceed100000(); } Ownable2StepUpgradeable.__Ownable2Step_init(); @@ -97,9 +97,9 @@ contract Registries is IRegistries, Initializable, Ownable2StepUpgradeable, IERC * @param _defaultPayoutPercentage The payout percentage as a value where the * denominator is 10000. */ - function setDefaultPayoutPercentage(uint16 _defaultPayoutPercentage) external onlyOwner { - if (_defaultPayoutPercentage > 10000) { - revert PercentageCannotExceed10000(); + function setDefaultPayoutPercentage(uint32 _defaultPayoutPercentage) external onlyOwner { + if (_defaultPayoutPercentage > 100000) { + revert PercentageCannotExceed100000(); } defaultPayoutPercentage = _defaultPayoutPercentage; diff --git a/contracts/epochs/EpochsManager.sol b/contracts/epochs/EpochsManager.sol index ceec1217..74de7d9c 100644 --- a/contracts/epochs/EpochsManager.sol +++ b/contracts/epochs/EpochsManager.sol @@ -142,7 +142,7 @@ contract EpochsManager is IEpochsManager, Initializable, Ownable2StepUpgradeable uint128 baseLiveWinProb, uint128 expiredWinProb, uint256 ticketDuration, - uint16 decayRate + uint32 decayRate ) = _ticketingParameters.getTicketingParameters(); uint256 nextEpochId = getNextEpochId(); diff --git a/contracts/interfaces/IRegistries.sol b/contracts/interfaces/IRegistries.sol index f9b4a122..4ec801c7 100644 --- a/contracts/interfaces/IRegistries.sol +++ b/contracts/interfaces/IRegistries.sol @@ -4,10 +4,10 @@ pragma solidity ^0.8.18; interface IRegistries { struct Registry { // Percentage of a tickets value that will be rewarded to - // delegated stakers expressed as a fraction of 10000. + // delegated stakers expressed as a fraction of 100000. // This value is currently locked to the default payout percentage // until epochs are implemented. - uint16 payoutPercentage; + uint32 payoutPercentage; // Public http/s endpoint to retrieve additional metadata // about the node. // The current metadata schema is as follows: @@ -23,7 +23,7 @@ interface IRegistries { function register(string calldata publicEndpoint) external; - function setDefaultPayoutPercentage(uint16 _defaultPayoutPercentage) external; + function setDefaultPayoutPercentage(uint32 _defaultPayoutPercentage) external; function setSeekerAccount( address seekerAccount, diff --git a/contracts/interfaces/epochs/IEpochsManager.sol b/contracts/interfaces/epochs/IEpochsManager.sol index 551133cc..e66c3cfa 100644 --- a/contracts/interfaces/epochs/IEpochsManager.sol +++ b/contracts/interfaces/epochs/IEpochsManager.sol @@ -15,9 +15,9 @@ interface IEpochsManager { // Zero here represents the epoch has not yet ended. // registry variables - uint16 defaultPayoutPercentage; + uint32 defaultPayoutPercentage; // ticketing variables - uint16 decayRate; + uint32 decayRate; uint256 faceValue; uint128 baseLiveWinProb; uint128 expiredWinProb; diff --git a/contracts/interfaces/payments/ticketing/ITicketingParameters.sol b/contracts/interfaces/payments/ticketing/ITicketingParameters.sol index 82045558..c3028027 100644 --- a/contracts/interfaces/payments/ticketing/ITicketingParameters.sol +++ b/contracts/interfaces/payments/ticketing/ITicketingParameters.sol @@ -8,12 +8,12 @@ interface ITicketingParameters { function setExpiredWinProb(uint128 _expiredWinProb) external; - function setDecayRate(uint16 _decayRate) external; + function setDecayRate(uint32 _decayRate) external; function setTicketDuration(uint256 _ticketDuration) external; function getTicketingParameters() external view - returns (uint256, uint128, uint128, uint256, uint16); + returns (uint256, uint128, uint128, uint256, uint32); } diff --git a/contracts/payments/ticketing/TicketingParameters.sol b/contracts/payments/ticketing/TicketingParameters.sol index 8a45063d..b6800059 100644 --- a/contracts/payments/ticketing/TicketingParameters.sol +++ b/contracts/payments/ticketing/TicketingParameters.sol @@ -50,15 +50,15 @@ contract TicketingParameters is * probability that will be decayed once a ticket has expired. * Example: 80% decayRate indicates that a ticket will decay down to 20% of its * base win probability upon reaching the block before its expiry. - * The value is expressed as a fraction of 10000. + * The value is expressed as a fraction of 100000. */ - uint16 public decayRate; + uint32 public decayRate; event FaceValueUpdated(uint256 faceValue); event BaseLiveWinProbUpdated(uint128 baseLiveWinprob); event ExpiredWinProbUpdated(uint128 expiredWinProb); event TicketDurationUpdated(uint256 ticketDuration); - event DecayRateUpdated(uint16 decayRate); + event DecayRateUpdated(uint32 decayRate); error FaceValueCannotBeZero(); error TicketDurationCannotBeZero(); @@ -67,7 +67,7 @@ contract TicketingParameters is uint256 _faceValue, uint128 _baseLiveWinProb, uint128 _expiredWinProb, - uint16 _decayRate, + uint32 _decayRate, uint256 _ticketDuration ) external initializer { if (_faceValue == 0) { @@ -136,7 +136,7 @@ contract TicketingParameters is * @param _decayRate The decay rate as a percentage, where the * denominator is 10000. */ - function setDecayRate(uint16 _decayRate) external onlyOwner { + function setDecayRate(uint32 _decayRate) external onlyOwner { decayRate = _decayRate; emit DecayRateUpdated(_decayRate); } @@ -166,7 +166,7 @@ contract TicketingParameters is function getTicketingParameters() external view - returns (uint256, uint128, uint128, uint256, uint16) + returns (uint256, uint128, uint128, uint256, uint32) { return (faceValue, baseLiveWinProb, expiredWinProb, ticketDuration, decayRate); } diff --git a/deployments/genesis.config.ts b/deployments/genesis.config.ts index 19c256eb..65313f00 100644 --- a/deployments/genesis.config.ts +++ b/deployments/genesis.config.ts @@ -66,7 +66,7 @@ const GenesisParameters: ContractParameters = { }, Registries: { - defaultPayoutPercentage: 5000, + defaultPayoutPercentage: 50000, }, TicketingParameters: { @@ -74,7 +74,7 @@ const GenesisParameters: ContractParameters = { baseLiveWinProb: (2n ** 128n - 1n) / 1000n, expiredWinProb: (2n ** 128n - 1n) / 1000n, ticketDuration: 80000, - decayRate: 8000, + decayRate: 80000, }, Ticketing: { @@ -105,7 +105,7 @@ const GanacheTestnetParameters: ContractParameters = { }, Registries: { - defaultPayoutPercentage: 5000, + defaultPayoutPercentage: 50000, }, TicketingParameters: { @@ -113,7 +113,7 @@ const GanacheTestnetParameters: ContractParameters = { baseLiveWinProb: (2n ** 128n - 1n) / 1000n, expiredWinProb: (2n ** 128n - 1n) / 1000n, ticketDuration: 10_000_000, // make sure the ticket never expires in the short time on testnet - decayRate: 8000, + decayRate: 80000, }, Ticketing: { @@ -144,7 +144,7 @@ const PorciniDevParameters: ContractParameters = { }, Registries: { - defaultPayoutPercentage: 5000, + defaultPayoutPercentage: 50000, }, TicketingParameters: { @@ -152,7 +152,7 @@ const PorciniDevParameters: ContractParameters = { baseLiveWinProb: 2n ** 128n - 1n / 10n, expiredWinProb: 2n ** 128n - 1n, ticketDuration: 17280, - decayRate: 8000, + decayRate: 80000, }, Ticketing: { diff --git a/test/payments/ticketing.test.ts b/test/payments/ticketing.test.ts index d7d80f2d..e440280c 100644 --- a/test/payments/ticketing.test.ts +++ b/test/payments/ticketing.test.ts @@ -2383,7 +2383,7 @@ describe('Ticketing', () => { faceValue, baseLiveWinProb: 100000, expiredWinProb: 1000, - decayRate: 8000, + decayRate: 80000, ticketDuration: 100, }); epochsManager = contracts.epochsManager; diff --git a/test/registries.test.ts b/test/registries.test.ts index 6171254c..61fb0eab 100644 --- a/test/registries.test.ts +++ b/test/registries.test.ts @@ -49,8 +49,8 @@ describe('Registries', () => { ); await expect( - registries.initialize(await seekers.getAddress(), 10001), - ).to.be.revertedWithCustomError(registries, 'PercentageCannotExceed10000'); + registries.initialize(await seekers.getAddress(), 100001), + ).to.be.revertedWithCustomError(registries, 'PercentageCannotExceed100000'); }); it('can allow owner to set default payout percentage', async () => { @@ -183,8 +183,8 @@ describe('Registries', () => { it('requires default payout percentage to not exceed 100%', async () => { await expect( - registries.setDefaultPayoutPercentage(10001), - ).to.be.revertedWithCustomError(registries, 'PercentageCannotExceed10000'); + registries.setDefaultPayoutPercentage(100001), + ).to.be.revertedWithCustomError(registries, 'PercentageCannotExceed100000'); }); it('can set seeker account with valid proof', async () => { diff --git a/test/utils.ts b/test/utils.ts index 05ba558a..39f47b3e 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -30,12 +30,14 @@ const initializeContracts = async function ( syloToken: SyloToken, opts: Options = {}, ): Promise { - const payoutPercentage = opts.payoutPercentage ? opts.payoutPercentage : 5000; + const payoutPercentage = opts.payoutPercentage + ? opts.payoutPercentage + : 50000; const faceValue = opts.faceValue ?? toWei('15'); const baseLiveWinProb = opts.baseLiveWinProb ?? 2n ** 128n - 1n; const expiredWinProb = opts.expiredWinProb ?? 1000; - const decayRate = opts.decayRate ?? 8000; + const decayRate = opts.decayRate ?? 80000; const ticketDuration = opts.ticketDuration ?? 20; const epochDuration = opts.epochDuration ?? 30; From 6b2cccada8919e598cbae152b616dbba0480ff93 Mon Sep 17 00:00:00 2001 From: John Carlo San Pedro Date: Tue, 20 Feb 2024 16:13:27 +1300 Subject: [PATCH 09/10] correct comments --- contracts/Registries.sol | 2 +- contracts/libraries/SyloUtils.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/Registries.sol b/contracts/Registries.sol index 719407b7..e5519ca5 100644 --- a/contracts/Registries.sol +++ b/contracts/Registries.sol @@ -95,7 +95,7 @@ contract Registries is IRegistries, Initializable, Ownable2StepUpgradeable, IERC * @notice Set the global default payout percentage value. Only callable * by the owner. * @param _defaultPayoutPercentage The payout percentage as a value where the - * denominator is 10000. + * denominator is 100000. */ function setDefaultPayoutPercentage(uint32 _defaultPayoutPercentage) external onlyOwner { if (_defaultPayoutPercentage > 100000) { diff --git a/contracts/libraries/SyloUtils.sol b/contracts/libraries/SyloUtils.sol index c37a6ff4..e6d82076 100644 --- a/contracts/libraries/SyloUtils.sol +++ b/contracts/libraries/SyloUtils.sol @@ -26,7 +26,7 @@ library SyloUtils { * @dev Multiply a value by a given percentage. Converts the provided * uint128 value to uint256 to avoid any reverts on overflow. * @param value The value to multiply. - * @param percentage The percentage, as a ratio of 10000. + * @param percentage The percentage, as a ratio of 100000. */ function percOf(uint128 value, uint32 percentage) internal pure returns (uint256) { return (uint256(value) * percentage) / PERCENTAGE_DENOMINATOR; @@ -37,7 +37,7 @@ library SyloUtils { * @param numerator The numerator limited to a uint128 value to prevent * phantom overflow. * @param denominator The denominator. - * @return The percentage, as a ratio of 10000. + * @return The percentage, as a ratio of 100000. */ function asPerc(uint128 numerator, uint256 denominator) internal pure returns (uint32) { return SafeCast.toUint32((uint256(numerator) * PERCENTAGE_DENOMINATOR) / denominator); From ea6f0cf0e671be9004738e27aff1883dc23cf608 Mon Sep 17 00:00:00 2001 From: John Carlo San Pedro Date: Tue, 20 Feb 2024 16:26:16 +1300 Subject: [PATCH 10/10] ensure oracle cannot be initialized with zero address --- contracts/SeekerPowerOracle.sol | 5 ++++ deployments/ganache_deployment_phase_two.json | 28 +++++++++---------- deployments/genesis.config.ts | 4 +-- test/seekerOracle.test.ts | 14 ++++++++++ test/staking.test.ts | 2 +- 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/contracts/SeekerPowerOracle.sol b/contracts/SeekerPowerOracle.sol index fda4bd1d..85acbbac 100644 --- a/contracts/SeekerPowerOracle.sol +++ b/contracts/SeekerPowerOracle.sol @@ -37,10 +37,15 @@ contract SeekerPowerOracle is ISeekerPowerOracle, Initializable, Ownable2StepUpg error UnauthorizedRegisterSeekerPowerCall(); error NonceCannotBeReused(); error PowerCannotBeZero(); + error OracleCannotBeZeroAddress(); function initialize(address _oracle) external initializer { Ownable2StepUpgradeable.__Ownable2Step_init(); + if (_oracle == address(0)) { + revert OracleCannotBeZeroAddress(); + } + oracle = _oracle; } diff --git a/deployments/ganache_deployment_phase_two.json b/deployments/ganache_deployment_phase_two.json index 275caf59..b3281211 100644 --- a/deployments/ganache_deployment_phase_two.json +++ b/deployments/ganache_deployment_phase_two.json @@ -1,15 +1,15 @@ { - "deployer": "0x835dF5fE77D479695a616F79A3FC3a25310eb7c6", - "syloToken": "0xc4db8fD3209c98290AB32693F5155c596B97Eabe", - "authorizedAccounts": "0x075EEeD1215982b78A2e05cD2213b5f53A718a9A", - "registries": "0xFB87c433852Bb2917B37b0471DFA5B369e75083A", - "ticketingParameters": "0x65A9be6e97eD2F417250A819e93BCb359b1140d0", - "epochsManager": "0x7bFCE7796fdE3Ba0F2052959d506bdA480518edA", - "stakingManager": "0xa4dE4FEA5e961e5C130013CfE207f7C08148A73C", - "rewardsManager": "0x4aa109E0DB0223ed2d9085679F450EfDf188CFf6", - "directory": "0x7E7C762176eaa1662d372399265760d4600CCf28", - "syloTicketing": "0x943E7031A7Ed0FC236173f05a3084104b81Aa480", - "seekers": "0x49C537a88016186Ef41713239799Fc975F9e9aFA", - "seekerPowerOracle": "0xca7efb9aA54e70F7a8e7efb878A48BaefA34F4AC", - "futurepassRegistrar": "0x7DBf77bb534997892e5Bcfbdc79Dd82E71C35245" -} \ No newline at end of file + "deployer": "0x835dF5fE77D479695a616F79A3FC3a25310eb7c6", + "syloToken": "0xc4db8fD3209c98290AB32693F5155c596B97Eabe", + "authorizedAccounts": "0x075EEeD1215982b78A2e05cD2213b5f53A718a9A", + "registries": "0xFB87c433852Bb2917B37b0471DFA5B369e75083A", + "ticketingParameters": "0x65A9be6e97eD2F417250A819e93BCb359b1140d0", + "epochsManager": "0x7bFCE7796fdE3Ba0F2052959d506bdA480518edA", + "stakingManager": "0xa4dE4FEA5e961e5C130013CfE207f7C08148A73C", + "rewardsManager": "0x4aa109E0DB0223ed2d9085679F450EfDf188CFf6", + "directory": "0x7E7C762176eaa1662d372399265760d4600CCf28", + "syloTicketing": "0x943E7031A7Ed0FC236173f05a3084104b81Aa480", + "seekers": "0x49C537a88016186Ef41713239799Fc975F9e9aFA", + "seekerPowerOracle": "0xca7efb9aA54e70F7a8e7efb878A48BaefA34F4AC", + "futurepassRegistrar": "0x7DBf77bb534997892e5Bcfbdc79Dd82E71C35245" +} diff --git a/deployments/genesis.config.ts b/deployments/genesis.config.ts index 65313f00..87410965 100644 --- a/deployments/genesis.config.ts +++ b/deployments/genesis.config.ts @@ -88,7 +88,7 @@ const GenesisParameters: ContractParameters = { }, SeekerPowerOracle: { - oracleAccount: '0x835dF5fE77D479695a616F79A3FC3a25310eb7c6', + oracleAccount: '', }, }; @@ -127,7 +127,7 @@ const GanacheTestnetParameters: ContractParameters = { }, SeekerPowerOracle: { - oracleAccount: '', + oracleAccount: '0x835dF5fE77D479695a616F79A3FC3a25310eb7c6', // deployer }, }; diff --git a/test/seekerOracle.test.ts b/test/seekerOracle.test.ts index 4892a5e8..2c4b25d6 100644 --- a/test/seekerOracle.test.ts +++ b/test/seekerOracle.test.ts @@ -25,6 +25,20 @@ describe('Seeker Power Oracle', () => { contracts = await utils.initializeContracts(deployer, token); }); + it('seeker power oracle cannot be initialized with invalid arguments', async () => { + const SeekerPowerOracle = await ethers.getContractFactory( + 'SeekerPowerOracle', + ); + const seekerPowerOracle = await SeekerPowerOracle.deploy(); + + await expect( + seekerPowerOracle.initialize(ethers.ZeroAddress), + ).to.be.revertedWithCustomError( + seekerPowerOracle, + 'OracleCannotBeZeroAddress', + ); + }); + it('seeker power oracle cannot be initialized twice', async () => { await expect( contracts.seekerPowerOracle.initialize(deployer), diff --git a/test/staking.test.ts b/test/staking.test.ts index b0dd7ad4..19e0ee26 100644 --- a/test/staking.test.ts +++ b/test/staking.test.ts @@ -523,7 +523,7 @@ describe('Staking', () => { it('cannot join directory with invalid arguments', async () => { await directory.addManager(owner); await expect( - directory.joinNextDirectory(ethers.ZeroAddress, 1), + directory.joinNextDirectory(ethers.ZeroAddress, defaultSeekerId), ).to.be.revertedWithCustomError(directory, 'StakeeCannotBeZeroAddress'); });