Skip to content
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

Merged
merged 27 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c534fc0
feat: contribute contracts stack
kupermind Oct 22, 2024
f0620e7
chore: name fix
kupermind Oct 22, 2024
badceb8
chore: removing unnecessary event
kupermind Oct 22, 2024
aa04650
feat: getting close to creating a service
kupermind Oct 22, 2024
6199f79
refactor: first implementation
kupermind Oct 22, 2024
8b52f29
chore: cleanup
kupermind Oct 22, 2024
b5b734c
refactor: polishing
kupermind Oct 22, 2024
0b24408
refactor: polishing
kupermind Oct 22, 2024
a43e44f
refactor: staking verifier
kupermind Oct 23, 2024
edf4380
refactor: service owner dependency first
kupermind Oct 23, 2024
585003f
chore: spacing
kupermind Oct 23, 2024
528f7c1
chore: adding deployment scripts
kupermind Oct 23, 2024
aaa2039
doc: v.1.4.0-internal-audit
Oct 23, 2024
63eedea
doc: v.1.4.0-internal-audit
Oct 23, 2024
bc9a4e9
refactor: NFT fix
kupermind Oct 23, 2024
3918217
chore: set initial contribute agent statuses
kupermind Oct 23, 2024
fda1740
Merge remote-tracking branch 'origin/v.1.4.0-internal-audit' into scr…
kupermind Oct 23, 2024
e5eb2de
addressing audit remarks and adding graphics
kupermind Oct 23, 2024
4dca0fd
chore: adding optimus deployment script for mode
kupermind Oct 23, 2024
80d45c2
test: add more tests
kupermind Oct 23, 2024
4783cd9
chore: missing ABIs
kupermind Oct 23, 2024
b9b5919
chore: adding ABIs
kupermind Oct 23, 2024
4c0c33d
chore: testnet re-deployment
kupermind Oct 23, 2024
b57872f
doc: update contribute flowchart
kupermind Oct 24, 2024
7bd49bb
doc: update contribute flowchart
kupermind Oct 24, 2024
011463c
test: next to full coverage
kupermind Oct 24, 2024
33044d6
Merge pull request #34 from valory-xyz/scripts_tests
DavidMinarsch Oct 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions contracts/contribute/ContributeActivityChecker.sol
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
Copy link
Collaborator Author

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.

nonces[0] = IContributors(contributorsProxy).mapMutisigActivities(multisig);
Copy link
Contributor

Choose a reason for hiding this comment

The 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);
}
}
}
286 changes: 286 additions & 0 deletions contracts/contribute/ContributeServiceManager.sol
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;
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These values don't really matter, but we have to use something.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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 for zero addresses
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,
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check if localNonce is safe as implemented

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we ensure uniqueness somehow?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 msg.sender is the actual socialId holder from the Ceramic somehow. This info is off-chain only for now.

/// @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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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);
}
}
Loading
Loading