diff --git a/contracts/contribute/ContributeServiceManager.sol b/contracts/contribute/ContributeServiceManager.sol index fb55bf0..d899ac2 100644 --- a/contracts/contribute/ContributeServiceManager.sol +++ b/contracts/contribute/ContributeServiceManager.sol @@ -9,13 +9,24 @@ interface IContributors { /// @param socialId Social id. /// @param serviceId Service Id. /// @param multisig Service multisig address. - function setServiceInfoForId(uint256 socialId, uint256 serviceId, address multisig) external; + /// @param stakingInstance Staking instance address. + /// @param serviceOwner Service owner. + function setServiceInfoForId( + uint256 socialId, + uint256 serviceId, + address multisig, + address stakingInstance, + address serviceOwner + ) external; /// @dev Gets service info corresponding to a specified social Id. /// @param socialId Social Id. - /// @return service Id Corresponding service Id. + /// @return serviceId Corresponding service Id. /// @return multisig Corresponding service multisig. - function mapSocialIdServiceInfo(uint256 socialId) external view returns (uint256 serviceId, address multisig); + /// @return stakingInstance Staking instance address. + /// @return serviceOwner Service owner. + function mapSocialIdServiceInfo(uint256 socialId) external view + returns (uint256 serviceId, address multisig, address stakingInstance, address serviceOwner); } // Staking interface @@ -35,10 +46,30 @@ interface IStaking { /// @dev Gets the service threshold. /// @return Threshold. function threshold() external view returns (uint256); + + /// @dev Stakes the service. + /// @param serviceId Service Id. + function stake(uint256 serviceId) external; + + /// @dev Unstakes the service with collected reward, if available. + /// @param serviceId Service Id. + /// @return reward Staking reward. + function unstake(uint256 serviceId) external returns (uint256); + + /// @dev Claims rewards for the service without an additional checkpoint call. + /// @param serviceId Service Id. + /// @return Staking reward. + function claim(uint256 serviceId) external returns (uint256); } // Token interface interface IToken { + /// @dev Transfers the token amount. + /// @param to Address to transfer to. + /// @param amount The amount to transfer. + /// @return True if the function execution is successful. + function transfer(address to, uint256 amount) external returns (bool); + /// @dev Transfers the token amount that was previously approved up until the maximum allowance. /// @param from Account address to transfer from. /// @param to Account address to transfer to. @@ -59,21 +90,41 @@ error ZeroAddress(); /// @dev Zero value. error ZeroValue(); -/// @dev Service is already created for the contributor. +/// @dev Service is already created and staked for the contributor. +/// @param socialId Social Id. /// @param serviceId Service Id. /// @param multisig Multisig address. -error ServiceAlreadyCreated(uint256 serviceId, address multisig); +error ServiceAlreadyStaked(uint256 socialId, uint256 serviceId, address multisig); /// @dev Wrong staking instance. /// @param stakingInstance Staking instance address. error WrongStakingInstance(address stakingInstance); +/// @dev Service is not defined for the social Id. +/// @param socialId Social Id. +error ServiceNotDefined(uint256 socialId); + +/// @dev Wrong service owner. +/// @param serviceId Service Id. +/// @param sender Sender address. +/// @param serviceOwner Actual service owner. +error ServiceOwnerOnly(uint256 serviceId, address sender, address serviceOwner); + /// @title ContributeServiceManager - Smart contract for managing services for contributors /// @author Aleksandr Kuperman - /// @author Andrey Lebedev - /// @author Tatiana Priemova - /// @author David Vilela - contract ContributeServiceManager { + event CreatedAndStaked(uint256 indexed socialId, address indexed serviceOwner, uint256 serviceId, + address indexed multisig, address stakingInstance); + event Staked(uint256 indexed socialId, address indexed serviceOwner, uint256 serviceId, + address indexed multisig, address stakingInstance); + event Unstaked(uint256 indexed socialId, address indexed serviceOwner, uint256 serviceId, + address indexed multisig, address stakingInstance); + event Claimed(uint256 indexed socialId, address indexed serviceOwner, uint256 serviceId, + address indexed multisig, address stakingInstance); + // Contribute agent Id uint256 public constant AGENT_ID = 6; // Contributor service config hash mock @@ -82,6 +133,8 @@ contract ContributeServiceManager { address public immutable contributorsProxy; // Service manager contract address address public immutable serviceManager; + // Service registry address + address public immutable serviceRegistry; // Service registry token utility address address public immutable serviceRegistryTokenUtility; // Safe multisig processing contract address @@ -89,8 +142,11 @@ contract ContributeServiceManager { // Safe fallback handler address public immutable fallbackHandler; - /// @dev StakingNativeToken initialization. - /// @param _contributorsProxy Contributors proxy contract address. + /// @dev ContributeServiceManager constructor. + /// @param _contributorsProxy Contributors proxy address. + /// @param _serviceManager Service manager address. + /// @param _safeMultisig Safe multisig address. + /// @param _fallbackHandler Multisig fallback handler address. constructor(address _contributorsProxy, address _serviceManager, address _safeMultisig, address _fallbackHandler) { // Check the zero address if (_contributorsProxy == address(0) || _serviceManager == address(0) || _safeMultisig == address(0) || @@ -102,19 +158,52 @@ contract ContributeServiceManager { serviceManager = _serviceManager; safeMultisig = _safeMultisig; fallbackHandler = _fallbackHandler; + serviceRegistry = IService(serviceManager).serviceRegistry(); serviceRegistryTokenUtility = IService(serviceManager).serviceRegistryTokenUtility(); } + function _createAndDeploy( + address token, + uint256 minStakingDeposit, + uint256 numAgentInstances, + uint256 threshold + ) internal returns (uint256 serviceId, address multisig) { + // Set agent params + IService.AgentParams[] memory agentParams = new IService.AgentParams[](1); + agentParams[0] = IService.AgentParams(uint32(numAgentInstances), uint96(minStakingDeposit)); + + // Set agent Ids + uint32[] memory agentIds = new uint32[](1); + agentIds[0] = uint32(AGENT_ID); + + // Set agent instances as [msg.sender] + address[] memory instances = new address[](1); + instances[0] = msg.sender; + + // Create a service owned by this contract + serviceId = IService(serviceManager).create(address(this), token, CONFIG_HASH, agentIds, + agentParams, uint32(threshold)); + + // Activate registration (1 wei as a deposit wrapper) + IService(serviceManager).activateRegistration{value: 1}(serviceId); + + // Register msg.sender as an agent instance (numAgentInstances wei as a bond wrapper) + IService(serviceManager).registerAgents{value: numAgentInstances}(serviceId, instances, agentIds); + + // Deploy the service + // TODO: fix the data + multisig = IService(serviceManager).deploy(serviceId, safeMultisig, "0x"); + } + /// @dev Creates and deploys a service for the contributor, and stakes it with a specified staking contract. /// @notice The service cannot be registered again if it is currently staked. /// @param socialId Contributor social Id. /// @param stakingInstance Contribute staking instance address. function createAndStake(uint256 socialId, address stakingInstance) external payable { // Check for existing service corresponding to the social Id - (uint256 serviceId, address multisig) = IContributors(contributorsProxy).mapSocialIdServiceInfo(socialId); - + (uint256 serviceId, address multisig, , ) = IContributors(contributorsProxy).mapSocialIdServiceInfo(socialId); if (serviceId > 0) { - revert ServiceAlreadyCreated(serviceId, multisig); + revert ServiceAlreadyStaked(socialId, serviceId, multisig); } // Get the token info from the staking contract @@ -155,30 +244,88 @@ contract ContributeServiceManager { // Approve token for the serviceRegistryTokenUtility contract IToken(token).approve(serviceRegistryTokenUtility, totalBond); - // Create a service owned by this contract - serviceId = IService(serviceManager).create(address(this), token, CONFIG_HASH, [AGENT_ID], - IService.AgentParams(numAgentInstances, minStakingDeposit), threshold); + // Create and deploy service + (serviceId, multisig) = _createAndDeploy(token, minStakingDeposit, numAgentInstances, threshold); - // Activate registration (1 wei as a deposit wrapper) - IService(serviceManager).activateRegistration{value: 1}(serviceId); - - // Register msg.sender as an agent instance (numAgentInstances wei as a bond wrapper) - IService(serviceManager).registerAgents{value: numAgentInstances}(serviceId, [msg.sender], [AGENT_ID]); + // Stake the service + _stake(socialId, serviceId, multisig, stakingInstance); - // Deploy the service - // TODO: fix the data - IService(serviceManager).deploy(serviceId, safeMultisig, "0x"); + emit CreatedAndStaked(socialId, msg.sender, serviceId, multisig, stakingInstance); + } + function _stake(uint256 socialId, uint256 serviceId, address multisig, address stakingInstance) public { // Add the service into its social Id corresponding record - IContributors(contributorsProxy).setServiceInfoForId(socialId, serviceId, multisig); -// stake(stakingInstance); + IContributors(contributorsProxy).setServiceInfoForId(socialId, serviceId, multisig, stakingInstance, msg.sender); + + // Stake the service + IStaking(stakingInstance).stake(serviceId); + } + + /// @dev Stakes the already deployed service. + function stake(uint256 socialId, uint256 serviceId, address stakingInstance) public { + // Check for existing service corresponding to the social Id + (uint256 serviceIdCheck, address multisig, , ) = IContributors(contributorsProxy).mapSocialIdServiceInfo(socialId); + if (serviceIdCheck > 0) { + revert ServiceAlreadyStaked(socialId, serviceIdCheck, multisig); + } + + // Transfer the service NFT + IToken(serviceRegistry).transferFrom(msg.sender, address(this), serviceId); + + // Approve service NFT for the staking instance + IToken(serviceRegistry).approve(stakingInstance, serviceId); + + // Get the service multisig + (, multisig, , , , , ) = IService(serviceRegistry).mapServices(serviceId); + + // Stake the service + _stake(socialId, serviceId, multisig, stakingInstance); + + emit Staked(socialId, msg.sender, serviceId, multisig, stakingInstance); } - function stake(uint256 serviceId, address stakingInstance) public { + function unstake(uint256 socialId) external { + // Check for existing service corresponding to the social Id + (uint256 serviceId, address multisig, address stakingInstance, address serviceOwner) = + IContributors(contributorsProxy).mapSocialIdServiceInfo(socialId); + if (serviceId == 0) { + revert ServiceNotDefined(socialId); + } + + // Check for service owner + if (msg.sender != serviceOwner) { + revert ServiceOwnerOnly(serviceId, msg.sender, serviceOwner); + } + + // Unstake the service + IStaking(stakingInstance).unstake(serviceId); + // Transfer the service back to the original owner + IToken(serviceRegistry).transfer(serviceOwner, serviceId); + + // Zero the service info: the service is out of the contribute records, however multisig activity is still valid + // If the same service is staked back, the multisig activity continues being tracked + IContributors(contributorsProxy).setServiceInfoForId(socialId, 0, address(0), address(0), address(0)); + + emit Unstaked(socialId, msg.sender, serviceId, multisig, stakingInstance); } - function unstake() external { + function claim(uint256 socialId) external { + // Check for existing service corresponding to the social Id + (uint256 serviceId, address multisig, address stakingInstance, address serviceOwner) = + IContributors(contributorsProxy).mapSocialIdServiceInfo(socialId); + if (serviceId == 0) { + revert ServiceNotDefined(socialId); + } + + // Check for service owner + if (msg.sender != serviceOwner) { + revert ServiceOwnerOnly(serviceId, msg.sender, serviceOwner); + } + + // Claim staking rewards + IStaking(stakingInstance).claim(serviceId); + emit Claimed(socialId, msg.sender, serviceId, multisig, stakingInstance); } } \ No newline at end of file diff --git a/contracts/contribute/Contributors.sol b/contracts/contribute/Contributors.sol index 1159cbc..9a4d6ad 100644 --- a/contracts/contribute/Contributors.sol +++ b/contracts/contribute/Contributors.sol @@ -30,6 +30,10 @@ struct ServiceInfo { uint256 serviceId; // Corresponding service multisig address multisig; + // Staking instance address + address stakingInstance; + // Service owner address + address serviceOwner; } /// @title Contributors - Smart contract for managing contributors @@ -133,12 +137,20 @@ contract Contributors { manager = newManager; emit ManagerUpdated(newManager); } - + /// @dev Sets service info for the social id. /// @param socialId Social id. /// @param serviceId Service Id. /// @param multisig Service multisig address. - function setServiceInfoForId(uint256 socialId, uint256 serviceId, address multisig) external { + /// @param stakingInstance Staking instance address. + /// @param serviceOwner Service owner. + function setServiceInfoForId( + uint256 socialId, + uint256 serviceId, + address multisig, + address stakingInstance, + address serviceOwner + ) external { // Check for manager if (msg.sender != manager) { revert OnlyManager(msg.sender, manager); @@ -148,6 +160,8 @@ contract Contributors { ServiceInfo storage serviceInfo = mapSocialIdServiceInfo[socialId]; serviceInfo.serviceId = serviceId; serviceInfo.multisig = multisig; + serviceInfo.stakingInstance = stakingInstance; + serviceInfo.serviceOwner = serviceOwner; emit SetServiceInfoForId(socialId, serviceId, multisig); } diff --git a/contracts/contribute/interfaces/IService.sol b/contracts/contribute/interfaces/IService.sol index f6b4d5b..03dba4b 100644 --- a/contracts/contribute/interfaces/IService.sol +++ b/contracts/contribute/interfaces/IService.sol @@ -53,7 +53,30 @@ interface IService { bytes memory data ) external returns (address multisig); + /// @dev Gets the serviceRegistry address. + /// @return serviceRegistry address. + function serviceRegistry() external returns (address); + /// @dev Gets the serviceRegistryTokenUtility address. /// @return serviceRegistryTokenUtility address. function serviceRegistryTokenUtility() external returns (address); + + /// @dev Gets the service instance from the map of services. + /// @param serviceId Service Id. + /// @return securityDeposit Registration activation deposit. + /// @return multisig Service multisig address. + /// @return configHash IPFS hashes pointing to the config metadata. + /// @return threshold Agent instance signers threshold. + /// @return maxNumAgentInstances Total number of agent instances. + /// @return numAgentInstances Actual number of agent instances. + /// @return state Service state. + function mapServices(uint256 serviceId) external view returns ( + uint96 securityDeposit, + address multisig, + bytes32 configHash, + uint32 threshold, + uint32 maxNumAgentInstances, + uint32 numAgentInstances, + uint8 state + ); }