-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: contribute contracts stack #33
Changes from 7 commits
c534fc0
f0620e7
badceb8
aa04650
6199f79
8b52f29
b5b734c
0b24408
a43e44f
edf4380
585003f
528f7c1
aaa2039
63eedea
bc9a4e9
3918217
fda1740
e5eb2de
4dca0fd
80d45c2
4783cd9
b9b5919
4c0c33d
b57872f
7bd49bb
011463c
33044d6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.28; | ||
|
||
// Contributors interface | ||
interface IContributors { | ||
function mapMutisigActivities(address multisig) external view returns (uint256); | ||
} | ||
|
||
/// @dev Zero address. | ||
error ZeroAddress(); | ||
|
||
/// @dev Zero value. | ||
error ZeroValue(); | ||
|
||
/// @title ContributeActivityChecker - Smart contract for performing contributors service staking activity check | ||
/// @author Aleksandr Kuperman - <[email protected]> | ||
/// @author Andrey Lebedev - <[email protected]> | ||
/// @author Tatiana Priemova - <[email protected]> | ||
/// @author David Vilela - <[email protected]> | ||
contract ContributeActivityChecker { | ||
// Liveness ratio in the format of 1e18 | ||
uint256 public immutable livenessRatio; | ||
// Contributors proxy contract address | ||
address public immutable contributorsProxy; | ||
|
||
/// @dev StakingNativeToken initialization. | ||
/// @param _contributorsProxy Contributors proxy contract address. | ||
/// @param _livenessRatio Liveness ratio in the format of 1e18. | ||
constructor(address _contributorsProxy, uint256 _livenessRatio) { | ||
// Check the zero address | ||
if (_contributorsProxy == address(0)) { | ||
revert ZeroAddress(); | ||
} | ||
|
||
// Check for zero value | ||
if (_livenessRatio == 0) { | ||
revert ZeroValue(); | ||
} | ||
|
||
contributorsProxy = _contributorsProxy; | ||
livenessRatio = _livenessRatio; | ||
} | ||
|
||
/// @dev Gets service multisig nonces. | ||
/// @param multisig Service multisig address. | ||
/// @return nonces Set of a single service multisig nonce. | ||
function getMultisigNonces(address multisig) external view virtual returns (uint256[] memory nonces) { | ||
nonces = new uint256[](1); | ||
// The nonces are equal to the social off-chain activity corresponding multisig activity | ||
nonces[0] = IContributors(contributorsProxy).mapMutisigActivities(multisig); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo |
||
} | ||
|
||
/// @dev Checks if the service multisig liveness ratio passes the defined liveness threshold. | ||
/// @notice The formula for calculating the ratio is the following: | ||
/// currentNonce - service multisig nonce at time now (block.timestamp); | ||
/// lastNonce - service multisig nonce at the previous checkpoint or staking time (tsStart); | ||
/// ratio = (currentNonce - lastNonce) / (block.timestamp - tsStart). | ||
/// @param curNonces Current service multisig set of a single nonce. | ||
/// @param lastNonces Last service multisig set of a single nonce. | ||
/// @param ts Time difference between current and last timestamps. | ||
/// @return ratioPass True, if the liveness ratio passes the check. | ||
function isRatioPass( | ||
uint256[] memory curNonces, | ||
uint256[] memory lastNonces, | ||
uint256 ts | ||
) external view virtual returns (bool ratioPass) { | ||
// If the checkpoint was called in the exact same block, the ratio is zero | ||
// If the current nonce is not greater than the last nonce, the ratio is zero | ||
if (ts > 0 && curNonces[0] > lastNonces[0]) { | ||
uint256 ratio = ((curNonces[0] - lastNonces[0]) * 1e18) / ts; | ||
ratioPass = (ratio >= livenessRatio); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,286 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.28; | ||
|
||
import {IContributors} from "./interfaces/IContributors.sol"; | ||
import {IService} from "./interfaces/IService.sol"; | ||
import {IStaking} from "./interfaces/IStaking.sol"; | ||
import {IToken} from "./interfaces/IToken.sol"; | ||
|
||
// Multisig interface | ||
interface IMultisig { | ||
/// @dev Returns array of owners. | ||
/// @return Array of Safe owners. | ||
function getOwners() external view returns (address[] memory); | ||
} | ||
|
||
/// @dev Zero address. | ||
error ZeroAddress(); | ||
|
||
/// @dev Zero value. | ||
error ZeroValue(); | ||
|
||
/// @dev Service is already created and staked for the contributor. | ||
/// @param socialId Social Id. | ||
/// @param serviceId Service Id. | ||
/// @param multisig Multisig address. | ||
error ServiceAlreadyStaked(uint256 socialId, uint256 serviceId, address multisig); | ||
|
||
/// @dev Wrong staking instance. | ||
/// @param stakingInstance Staking instance address. | ||
error WrongStakingInstance(address stakingInstance); | ||
|
||
/// @dev Wrong provided service setup. | ||
/// @param socialId Social Id. | ||
/// @param serviceId Service Id. | ||
/// @param multisig Multisig address. | ||
error WrongServiceSetup(uint256 socialId, uint256 serviceId, address multisig); | ||
|
||
/// @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 - <[email protected]> | ||
/// @author Andrey Lebedev - <[email protected]> | ||
/// @author Tatiana Priemova - <[email protected]> | ||
/// @author David Vilela - <[email protected]> | ||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should deploy a meaningful agent id and then use it here |
||
// Contributor service config hash mock | ||
bytes32 public constant CONFIG_HASH = 0x0000000000000000000000000000000000000000000000000000000000000006; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These values don't really matter, but we have to use something. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They do matter. We should upload a relevant config file and then take it's hash. |
||
// Contributors proxy contract address | ||
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 | ||
address public immutable safeMultisig; | ||
// Safe fallback handler | ||
address public immutable fallbackHandler; | ||
|
||
// Nonce | ||
uint256 internal nonce; | ||
|
||
/// @dev ContributeServiceManager constructor. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure it makes sense to call this ServiceManager. It's confusing because it's not the same as our other ServiceManager... |
||
/// @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) || | ||
_fallbackHandler == address(0)) { | ||
revert ZeroAddress(); | ||
} | ||
|
||
contributorsProxy = _contributorsProxy; | ||
serviceManager = _serviceManager; | ||
safeMultisig = _safeMultisig; | ||
fallbackHandler = _fallbackHandler; | ||
serviceRegistry = IService(serviceManager).serviceRegistry(); | ||
serviceRegistryTokenUtility = IService(serviceManager).serviceRegistryTokenUtility(); | ||
} | ||
|
||
/// @dev Creates and deploys a service for the contributor. | ||
/// @param token Staking token address. | ||
/// @param minStakingDeposit Min staking deposit value. | ||
/// @param numAgentInstances Number of agent instances in the service. | ||
/// @param threshold Threshold. | ||
/// @return serviceId Minted service Id. | ||
/// @return multisig Service multisig. | ||
function _createAndDeploy( | ||
address token, | ||
uint256 minStakingDeposit, | ||
uint256 numAgentInstances, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this manager is swappable by design, why do we even have numAgentInstances as an arg. we can hardcode it to 1... Same for threshold. Just introduces unnecessary config There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok, just wanted to make it a bit general case, but yeah, we can always redeploy a new one. |
||
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); | ||
|
||
// Prepare Safe multisig data | ||
uint256 localNonce = nonce; | ||
bytes memory data = abi.encodePacked(address(0), fallbackHandler, address(0), address(0), uint256(0), | ||
localNonce, "0x"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. check if localNonce is safe as implemented There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. modified |
||
// Deploy the service | ||
multisig = IService(serviceManager).deploy(serviceId, safeMultisig, data); | ||
|
||
// Update the nonce | ||
nonce = localNonce + 1; | ||
} | ||
|
||
/// @dev Stakes the already deployed service. | ||
/// @param socialId Social Id. | ||
/// @param serviceId Service Id. | ||
/// @param multisig Corresponding service multisig. | ||
/// @param stakingInstance Staking instance. | ||
function _stake(uint256 socialId, uint256 serviceId, address multisig, address stakingInstance) internal { | ||
// Add the service into its social Id corresponding record | ||
IContributors(contributorsProxy).setServiceInfoForId(socialId, serviceId, multisig, stakingInstance, msg.sender); | ||
|
||
// Approve service NFT for the staking instance | ||
IToken(serviceRegistry).approve(stakingInstance, serviceId); | ||
|
||
// Stake the service | ||
IStaking(stakingInstance).stake(serviceId); | ||
} | ||
|
||
/// @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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we ensure uniqueness somehow? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Working on it now, what's more concerning is that we need to have a proof the |
||
/// @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); | ||
if (serviceId > 0) { | ||
revert ServiceAlreadyStaked(socialId, serviceId, multisig); | ||
} | ||
|
||
// Get the token info from the staking contract | ||
// If this call fails, it means the staking contract does not have a token and is not compatible | ||
address token = IStaking(stakingInstance).stakingToken(); | ||
|
||
// Get other service info for staking | ||
uint256 minStakingDeposit = IStaking(stakingInstance).minStakingDeposit(); | ||
uint256 numAgentInstances = IStaking(stakingInstance).numAgentInstances(); | ||
uint256 threshold = IStaking(stakingInstance).threshold(); | ||
// Check for number of agent instances that must be equal to one, | ||
// since msg.sender is the only service multisig owner | ||
if (numAgentInstances != 1 || threshold != 1) { | ||
revert WrongStakingInstance(stakingInstance); | ||
} | ||
|
||
// Calculate the total bond required for the service deployment: | ||
uint256 totalBond = (1 + numAgentInstances) * minStakingDeposit; | ||
|
||
// Transfer the total bond amount from the contributor | ||
IToken(token).transferFrom(msg.sender, address(this), totalBond); | ||
// Approve token for the serviceRegistryTokenUtility contract | ||
IToken(token).approve(serviceRegistryTokenUtility, totalBond); | ||
|
||
// Create and deploy service | ||
(serviceId, multisig) = _createAndDeploy(token, minStakingDeposit, numAgentInstances, threshold); | ||
|
||
// Stake the service | ||
_stake(socialId, serviceId, multisig, stakingInstance); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. on the staking contract side we should ensure the configs are tight so that any wrong staking contract instance usage reverts here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good point, let's use the verifier |
||
|
||
emit CreatedAndStaked(socialId, msg.sender, serviceId, multisig, stakingInstance); | ||
} | ||
|
||
/// @dev Stakes the already deployed service. | ||
/// @param socialId Social Id. | ||
/// @param serviceId Service Id. | ||
/// @param stakingInstance Staking instance. | ||
function stake(uint256 socialId, uint256 serviceId, address stakingInstance) external { | ||
// 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); | ||
} | ||
|
||
// Get the service multisig | ||
(, multisig, , , , , ) = IService(serviceRegistry).mapServices(serviceId); | ||
|
||
// Check that the service multisig owner is msg.sender | ||
uint256 numAgentInstances = IStaking(stakingInstance).numAgentInstances(); | ||
address[] memory multisigOwners = IMultisig(multisig).getOwners(); | ||
if (multisigOwners.length != numAgentInstances || multisigOwners[0] != msg.sender) { | ||
revert WrongServiceSetup(socialId, serviceId, multisig); | ||
} | ||
|
||
// Transfer the service NFT | ||
IToken(serviceRegistry).transferFrom(msg.sender, address(this), serviceId); | ||
|
||
// Stake the service | ||
_stake(socialId, serviceId, multisig, stakingInstance); | ||
|
||
emit Staked(socialId, msg.sender, serviceId, multisig, stakingInstance); | ||
} | ||
|
||
/// @dev Unstakes service Id corresponding to the social Id and clears the contributor record. | ||
/// @param socialId Social Id. | ||
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(msg.sender, 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); | ||
} | ||
|
||
/// @dev Claims rewards for the service. | ||
/// @param socialId Social Id. | ||
/// @return reward Staking reward. | ||
function claim(uint256 socialId) external returns (uint256 reward) { | ||
// 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 | ||
reward = IStaking(stakingInstance).claim(serviceId); | ||
|
||
emit Claimed(socialId, msg.sender, serviceId, multisig, stakingInstance); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the main idea of contributors activity checking. This can be then modified / extended.