diff --git a/eth-contracts/contracts/ServiceProviderFactory.sol b/eth-contracts/contracts/ServiceProviderFactory.sol index 8f240a907bc..52521efead7 100644 --- a/eth-contracts/contracts/ServiceProviderFactory.sol +++ b/eth-contracts/contracts/ServiceProviderFactory.sol @@ -17,6 +17,9 @@ contract ServiceProviderFactory is InitializableV2 { string private constant ERROR_ONLY_GOVERNANCE = ( "ServiceProviderFactory: Only callable by Governance contract" ); + string private constant ERROR_ONLY_SP_GOVERNANCE = ( + "ServiceProviderFactory: Only callable by Service Provider or Governance" + ); address private stakingAddress; address private delegateManagerAddress; @@ -24,6 +27,7 @@ contract ServiceProviderFactory is InitializableV2 { address private serviceTypeManagerAddress; address private claimsManagerAddress; uint256 private decreaseStakeLockupDuration; + uint256 private deployerCutLockupDuration; /// @dev - Stores following entities /// 1) Directly staked amount by SP, not including delegators @@ -47,8 +51,11 @@ contract ServiceProviderFactory is InitializableV2 { uint256 lockupExpiryBlock; } - /// @dev - Mapping of service provider address to details - mapping(address => ServiceProviderDetails) private spDetails; + /// @dev - Data structure for time delay during deployer cut update + struct UpdateDeployerCutRequest { + uint256 newDeployerCut; + uint256 lockupExpiryBlock; + } /// @dev - Struct maintaining information about sp /// @dev - blocknumber is block.number when endpoint registered @@ -59,6 +66,9 @@ contract ServiceProviderFactory is InitializableV2 { address delegateOwnerWallet; } + /// @dev - Mapping of service provider address to details + mapping(address => ServiceProviderDetails) private spDetails; + /// @dev - Uniquely assigned serviceProvider ID, incremented for each service type /// @notice - Keeps track of the total number of services registered regardless of /// whether some have been deregistered since @@ -82,6 +92,9 @@ contract ServiceProviderFactory is InitializableV2 { /// @dev - Mapping of service provider -> decrease stake request mapping(address => DecreaseStakeRequest) private decreaseStakeRequests; + /// @dev - Mapping of service provider -> update deployer cut requests + mapping(address => UpdateDeployerCutRequest) private updateDeployerCutRequests; + event RegisteredServiceProvider( uint256 indexed _spID, bytes32 indexed _serviceType, @@ -124,6 +137,7 @@ contract ServiceProviderFactory is InitializableV2 { ); event DecreaseStakeLockupDurationUpdated(uint256 indexed _lockupDuration); + event UpdateDeployerCutLockupDurationUpdated(uint256 indexed _lockupDuration); event GovernanceAddressUpdated(address indexed _newGovernanceAddress); event StakingAddressUpdated(address indexed _newStakingAddress); event ClaimsManagerAddressUpdated(address indexed _newClaimsManagerAddress); @@ -140,11 +154,15 @@ contract ServiceProviderFactory is InitializableV2 { */ function initialize ( address _governanceAddress, - uint256 _decreaseStakeLockupDuration + address _claimsManagerAddress, + uint256 _decreaseStakeLockupDuration, + uint256 _deployerCutLockupDuration ) public initializer { - decreaseStakeLockupDuration = _decreaseStakeLockupDuration; _updateGovernanceAddress(_governanceAddress); + claimsManagerAddress = _claimsManagerAddress; + _updateDecreaseStakeLockupDuration(_decreaseStakeLockupDuration); + _updateDeployerCutLockupDuration(_deployerCutLockupDuration); InitializableV2.initialize(); } @@ -575,6 +593,86 @@ contract ServiceProviderFactory is InitializableV2 { return spId; } + /** + * @notice Update the deployer cut for a given service provider + * @param _serviceProvider - address of service provider + * @param _cut - new value for deployer cut + */ + function requestUpdateDeployerCut(address _serviceProvider, uint256 _cut) external + { + _requireIsInitialized(); + + require( + msg.sender == _serviceProvider || msg.sender == governanceAddress, + ERROR_ONLY_SP_GOVERNANCE + ); + + require( + (updateDeployerCutRequests[_serviceProvider].lockupExpiryBlock == 0) && + (updateDeployerCutRequests[_serviceProvider].newDeployerCut == 0), + "ServiceProviderFactory: Update deployer cut operation pending" + ); + + require( + _cut <= DEPLOYER_CUT_BASE, + "ServiceProviderFactory: Service Provider cut cannot exceed base value" + ); + + updateDeployerCutRequests[_serviceProvider] = UpdateDeployerCutRequest({ + lockupExpiryBlock: block.number + deployerCutLockupDuration, + newDeployerCut: _cut + }); + } + + /** + * @notice Cancel a pending request to update deployer cut + * @param _serviceProvider - address of service provider + */ + function cancelUpdateDeployerCut(address _serviceProvider) external + { + _requireIsInitialized(); + _requirePendingDeployerCutOperation(_serviceProvider); + + require( + msg.sender == _serviceProvider || msg.sender == governanceAddress, + ERROR_ONLY_SP_GOVERNANCE + ); + + // Zero out request information + delete updateDeployerCutRequests[_serviceProvider]; + } + + /** + * @notice Evalue request to update service provider cut of claims + * @notice Update service provider cut as % of delegate claim, divided by the deployerCutBase. + * @dev SPs will interact with this value as a percent, value translation done client side + @dev A value of 5 dictates a 5% cut, with ( 5 / 100 ) * delegateReward going to an SP from each delegator each round. + */ + function updateDeployerCut(address _serviceProvider) external + { + _requireIsInitialized(); + _requirePendingDeployerCutOperation(_serviceProvider); + + require( + msg.sender == _serviceProvider || msg.sender == governanceAddress, + ERROR_ONLY_SP_GOVERNANCE + ); + + require( + updateDeployerCutRequests[_serviceProvider].lockupExpiryBlock <= block.number, + "ServiceProviderFactory: Lockup must be expired" + ); + + spDetails[_serviceProvider].deployerCut = ( + updateDeployerCutRequests[_serviceProvider].newDeployerCut + ); + + // Zero out request information + delete updateDeployerCutRequests[_serviceProvider]; + + emit ServiceProviderCutUpdated(_serviceProvider, spDetails[_serviceProvider].deployerCut); + } + /** * @notice Update service provider balance * @dev Called by DelegateManager by functions modifying entire stake like claim and slash @@ -599,34 +697,21 @@ contract ServiceProviderFactory is InitializableV2 { _updateServiceProviderBoundStatus(_serviceProvider); } - /** - * @notice Update service provider cut of claims - * @notice Update service provider cut as % of delegate claim, divided by the deployerCutBase. - * @dev SPs will interact with this value as a percent, value translation done client side - @dev A value of 5 dictates a 5% cut, with ( 5 / 100 ) * delegateReward going to an SP from each delegator each round. - * @param _serviceProvider - address of service provider - * @param _cut - new deployer cut value - */ - function updateServiceProviderCut( - address _serviceProvider, - uint256 _cut - ) external - { + /// @notice Update service provider lockup duration + function updateDecreaseStakeLockupDuration(uint256 _duration) external { _requireIsInitialized(); require( - msg.sender == _serviceProvider, - "ServiceProviderFactory: Service Provider cut update operation restricted to deployer"); + msg.sender == governanceAddress, + ERROR_ONLY_GOVERNANCE + ); - require( - _cut <= DEPLOYER_CUT_BASE, - "ServiceProviderFactory: Service Provider cut cannot exceed base value"); - spDetails[_serviceProvider].deployerCut = _cut; - emit ServiceProviderCutUpdated(_serviceProvider, _cut); + _updateDecreaseStakeLockupDuration(_duration); + emit DecreaseStakeLockupDurationUpdated(_duration); } /// @notice Update service provider lockup duration - function updateDecreaseStakeLockupDuration(uint256 _duration) external { + function updateDeployerCutLockupDuration(uint256 _duration) external { _requireIsInitialized(); require( @@ -634,8 +719,8 @@ contract ServiceProviderFactory is InitializableV2 { ERROR_ONLY_GOVERNANCE ); - decreaseStakeLockupDuration = _duration; - emit DecreaseStakeLockupDurationUpdated(_duration); + _updateDeployerCutLockupDuration(_duration); + emit UpdateDeployerCutLockupDurationUpdated(_duration); } /// @notice Get denominator for deployer cut calculations @@ -647,6 +732,15 @@ contract ServiceProviderFactory is InitializableV2 { return DEPLOYER_CUT_BASE; } + /// @notice Get current deployer cut update lockup duration + function getDeployerCutLockupDuration() + external view returns (uint256) + { + _requireIsInitialized(); + + return deployerCutLockupDuration; + } + /// @notice Get total number of service providers for a given serviceType function getTotalServiceTypeProviders(bytes32 _serviceType) external view returns (uint256) @@ -736,6 +830,21 @@ contract ServiceProviderFactory is InitializableV2 { ); } + /** + * @notice Get information about pending decrease stake requests for service provider + * @param _serviceProvider - address of service provider + */ + function getPendingUpdateDeployerCutRequest(address _serviceProvider) + external view returns (uint256 newDeployerCut, uint256 lockupExpiryBlock) + { + _requireIsInitialized(); + + return ( + updateDeployerCutRequests[_serviceProvider].newDeployerCut, + updateDeployerCutRequests[_serviceProvider].lockupExpiryBlock + ); + } + /// @notice Get current unstake lockup duration function getDecreaseStakeLockupDuration() external view returns (uint256) @@ -892,6 +1001,33 @@ contract ServiceProviderFactory is InitializableV2 { governanceAddress = _governanceAddress; } + /** + * @notice Set the deployer cut lockup duration + * @param _duration - incoming duration + */ + function _updateDeployerCutLockupDuration(uint256 _duration) internal + { + require( + ClaimsManager(claimsManagerAddress).getFundingRoundBlockDiff() < _duration, + "ServiceProviderFactory: Incoming duration must be greater than funding round block diff" + ); + deployerCutLockupDuration = _duration; + } + + /** + * @notice Set the decrease stake lockup duration + * @param _duration - incoming duration + */ + function _updateDecreaseStakeLockupDuration(uint256 _duration) internal + { + Governance governance = Governance(governanceAddress); + require( + _duration > governance.getVotingPeriod() + governance.getExecutionDelay(), + "ServiceProviderFactory: decreaseStakeLockupDuration duration must be greater than governance votingPeriod + executionDelay" + ); + decreaseStakeLockupDuration = _duration; + } + /** * @notice Compare a given amount input against valid min and max bounds for service provider * @param _serviceProvider - address of service provider @@ -936,6 +1072,12 @@ contract ServiceProviderFactory is InitializableV2 { } // ========================================= Private Functions ========================================= + function _requirePendingDeployerCutOperation (address _serviceProvider) private view { + require( + (updateDeployerCutRequests[_serviceProvider].lockupExpiryBlock != 0), + "ServiceProviderFactory: No update deployer cut operation pending" + ); + } function _requireStakingAddressIsSet() private view { require( diff --git a/eth-contracts/migrations/7_versioning_service_migration.js b/eth-contracts/migrations/7_versioning_service_migration.js index 2b9f04d7944..ab433d3c7e6 100644 --- a/eth-contracts/migrations/7_versioning_service_migration.js +++ b/eth-contracts/migrations/7_versioning_service_migration.js @@ -35,6 +35,10 @@ const dpTypeMax = _lib.audToWei(2000000) // - 1/13 block/s * 604800 s/wk ~= 46523 block/wk const decreaseStakeLockupDuration = 46523 +// modifying deployer cut = 8 days in blocks +// - 1/13 block/s * 691200 s/8 days ~= 53169 block/wk +const deployerCutLockupDuration = 53169 + module.exports = (deployer, network, accounts) => { deployer.then(async () => { const config = contractConfig[network] @@ -106,8 +110,13 @@ module.exports = (deployer, network, accounts) => { const serviceProviderFactory0 = await deployer.deploy(ServiceProviderFactory, { from: proxyDeployerAddress }) const serviceProviderFactoryCalldata = _lib.encodeCall( 'initialize', - ['address', 'uint256'], - [process.env.governanceAddress, decreaseStakeLockupDuration] + ['address', 'address', 'uint256', 'uint256'], + [ + process.env.governanceAddress, + process.env.claimsManagerAddress, + decreaseStakeLockupDuration, + deployerCutLockupDuration + ] ) const serviceProviderFactoryProxy = await deployer.deploy( AudiusAdminUpgradeabilityProxy, diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index bc06661e28e..e4b7db996b7 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -29,14 +29,15 @@ const DEFAULT_AMOUNT = _lib.toBN(DEFAULT_AMOUNT_VAL) const VOTING_PERIOD = 10 const EXECUTION_DELAY = VOTING_PERIOD const VOTING_QUORUM_PERCENT = 10 -const DECREASE_STAKE_LOCKUP_DURATION = 10 -const UNDELEGATE_LOCKUP_DURATION = 21 +const DEPLOYER_CUT_LOCKUP_DURATION = 11 +const UNDELEGATE_LOCKUP_DURATION = VOTING_PERIOD + EXECUTION_DELAY + 1 +const DECREASE_STAKE_LOCKUP_DURATION = UNDELEGATE_LOCKUP_DURATION const callValue0 = _lib.toBN(0) contract('DelegateManager', async (accounts) => { - let staking, stakingAddress, token, registry, governance, claimsManager0, claimsManagerProxy + let staking, stakingAddress, token, registry, governance let serviceProviderFactory, serviceTypeManager, claimsManager, delegateManager // intentionally not using acct0 to make sure no TX accidentally succeeds without specifying sender @@ -112,12 +113,28 @@ contract('DelegateManager', async (accounts) => { // Register discprov serviceType await _lib.addServiceType(testDiscProvType, serviceTypeMinStake, serviceTypeMaxStake, governance, guardianAddress, serviceTypeManagerProxyKey) + claimsManager = await _lib.deployClaimsManager( + artifacts, + registry, + governance, + proxyDeployerAddress, + guardianAddress, + token.address, + 10, + claimsManagerProxyKey + ) + // Deploy ServiceProviderFactory let serviceProviderFactory0 = await ServiceProviderFactory.new({ from: proxyDeployerAddress }) const serviceProviderFactoryCalldata = _lib.encodeCall( 'initialize', - ['address', 'uint256'], - [governance.address, DECREASE_STAKE_LOCKUP_DURATION] + ['address', 'address', 'uint256', 'uint256'], + [ + governance.address, + claimsManager.address, + DECREASE_STAKE_LOCKUP_DURATION, + DEPLOYER_CUT_LOCKUP_DURATION + ] ) let serviceProviderFactoryProxy = await AudiusAdminUpgradeabilityProxy.new( serviceProviderFactory0.address, @@ -128,29 +145,7 @@ contract('DelegateManager', async (accounts) => { serviceProviderFactory = await ServiceProviderFactory.at(serviceProviderFactoryProxy.address) await registry.addContract(serviceProviderFactoryKey, serviceProviderFactoryProxy.address, { from: proxyDeployerAddress }) - // Deploy new claimsManager proxy - claimsManager0 = await ClaimsManager.new({ from: proxyDeployerAddress }) - const claimsInitializeCallData = _lib.encodeCall( - 'initialize', - ['address', 'address'], - [token.address, governance.address] - ) - claimsManagerProxy = await AudiusAdminUpgradeabilityProxy.new( - claimsManager0.address, - governance.address, - claimsInitializeCallData, - { from: proxyDeployerAddress } - ) - claimsManager = await ClaimsManager.at(claimsManagerProxy.address) - - // Register claimsManagerProxy - await registry.addContract( - claimsManagerProxyKey, - claimsManagerProxy.address, - { from: proxyDeployerAddress } - ) - - // Register new contract as a minter, from the same address that deployed the contract + // Register new ClaimsManager contract as a minter, from the same address that deployed the contract await governance.guardianExecuteTransaction( tokenRegKey, callValue0, @@ -189,7 +184,7 @@ contract('DelegateManager', async (accounts) => { stakingProxyKey, staking, serviceProviderFactoryProxy.address, - claimsManagerProxy.address, + claimsManager.address, delegateManagerProxy.address ) @@ -249,7 +244,7 @@ contract('DelegateManager', async (accounts) => { serviceProviderFactory, staking.address, serviceTypeManagerProxy.address, - claimsManagerProxy.address, + claimsManager.address, delegateManagerProxy.address ) @@ -458,9 +453,31 @@ contract('DelegateManager', async (accounts) => { let finalBal = await token.balanceOf(stakerAccount) assert.isTrue(initialBal.eq(finalBal.add(DEFAULT_AMOUNT)), 'Expect funds to be transferred') - // Update SP Deployer Cut to 10% - await serviceProviderFactory.updateServiceProviderCut(stakerAccount, 10, { from: stakerAccount }) - await serviceProviderFactory.updateServiceProviderCut(stakerAccount2, 10, { from: stakerAccount2 }) + let updatedCut = 10 + + // Request Update SP Deployer Cut to 10% + await serviceProviderFactory.requestUpdateDeployerCut(stakerAccount, updatedCut, { from: stakerAccount }) + await serviceProviderFactory.requestUpdateDeployerCut(stakerAccount2, updatedCut, { from: stakerAccount2 }) + + // Advance to 2nd update block number + let pending2ndUpdate = await serviceProviderFactory.getPendingUpdateDeployerCutRequest(stakerAccount2) + await time.advanceBlockTo(pending2ndUpdate.lockupExpiryBlock) + + // Evaluate both updates + await serviceProviderFactory.updateDeployerCut( + stakerAccount2, + { from: stakerAccount2 } + ) + await serviceProviderFactory.updateDeployerCut( + stakerAccount, + { from: stakerAccount } + ) + + // Confirm updates + let info = await serviceProviderFactory.getServiceProviderDetails(stakerAccount) + assert.isTrue((info.deployerCut).eq(_lib.toBN(updatedCut)), 'Expect updated cut') + info = await serviceProviderFactory.getServiceProviderDetails(stakerAccount2) + assert.isTrue((info.deployerCut).eq(_lib.toBN(updatedCut)), 'Expect updated cut') }) it('Initial state + claim', async () => { diff --git a/eth-contracts/test/governance.test.js b/eth-contracts/test/governance.test.js index 700a4977f14..b51ebe8e31e 100644 --- a/eth-contracts/test/governance.test.js +++ b/eth-contracts/test/governance.test.js @@ -49,11 +49,12 @@ contract('Governance.sol', async (accounts) => { const votingPeriod = 10 const votingQuorumPercent = 10 - const decreaseStakeLockupDuration = 10 const maxInProgressProposals = 20 const maxDescriptionLength = 250 const executionDelay = votingPeriod - const undelegateLockupDuration = 21 + const deployerCutLockupDuration = 11 + const undelegateLockupDuration = votingPeriod + executionDelay + 1 + const decreaseStakeLockupDuration = undelegateLockupDuration // intentionally not using acct0 to make sure no TX accidentally succeeds without specifying sender const [, proxyAdminAddress, proxyDeployerAddress, newUpdateAddress] = accounts @@ -157,12 +158,29 @@ contract('Governance.sol', async (accounts) => { // Register discprov serviceType await _lib.addServiceType(testDiscProvType, spMinStake, spMaxStake, governance, guardianAddress, serviceTypeManagerProxyKey) + // Deploy + register claimsManagerProxy + claimsManager = await _lib.deployClaimsManager( + artifacts, + registry, + governance, + proxyDeployerAddress, + guardianAddress, + token.address, + 10, + claimsManagerProxyKey + ) + // Deploy + Register ServiceProviderFactory contract const serviceProviderFactory0 = await ServiceProviderFactory.new({ from: proxyDeployerAddress }) const serviceProviderFactoryCalldata = _lib.encodeCall( 'initialize', - ['address', 'uint256'], - [governance.address, decreaseStakeLockupDuration] + ['address', 'address', 'uint256', 'uint256'], + [ + governance.address, + claimsManager.address, + decreaseStakeLockupDuration, + deployerCutLockupDuration + ] ) const serviceProviderFactoryProxy = await AudiusAdminUpgradeabilityProxy.new( serviceProviderFactory0.address, @@ -173,26 +191,6 @@ contract('Governance.sol', async (accounts) => { serviceProviderFactory = await ServiceProviderFactory.at(serviceProviderFactoryProxy.address) await registry.addContract(serviceProviderFactoryKey, serviceProviderFactoryProxy.address, { from: proxyDeployerAddress }) - // Deploy + register claimsManagerProxy - const claimsManager0 = await ClaimsManager.new({ from: proxyDeployerAddress }) - const claimsInitializeCallData = _lib.encodeCall( - 'initialize', - ['address', 'address'], - [token.address, governance.address] - ) - const claimsManagerProxy = await AudiusAdminUpgradeabilityProxy.new( - claimsManager0.address, - governance.address, - claimsInitializeCallData, - { from: proxyDeployerAddress } - ) - claimsManager = await ClaimsManager.at(claimsManagerProxy.address) - await registry.addContract( - claimsManagerProxyKey, - claimsManagerProxy.address, - { from: proxyDeployerAddress } - ) - // Register new contract as a minter, from the same address that deployed the contract await governance.guardianExecuteTransaction( tokenRegKey, @@ -232,7 +230,7 @@ contract('Governance.sol', async (accounts) => { stakingProxyKey, staking, serviceProviderFactoryProxy.address, - claimsManagerProxy.address, + claimsManager.address, delegateManagerProxy.address ) // ---- Set up claims manager contract permissions @@ -265,7 +263,7 @@ contract('Governance.sol', async (accounts) => { serviceProviderFactory, staking.address, serviceTypeManagerProxy.address, - claimsManagerProxy.address, + claimsManager.address, delegateManager.address ) }) diff --git a/eth-contracts/test/serviceProvider.test.js b/eth-contracts/test/serviceProvider.test.js index d22d098b48f..7f317bf5e16 100644 --- a/eth-contracts/test/serviceProvider.test.js +++ b/eth-contracts/test/serviceProvider.test.js @@ -1,4 +1,5 @@ import * as _lib from '../utils/lib.js' +import { update } from 'lodash' const { time, expectEvent } = require('@openzeppelin/test-helpers') const Staking = artifacts.require('Staking') @@ -28,14 +29,15 @@ const MIN_STAKE_AMOUNT = 10 const VOTING_PERIOD = 10 const EXECUTION_DELAY = VOTING_PERIOD const VOTING_QUORUM_PERCENT = 10 -const DECREASE_STAKE_LOCKUP_DURATION = 10 +const DECREASE_STAKE_LOCKUP_DURATION = VOTING_PERIOD + EXECUTION_DELAY + 1 +const DEPLOYER_CUT_LOCKUP_DURATION = 11 const INITIAL_BAL = _lib.audToWeiBN(1000) const DEFAULT_AMOUNT = _lib.audToWeiBN(120) contract('ServiceProvider test', async (accounts) => { - let token, registry, staking0, stakingInitializeData, proxy, claimsManager0, claimsManagerProxy, claimsManager, governance + let token, registry, staking0, stakingInitializeData, proxy, claimsManager, governance let staking, serviceProviderFactory, serviceTypeManager, mockDelegateManager // intentionally not using acct0 to make sure no TX accidentally succeeds without specifying sender @@ -169,24 +171,21 @@ contract('ServiceProvider test', async (accounts) => { await registry.addContract(serviceTypeManagerProxyKey, serviceTypeManager.address, { from: proxyDeployerAddress }) // Deploy + register claimsManagerProxy - claimsManager0 = await ClaimsManager.new({ from: proxyDeployerAddress }) - const claimsInitializeCallData = _lib.encodeCall( - 'initialize', - ['address', 'address'], - [token.address, governance.address] - ) - claimsManagerProxy = await AudiusAdminUpgradeabilityProxy.new( - claimsManager0.address, - governance.address, - claimsInitializeCallData, - { from: proxyDeployerAddress } + claimsManager = await _lib.deployClaimsManager( + artifacts, + registry, + governance, + proxyDeployerAddress, + guardianAddress, + token.address, + 10, + claimsManagerProxyKey ) - claimsManager = await ClaimsManager.at(claimsManagerProxy.address) - await registry.addContract(claimsManagerProxyKey, claimsManagerProxy.address, { from: proxyDeployerAddress }) + // End claims manager setup // Deploy mock delegate manager with only function to forward processClaim call mockDelegateManager = await MockDelegateManager.new() - await mockDelegateManager.initialize(claimsManagerProxy.address) + await mockDelegateManager.initialize(claimsManager.address) await registry.addContract(delegateManagerKey, mockDelegateManager.address, { from: proxyDeployerAddress }) /** addServiceTypes creatornode and discprov via Governance */ @@ -219,8 +218,13 @@ contract('ServiceProvider test', async (accounts) => { let serviceProviderFactory0 = await ServiceProviderFactory.new({ from: proxyDeployerAddress }) const serviceProviderFactoryCalldata = _lib.encodeCall( 'initialize', - ['address', 'uint256'], - [governance.address, DECREASE_STAKE_LOCKUP_DURATION] + ['address', 'address', 'uint256', 'uint256'], + [ + governance.address, + claimsManager.address, + DECREASE_STAKE_LOCKUP_DURATION, + DEPLOYER_CUT_LOCKUP_DURATION + ] ) let serviceProviderFactoryProxy = await AudiusAdminUpgradeabilityProxy.new( serviceProviderFactory0.address, @@ -248,7 +252,7 @@ contract('ServiceProvider test', async (accounts) => { stakingProxyKey, staking, serviceProviderFactoryProxy.address, - claimsManagerProxy.address, + claimsManager.address, _lib.addressZero ) // ---- Set up claims manager contract permissions @@ -289,7 +293,7 @@ contract('ServiceProvider test', async (accounts) => { serviceProviderFactory, staking.address, serviceTypeManagerProxy.address, - claimsManagerProxy.address, + claimsManager.address, mockDelegateManager.address ) @@ -315,7 +319,7 @@ contract('ServiceProvider test', async (accounts) => { initTxs.claimsManagerTx.tx, ServiceProviderFactory, 'ClaimsManagerAddressUpdated', - { _newClaimsManagerAddress: claimsManagerProxy.address } + { _newClaimsManagerAddress: claimsManager.address } ) }) @@ -492,16 +496,49 @@ contract('ServiceProvider test', async (accounts) => { it('Update service provider cut', async () => { let updatedCutValue = 10 + // Permission of request to input account + await _lib.assertRevert( + serviceProviderFactory.requestUpdateDeployerCut(stakerAccount, updatedCutValue, { from: accounts[4] }), + 'Only callable by Service Provider or Governance' + ) + // Eval fails if no pending operation await _lib.assertRevert( - serviceProviderFactory.updateServiceProviderCut( + serviceProviderFactory.updateDeployerCut( stakerAccount, - updatedCutValue, { from: accounts[4] }), - 'Service Provider cut update operation restricted to deployer') - let updateTx = await serviceProviderFactory.updateServiceProviderCut( + 'No update deployer cut operation pending' + ) + + await _lib.assertRevert( + serviceProviderFactory.cancelUpdateDeployerCut(stakerAccount), + 'No update deployer cut operation pending' + ) + + let deployerCutUpdateDuration = await serviceProviderFactory.getDeployerCutLockupDuration() + let requestTx = await serviceProviderFactory.requestUpdateDeployerCut(stakerAccount, updatedCutValue, { from: stakerAccount }) + let requestBlock = _lib.toBN(requestTx.receipt.blockNumber) + + // Retrieve pending info + let pendingOp = await serviceProviderFactory.getPendingUpdateDeployerCutRequest(stakerAccount) + assert.isTrue( + (requestBlock.add(deployerCutUpdateDuration)).eq(pendingOp.lockupExpiryBlock), + 'Unexpected expiry block' + ) + + await _lib.assertRevert( + serviceProviderFactory.updateDeployerCut( + stakerAccount, + { from: stakerAccount } + ), + 'Lockup must be expired' + ) + + await time.advanceBlockTo(pendingOp.lockupExpiryBlock) + + let updateTx = await serviceProviderFactory.updateDeployerCut( stakerAccount, - updatedCutValue, - { from: stakerAccount }) + { from: stakerAccount } + ) await expectEvent.inTransaction( updateTx.tx, @@ -511,15 +548,65 @@ contract('ServiceProvider test', async (accounts) => { _updatedCut: `${updatedCutValue}` } ) - let info = await serviceProviderFactory.getServiceProviderDetails(stakerAccount) assert.isTrue((info.deployerCut).eq(_lib.toBN(updatedCutValue)), 'Expect updated cut') - let newCut = 110 + + // Reset the value for updated cut to 0 + updatedCutValue = 0 + requestTx = await serviceProviderFactory.requestUpdateDeployerCut(stakerAccount, updatedCutValue, { from: stakerAccount }) + pendingOp = await serviceProviderFactory.getPendingUpdateDeployerCutRequest(stakerAccount) + await time.advanceBlockTo(pendingOp.lockupExpiryBlock) + updateTx = await serviceProviderFactory.updateDeployerCut(stakerAccount, { from: stakerAccount }) + info = await serviceProviderFactory.getServiceProviderDetails(stakerAccount) + assert.isTrue((info.deployerCut).eq(_lib.toBN(updatedCutValue)), 'Expect updated cut') + + // Confirm cancellation works + let preUpdatecut = updatedCutValue + updatedCutValue = 10 + // Submit request + requestTx = await serviceProviderFactory.requestUpdateDeployerCut(stakerAccount, updatedCutValue, { from: stakerAccount }) + // Confirm request status + pendingOp = await serviceProviderFactory.getPendingUpdateDeployerCutRequest(stakerAccount) + assert.isTrue(pendingOp.newDeployerCut.eq(_lib.toBN(updatedCutValue)), 'Expect in flight request') + assert.isTrue(!pendingOp.lockupExpiryBlock.eq(_lib.toBN(0)), 'Expect in flight request') + // Submit cancellation + await serviceProviderFactory.cancelUpdateDeployerCut(stakerAccount, { from: stakerAccount }) + // Confirm request status + pendingOp = await serviceProviderFactory.getPendingUpdateDeployerCutRequest(stakerAccount) + assert.isTrue(pendingOp.newDeployerCut.eq(_lib.toBN(0)), 'Expect cancelled request') + assert.isTrue(pendingOp.lockupExpiryBlock.eq(_lib.toBN(0)), 'Expect cancelled request') + // Confirm no change in deployer cut + info = await serviceProviderFactory.getServiceProviderDetails(stakerAccount) + assert.isTrue((info.deployerCut).eq(_lib.toBN(preUpdatecut)), 'Expect updated cut') + + let invalidCut = 110 let base = await serviceProviderFactory.getServiceProviderDeployerCutBase() - assert.isTrue(_lib.toBN(newCut).gt(base), 'Expect invalid newCut') + assert.isTrue(_lib.toBN(invalidCut).gt(base), 'Expect invalid newCut') await _lib.assertRevert( - serviceProviderFactory.updateServiceProviderCut(stakerAccount, newCut, { from: stakerAccount }), + serviceProviderFactory.requestUpdateDeployerCut(stakerAccount, invalidCut, { from: stakerAccount }), 'Service Provider cut cannot exceed base value') + + // Set an invalid value for lockup + await _lib.assertRevert( + governance.guardianExecuteTransaction( + serviceProviderFactoryKey, + callValue, + 'updateDeployerCutLockupDuration(uint256)', + _lib.abiEncode(['uint256'], [1]), + { from: guardianAddress } + ) + ) + + let validUpdatedDuration = DEPLOYER_CUT_LOCKUP_DURATION + 1 + await governance.guardianExecuteTransaction( + serviceProviderFactoryKey, + callValue, + 'updateDeployerCutLockupDuration(uint256)', + _lib.abiEncode(['uint256'], [validUpdatedDuration]), + { from: guardianAddress } + ) + let fromChainDuration = await serviceProviderFactory.getDeployerCutLockupDuration() + assert.isTrue(fromChainDuration.eq(_lib.toBN(validUpdatedDuration)), 'Expected update') }) it('Fails to register duplicate endpoint w/same account', async () => { diff --git a/eth-contracts/utils/lib.js b/eth-contracts/utils/lib.js index 4241a1c0468..358b2c326da 100644 --- a/eth-contracts/utils/lib.js +++ b/eth-contracts/utils/lib.js @@ -123,7 +123,15 @@ export const encodeCall = (name, args, values) => { return '0x' + methodId + params } -export const registerServiceProvider = async (token, staking, serviceProviderFactory, type, endpoint, amount, account) => { +export const registerServiceProvider = async ( + token, + staking, + serviceProviderFactory, + type, + endpoint, + amount, + account +) => { // Approve staking transfer await token.approve(staking.address, amount, { from: account }) // register service provider @@ -250,6 +258,45 @@ export const deployGovernance = async ( return governance } +export const deployClaimsManager = async ( + artifacts, + registry, + governance, + proxyDeployerAddress, + guardianAddress, + tokenAddress, + fundingDiff, + registryKey +) => { + const governanceAddress = governance.address + const ClaimsManager = artifacts.require('ClaimsManager') + const AudiusAdminUpgradeabilityProxy = artifacts.require('AudiusAdminUpgradeabilityProxy') + const claimsManager0 = await ClaimsManager.new({ from: proxyDeployerAddress }) + const claimsInitializeCallData = encodeCall( + 'initialize', + ['address', 'address'], + [tokenAddress, governanceAddress] + ) + let claimsManagerProxy = await AudiusAdminUpgradeabilityProxy.new( + claimsManager0.address, + governanceAddress, + claimsInitializeCallData, + { from: proxyDeployerAddress } + ) + let claimsManager = await ClaimsManager.at(claimsManagerProxy.address) + await registry.addContract(registryKey, claimsManagerProxy.address, { from: proxyDeployerAddress }) + + // Update funding found block diff + await governance.guardianExecuteTransaction( + registryKey, + toBN(0), + 'updateFundingRoundBlockDiff(uint256)', + abiEncode(['uint256'], [fundingDiff]), + { from: guardianAddress } + ) + return claimsManager +} + export const addServiceType = async (serviceType, typeMin, typeMax, governance, guardianAddress, serviceTypeManagerRegKey) => { const addServiceTypeSignature = 'addServiceType(bytes32,uint256,uint256)' const callValue0 = toBN(0) @@ -538,10 +585,6 @@ export const configureServiceProviderFactoryAddresses = async ( { from: guardianAddress }) assert.equal(serviceTypeManagerAddress, await spFactory.getServiceTypeManagerAddress(), 'Unexpected service type manager address') - await assertRevert( - spFactory.increaseStake(100), - "claimsManagerAddress is not set" - ) let claimsManagerTx = await governance.guardianExecuteTransaction( key, toBN(0),