diff --git a/contracts/SeekerPowerOracle.sol b/contracts/SeekerPowerOracle.sol index cb7bfd5f..ad141f29 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 owner 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. @@ -33,14 +34,22 @@ contract SeekerPowerOracle is ISeekerPowerOracle, Initializable, Ownable2StepUpg event SeekerPowerUpdated(uint256 indexed seekerId, uint256 indexed power); + error UnauthorizedSetSeekerCall(); + error NonceCannotBeReused(); + function initialize(address _oracle) external initializer { Ownable2StepUpgradeable.__Ownable2Step_init(); oracle = _oracle; } - error UnauthorizedSetSeekerCall(); - error NonceCannotBeReused(); + /** + * @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. 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 0d69af36..e633a6f9 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 }, @@ -181,6 +187,7 @@ const advanceBlock = async function (i: number): Promise { async function setSeekerRegistry( registries: Registries, seekers: TestSeekers, + seekerPowerOracle: SeekerPowerOracle, account: Signer, seekerAccount: Signer, tokenId: number, @@ -212,6 +219,9 @@ async function setSeekerRegistry( nonce, signature, ); + + // set the seeker power default to the max value + await seekerPowerOracle.registerSeekerPowerRestricted(tokenId, MaxUint256); } export default {