Transaction Hash:
Block:
21994255 at Mar-07-2025 09:58:11 AM +UTC
Transaction Fee:
0.001269971762888391 ETH
$3.19
Gas Used:
415,169 Gas / 3.058927239 Gwei
Emitted Events:
68 |
GnosisSafeProxy.0x141df868a6331af528e38c83b7aa03edc19be66e37ae67f9285bf4f8e3c6a1a8( 0x141df868a6331af528e38c83b7aa03edc19be66e37ae67f9285bf4f8e3c6a1a8, 0x000000000000000000000000a6b71e26c5e0845f74c812102ca7114b6a896ab2, 0000000000000000000000000000000000000000000000000000000000000080, 0000000000000000000000000000000000000000000000000000000000000001, 0000000000000000000000000000000000000000000000000000000000000000, 000000000000000000000000f48f2b2d2a534e402487b3ee7c18c33aec0fe5e4, 0000000000000000000000000000000000000000000000000000000000000001, 000000000000000000000000398030c56cd8c046a573c1c1fd22574bf837b5ab )
|
69 |
GnosisSafeProxyFactory.ProxyCreation( proxy=GnosisSafeProxy, singleton=GnosisSafe )
|
70 |
ServiceRegistry.CreateMultisigWithAgents( serviceId=44, multisig=GnosisSafeProxy )
|
71 |
ServiceRegistry.DeployService( serviceId=44 )
|
72 |
ServiceManagerToken.CreateMultisig( multisig=GnosisSafeProxy )
|
Account State Difference:
Address | Before | After | State Difference | ||
---|---|---|---|---|---|
0x353c5165...E72aFc15b |
0.003092011384433071 Eth
Nonce: 9
|
0.00182203962154468 Eth
Nonce: 10
| 0.001269971762888391 | ||
0x4838B106...B0BAD5f97
Miner
| (Titan Builder) | 13.71116928248895757 Eth | 13.71220720498895757 Eth | 0.0010379225 | |
0x48b6af7B...f54e775cA | |||||
0x692b53aF...B68F3AC86 |
0 Eth
Nonce: 0
|
0 Eth
Nonce: 1
| |||
0xa6B71E26...b6a896AB2 | (Safe: Proxy Factory 1.3.0 Default) |
Execution Trace
ServiceManagerToken.deploy( serviceId=44, multisigImplementation=0x46C0D07F55d4F9B5Eed2Fc9680B5953e5fd7b461, data=0x0000000000000000000000000000000000000000F48F2B2D2A534E402487B3EE7C18C33AEC0FE5E40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000067CAC2EC ) => ( multisig=0x692b53aF59428c396F4Dfd831edA603B68F3AC86 )

ServiceRegistry.deploy( serviceOwner=0x353c516564CA891B5db47693a3796CCE72aFc15b, serviceId=44, multisigImplementation=0x46C0D07F55d4F9B5Eed2Fc9680B5953e5fd7b461, data=0x0000000000000000000000000000000000000000F48F2B2D2A534E402487B3EE7C18C33AEC0FE5E40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000067CAC2EC ) => ( multisig=0x692b53aF59428c396F4Dfd831edA603B68F3AC86 )
GnosisSafeMultisig.create( owners=[0x398030C56cD8C046a573c1C1FD22574BF837B5ab], threshold=1, data=0x0000000000000000000000000000000000000000F48F2B2D2A534E402487B3EE7C18C33AEC0FE5E40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000067CAC2EC ) => ( multisig=0x692b53aF59428c396F4Dfd831edA603B68F3AC86 )
GnosisSafeProxyFactory.createProxyWithNonce( _singleton=0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552, initializer=0xB63E800D0000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000F48F2B2D2A534E402487B3EE7C18C33AEC0FE5E40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000398030C56CD8C046A573C1C1FD22574BF837B5AB0000000000000000000000000000000000000000000000000000000000000000, saltNonce=1741341420 ) => ( proxy=0x692b53aF59428c396F4Dfd831edA603B68F3AC86 )
-
GnosisSafeProxy.60806040( )
GnosisSafeProxy.b63e800d( )
-
GnosisSafe.setup( _owners=[0x398030C56cD8C046a573c1C1FD22574BF837B5ab], _threshold=1, to=0x0000000000000000000000000000000000000000, data=0x, fallbackHandler=0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4, paymentToken=0x0000000000000000000000000000000000000000, payment=0, paymentReceiver=0x0000000000000000000000000000000000000000 )
-
-
-
AgentRegistry.calculateSubComponents( unitIds=[49] ) => ( subComponentIds=[1] )
deploy[ServiceManagerToken (ln:490)]
deploy[ServiceManagerToken (ln:496)]
CreateMultisig[ServiceManagerToken (ln:497)]
File 1 of 7: ServiceManagerToken
File 2 of 7: GnosisSafeProxy
File 3 of 7: GnosisSafeProxyFactory
File 4 of 7: ServiceRegistry
File 5 of 7: GnosisSafeMultisig
File 6 of 7: GnosisSafe
File 7 of 7: AgentRegistry
// SPDX-License-Identifier: MIT pragma solidity ^0.8.15; import "./interfaces/IErrorsRegistries.sol"; /// @title Generic Manager - Smart contract for generic registry manager template /// @author Aleksandr Kuperman - <[email protected]> abstract contract GenericManager is IErrorsRegistries { event OwnerUpdated(address indexed owner); event Pause(address indexed owner); event Unpause(address indexed owner); // Owner address address public owner; // Pause switch bool public paused; /// @dev Changes the owner address. /// @param newOwner Address of a new owner. function changeOwner(address newOwner) external virtual { // Check for the ownership if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } // Check for the zero address if (newOwner == address(0)) { revert ZeroAddress(); } owner = newOwner; emit OwnerUpdated(newOwner); } /// @dev Pauses the contract. function pause() external virtual { // Check for the ownership if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } paused = true; emit Pause(msg.sender); } /// @dev Unpauses the contract. function unpause() external virtual { // Check for the ownership if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } paused = false; emit Unpause(msg.sender); } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; /// @dev Errors. interface IErrorsRegistries { /// @dev Only `manager` has a privilege, but the `sender` was provided. /// @param sender Sender address. /// @param manager Required sender address as a manager. error ManagerOnly(address sender, address manager); /// @dev Only `owner` has a privilege, but the `sender` was provided. /// @param sender Sender address. /// @param owner Required sender address as an owner. error OwnerOnly(address sender, address owner); /// @dev Hash already exists in the records. error HashExists(); /// @dev Provided zero address. error ZeroAddress(); /// @dev Agent Id is not correctly provided for the current routine. /// @param agentId Component Id. error WrongAgentId(uint256 agentId); /// @dev Wrong length of two arrays. /// @param numValues1 Number of values in a first array. /// @param numValues2 Numberf of values in a second array. error WrongArrayLength(uint256 numValues1, uint256 numValues2); /// @dev Canonical agent Id is not found. /// @param agentId Canonical agent Id. error AgentNotFound(uint256 agentId); /// @dev Component Id is not found. /// @param componentId Component Id. error ComponentNotFound(uint256 componentId); /// @dev Multisig threshold is out of bounds. /// @param currentThreshold Current threshold value. /// @param minThreshold Minimum possible threshold value. /// @param maxThreshold Maximum possible threshold value. error WrongThreshold(uint256 currentThreshold, uint256 minThreshold, uint256 maxThreshold); /// @dev Agent instance is already registered with a specified `operator`. /// @param operator Operator that registered an instance. error AgentInstanceRegistered(address operator); /// @dev Wrong operator is specified when interacting with a specified `serviceId`. /// @param serviceId Service Id. error WrongOperator(uint256 serviceId); /// @dev Operator has no registered instances in the service. /// @param operator Operator address. /// @param serviceId Service Id. error OperatorHasNoInstances(address operator, uint256 serviceId); /// @dev Canonical `agentId` is not found as a part of `serviceId`. /// @param agentId Canonical agent Id. /// @param serviceId Service Id. error AgentNotInService(uint256 agentId, uint256 serviceId); /// @dev The contract is paused. error Paused(); /// @dev Zero value when it has to be different from zero. error ZeroValue(); /// @dev Value overflow. /// @param provided Overflow value. /// @param max Maximum possible value. error Overflow(uint256 provided, uint256 max); /// @dev Service must be inactive. /// @param serviceId Service Id. error ServiceMustBeInactive(uint256 serviceId); /// @dev All the agent instance slots for a specific `serviceId` are filled. /// @param serviceId Service Id. error AgentInstancesSlotsFilled(uint256 serviceId); /// @dev Wrong state of a service. /// @param state Service state. /// @param serviceId Service Id. error WrongServiceState(uint256 state, uint256 serviceId); /// @dev Only own service multisig is allowed. /// @param provided Provided address. /// @param expected Expected multisig address. /// @param serviceId Service Id. error OnlyOwnServiceMultisig(address provided, address expected, uint256 serviceId); /// @dev Multisig is not whitelisted. /// @param multisig Address of a multisig implementation. error UnauthorizedMultisig(address multisig); /// @dev Incorrect deposit provided for the registration activation. /// @param sent Sent amount. /// @param expected Expected amount. /// @param serviceId Service Id. error IncorrectRegistrationDepositValue(uint256 sent, uint256 expected, uint256 serviceId); /// @dev Insufficient value provided for the agent instance bonding. /// @param sent Sent amount. /// @param expected Expected amount. /// @param serviceId Service Id. error IncorrectAgentBondingValue(uint256 sent, uint256 expected, uint256 serviceId); /// @dev Failure of a transfer. /// @param token Address of a token. /// @param from Address `from`. /// @param to Address `to`. /// @param value Value. error TransferFailed(address token, address from, address to, uint256 value); /// @dev Caught reentrancy violation. error ReentrancyGuard(); } // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; /// @dev Required interface for the service manipulation. interface IService{ struct AgentParams { // Number of agent instances uint32 slots; // Bond per agent instance uint96 bond; } /// @dev Creates a new service. /// @param serviceOwner Individual that creates and controls a service. /// @param configHash IPFS hash pointing to the config metadata. /// @param agentIds Canonical agent Ids in a sorted ascending order. /// @param agentParams Number of agent instances and required bond to register an instance in the service. /// @param threshold Signers threshold for a multisig composed by agent instances. /// @return serviceId Created service Id. function create( address serviceOwner, bytes32 configHash, uint32[] memory agentIds, AgentParams[] memory agentParams, uint32 threshold ) external returns (uint256 serviceId); /// @dev Updates a service in a CRUD way. /// @param serviceOwner Individual that creates and controls a service. /// @param configHash IPFS hash pointing to the config metadata. /// @param agentIds Canonical agent Ids in a sorted ascending order. /// @param agentParams Number of agent instances and required bond to register an instance in the service. /// @param threshold Signers threshold for a multisig composed by agent instances. /// @param serviceId Service Id to be updated. /// @return success True, if function executed successfully. function update( address serviceOwner, bytes32 configHash, uint32[] memory agentIds, AgentParams[] memory agentParams, uint32 threshold, uint256 serviceId ) external returns (bool success); /// @dev Activates the service. /// @param serviceOwner Individual that creates and controls a service. /// @param serviceId Correspondent service Id. /// @return success True, if function executed successfully. function activateRegistration(address serviceOwner, uint256 serviceId) external payable returns (bool success); /// @dev Registers agent instances. /// @param operator Address of the operator. /// @param serviceId Service Id to be updated. /// @param agentInstances Agent instance addresses. /// @param agentIds Canonical Ids of the agent correspondent to the agent instance. /// @return success True, if function executed successfully. function registerAgents( address operator, uint256 serviceId, address[] memory agentInstances, uint32[] memory agentIds ) external payable returns (bool success); /// @dev Creates multisig instance controlled by the set of service agent instances and deploys the service. /// @param serviceOwner Individual that creates and controls a service. /// @param serviceId Correspondent service Id. /// @param multisigImplementation Multisig implementation address. /// @param data Data payload for the multisig creation. /// @return multisig Address of the created multisig. function deploy( address serviceOwner, uint256 serviceId, address multisigImplementation, bytes memory data ) external returns (address multisig); /// @dev Terminates the service. /// @param serviceOwner Owner of the service. /// @param serviceId Service Id to be updated. /// @return success True, if function executed successfully. /// @return refund Refund to return to the serviceOwner. function terminate(address serviceOwner, uint256 serviceId) external returns (bool success, uint256 refund); /// @dev Unbonds agent instances of the operator from the service. /// @param operator Operator of agent instances. /// @param serviceId Service Id. /// @return success True, if function executed successfully. /// @return refund The amount of refund returned to the operator. function unbond(address operator, uint256 serviceId) external returns (bool success, uint256 refund); } // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; /// @dev Interface for the service registration token utility manipulation. interface IServiceTokenUtility { /// @dev Creates a record with the token-related information for the specified service. /// @param serviceId Service Id. /// @param token Token address. /// @param agentIds Set of agent Ids. /// @param bonds Set of correspondent bonds. function createWithToken( uint256 serviceId, address token, uint32[] memory agentIds, uint256[] memory bonds ) external; /// @dev Resets a record with token and security deposit data. /// @param serviceId Service Id. function resetServiceToken(uint256 serviceId) external; /// @dev Deposit a token security deposit for the service registration after its activation. /// @param serviceId Service Id. /// @return isTokenSecured True if the service Id is token secured, false if ETH secured otherwise. function activateRegistrationTokenDeposit(uint256 serviceId) external returns (bool isTokenSecured); /// @dev Deposits bonded tokens from the operator during the agent instance registration. /// @param operator Operator address. /// @param serviceId Service Id. /// @param agentIds Set of agent Ids for corresponding agent instances opertor is registering. /// @return isTokenSecured True if the service Id is token secured, false if ETH secured otherwise. function registerAgentsTokenDeposit( address operator, uint256 serviceId, uint32[] memory agentIds ) external returns (bool isTokenSecured); /// @dev Withdraws a token security deposit to the service owner after the service termination. /// @param serviceId Service Id. /// @return securityRefund Returned token security deposit, or zero if the service is ETH-secured. function terminateTokenRefund(uint256 serviceId) external returns (uint256 securityRefund); /// @dev Withdraws bonded tokens to the operator during the unbond phase. /// @param operator Operator address. /// @param serviceId Service Id. /// @return refund Returned bonded token amount, or zero if the service is ETH-secured. function unbondTokenRefund(address operator, uint256 serviceId) external returns (uint256 refund); /// @dev Gets service token secured status. /// @param serviceId Service Id. /// @return True if the service Id is token secured. function isTokenSecuredService(uint256 serviceId) external view returns (bool); } // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import {GenericManager} from "./GenericManager.sol"; import {OperatorSignedHashes} from "./utils/OperatorSignedHashes.sol"; import "./interfaces/IService.sol"; import "./interfaces/IServiceTokenUtility.sol"; // Operator whitelist interface interface IOperatorWhitelist { /// @dev Gets operator whitelisting status. /// @param serviceId Service Id. /// @param operator Operator address. /// @return status Whitelisting status. function isOperatorWhitelisted(uint256 serviceId, address operator) external view returns (bool status); } // Generic token interface interface IToken { /// @dev Gets the owner of the token Id. /// @param tokenId Token Id. /// @return Token Id owner address. function ownerOf(uint256 tokenId) external view returns (address); } /// @title Service Manager - Periphery smart contract for managing services with custom ERC20 tokens or ETH /// @author Aleksandr Kuperman - <[email protected]> /// @author AL contract ServiceManagerToken is GenericManager, OperatorSignedHashes { event OperatorWhitelistUpdated(address indexed operatorWhitelist); event CreateMultisig(address indexed multisig); // Service Registry address address public immutable serviceRegistry; // Service Registry Token Utility address address public immutable serviceRegistryTokenUtility; // A well-known representation of ETH as an address address public constant ETH_TOKEN_ADDRESS = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); // Bond wrapping constant uint96 public constant BOND_WRAPPER = 1; // Operator whitelist address address public operatorWhitelist; /// @dev ServiceRegistryTokenUtility constructor. /// @param _serviceRegistry Service Registry contract address. /// @param _serviceRegistryTokenUtility Service Registry Token Utility contract address. constructor(address _serviceRegistry, address _serviceRegistryTokenUtility, address _operatorWhitelist) OperatorSignedHashes("Service Manager Token", "1.1.1") { // Check for the Service Registry related contract zero addresses if (_serviceRegistry == address(0) || _serviceRegistryTokenUtility == address(0)) { revert ZeroAddress(); } serviceRegistry = _serviceRegistry; serviceRegistryTokenUtility = _serviceRegistryTokenUtility; operatorWhitelist = _operatorWhitelist; owner = msg.sender; } /// @dev Sets the operator whitelist contract address. /// @param newOperatorWhitelist New operator whitelist contract address. function setOperatorWhitelist(address newOperatorWhitelist) external { // Check for the contract ownership if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } operatorWhitelist = newOperatorWhitelist; emit OperatorWhitelistUpdated(newOperatorWhitelist); } /// @dev Creates a new service. /// @param serviceOwner Individual that creates and controls a service. /// @param token ERC20 token address for the security deposit, or ETH. /// @param configHash IPFS hash pointing to the config metadata. /// @param agentIds Canonical agent Ids. /// @param agentParams Number of agent instances and required bond to register an instance in the service. /// @param threshold Threshold for a multisig composed by agents. /// @return serviceId Created service Id. function create( address serviceOwner, address token, bytes32 configHash, uint32[] memory agentIds, IService.AgentParams[] memory agentParams, uint32 threshold ) external returns (uint256 serviceId) { // Check if the minting is paused if (paused) { revert Paused(); } // Check for the zero address if (token == address(0)) { revert ZeroAddress(); } // Check for the custom ERC20 token or ETH based bond if (token == ETH_TOKEN_ADDRESS) { // Call the original ServiceRegistry contract function serviceId = IService(serviceRegistry).create(serviceOwner, configHash, agentIds, agentParams, threshold); } else { // Wrap agent params with just 1 WEI bond going to the original ServiceRegistry contract, // and actual token bonds being recorded with the ServiceRegistryTokenUtility contract uint256 numAgents = agentParams.length; uint256[] memory bonds = new uint256[](numAgents); for (uint256 i = 0; i < numAgents; ++i) { // Check for the zero bond value if (agentParams[i].bond == 0) { revert ZeroValue(); } // Copy actual bond values for each agent Id bonds[i] = agentParams[i].bond; // Wrap bonds with the BOND_WRAPPER value for the original ServiceRegistry contract agentParams[i].bond = BOND_WRAPPER; } // Call the original ServiceRegistry contract function serviceId = IService(serviceRegistry).create(serviceOwner, configHash, agentIds, agentParams, threshold); // Create a token-related record for the service IServiceTokenUtility(serviceRegistryTokenUtility).createWithToken(serviceId, token, agentIds, bonds); } } /// @dev Updates a service in a CRUD way. /// @param token ERC20 token address for the security deposit, or ETH. /// @param configHash IPFS hash pointing to the config metadata. /// @param agentIds Canonical agent Ids. /// @param agentParams Number of agent instances and required bond to register an instance in the service. /// @param threshold Threshold for a multisig composed by agents. /// @param serviceId Service Id to be updated. /// @return success True, if function executed successfully. function update( address token, bytes32 configHash, uint32[] memory agentIds, IService.AgentParams[] memory agentParams, uint32 threshold, uint256 serviceId ) external returns (bool success) { // Check for the zero address if (token == address(0)) { revert ZeroAddress(); } uint256 numAgents = agentParams.length; if (token == ETH_TOKEN_ADDRESS) { // If any of the slots is a non-zero, the correspondent bond cannot be zero for (uint256 i = 0; i < numAgents; ++i) { // Check for the zero bond value if (agentParams[i].slots > 0 && agentParams[i].bond == 0) { revert ZeroValue(); } } // Call the original ServiceRegistry contract function success = IService(serviceRegistry).update(msg.sender, configHash, agentIds, agentParams, threshold, serviceId); // Reset the service token-based data // This function still needs to be called as the previous token could be a custom ERC20 token IServiceTokenUtility(serviceRegistryTokenUtility).resetServiceToken(serviceId); } else { // Wrap agent params with just 1 WEI bond going to the original ServiceRegistry contract, // and actual token bonds being recorded with the ServiceRegistryTokenUtility contract uint256[] memory bonds = new uint256[](numAgents); for (uint256 i = 0; i < numAgents; ++i) { // Copy actual bond values for each agent Id that has at least one slot in the updated service if (agentParams[i].slots > 0) { // Check for the zero bond value if (agentParams[i].bond == 0) { revert ZeroValue(); } bonds[i] = agentParams[i].bond; // Wrap bonds with the BOND_WRAPPER value for the original ServiceRegistry contract agentParams[i].bond = BOND_WRAPPER; } } // Call the original ServiceRegistry contract function success = IService(serviceRegistry).update(msg.sender, configHash, agentIds, agentParams, threshold, serviceId); // Update relevant data in the ServiceRegistryTokenUtility contract // We follow the optimistic design where existing bonds are just overwritten without a clearing // bond values of agent Ids that are not going to be used in the service. This is coming from the fact // that all the checks are done on the original ServiceRegistry side IServiceTokenUtility(serviceRegistryTokenUtility).createWithToken(serviceId, token, agentIds, bonds); } } /// @dev Activates the service and its sensitive components. /// @param serviceId Correspondent service Id. /// @return success True, if function executed successfully. function activateRegistration(uint256 serviceId) external payable returns (bool success) { // Record the actual ERC20 security deposit bool isTokenSecured = IServiceTokenUtility(serviceRegistryTokenUtility).activateRegistrationTokenDeposit(serviceId); // Activate registration in the original ServiceRegistry contract if (isTokenSecured) { // If the service Id is based on the ERC20 token, the provided value to the standard registration is 1 success = IService(serviceRegistry).activateRegistration{value: BOND_WRAPPER}(msg.sender, serviceId); } else { // Otherwise follow the standard msg.value path success = IService(serviceRegistry).activateRegistration{value: msg.value}(msg.sender, serviceId); } } /// @dev Registers agent instances. /// @param serviceId Service Id to be updated. /// @param agentInstances Agent instance addresses. /// @param agentIds Canonical Ids of the agent correspondent to the agent instance. /// @return success True, if function executed successfully. function registerAgents( uint256 serviceId, address[] memory agentInstances, uint32[] memory agentIds ) external payable returns (bool success) { if (operatorWhitelist != address(0)) { // Check if the operator is whitelisted if (!IOperatorWhitelist(operatorWhitelist).isOperatorWhitelisted(serviceId, msg.sender)) { revert WrongOperator(serviceId); } } // Record the actual ERC20 bond bool isTokenSecured = IServiceTokenUtility(serviceRegistryTokenUtility).registerAgentsTokenDeposit(msg.sender, serviceId, agentIds); // Register agent instances in a main ServiceRegistry contract if (isTokenSecured) { // If the service Id is based on the ERC20 token, the provided value to the standard registration is 1 // multiplied by the number of agent instances success = IService(serviceRegistry).registerAgents{value: agentInstances.length * BOND_WRAPPER}(msg.sender, serviceId, agentInstances, agentIds); } else { // Otherwise follow the standard msg.value path success = IService(serviceRegistry).registerAgents{value: msg.value}(msg.sender, serviceId, agentInstances, agentIds); } } /// @dev Creates multisig instance controlled by the set of service agent instances and deploys the service. /// @param serviceId Correspondent service Id. /// @param multisigImplementation Multisig implementation address. /// @param data Data payload for the multisig creation. /// @return multisig Address of the created multisig. function deploy( uint256 serviceId, address multisigImplementation, bytes memory data ) external returns (address multisig) { multisig = IService(serviceRegistry).deploy(msg.sender, serviceId, multisigImplementation, data); emit CreateMultisig(multisig); } /// @dev Terminates the service. /// @param serviceId Service Id. /// @return success True, if function executed successfully. /// @return refund Refund for the service owner. function terminate(uint256 serviceId) external returns (bool success, uint256 refund) { // Withdraw the ERC20 token if the service is token-based uint256 tokenRefund = IServiceTokenUtility(serviceRegistryTokenUtility).terminateTokenRefund(serviceId); // Terminate the service with the regular service registry routine (success, refund) = IService(serviceRegistry).terminate(msg.sender, serviceId); // If the service is token-based, the actual refund is provided via the serviceRegistryTokenUtility contract if (tokenRefund > 0) { refund = tokenRefund; } } /// @dev Unbonds agent instances of the operator from the service. /// @param serviceId Service Id. /// @return success True, if function executed successfully. /// @return refund The amount of refund returned to the operator. function unbond(uint256 serviceId) external returns (bool success, uint256 refund) { // Withdraw the ERC20 token if the service is token-based uint256 tokenRefund = IServiceTokenUtility(serviceRegistryTokenUtility).unbondTokenRefund(msg.sender, serviceId); // Unbond with the regular service registry routine (success, refund) = IService(serviceRegistry).unbond(msg.sender, serviceId); // If the service is token-based, the actual refund is provided via the serviceRegistryTokenUtility contract if (tokenRefund > 0) { refund = tokenRefund; } } /// @dev Unbonds agent instances of the operator by the service owner via the operator's pre-signed message hash. /// @notice Note that this function accounts for the operator being the EOA, or the contract that has an /// isValidSignature() function that would confirm the message hash was signed by the operator contract. /// Otherwise, if the message hash has been pre-approved, the corresponding map of hashes is going to /// to verify the signed hash, similar to the Safe contract implementation in v1.3.0: /// https://github.com/safe-global/safe-contracts/blob/186a21a74b327f17fc41217a927dea7064f74604/contracts/GnosisSafe.sol#L240-L304 /// Also note that only the service owner is able to call this function on behalf of the operator. /// @param operator Operator address that signed the unbond message hash. /// @param serviceId Service Id. /// @param signature Signature byte array associated with operator message hash signature. /// @return success True, if the function executed successfully. /// @return refund The amount of refund returned to the operator. function unbondWithSignature( address operator, uint256 serviceId, bytes memory signature ) external returns (bool success, uint256 refund) { // Check the service owner address serviceOwner = IToken(serviceRegistry).ownerOf(serviceId); if (msg.sender != serviceOwner) { revert OwnerOnly(msg.sender, serviceOwner); } // Get the (operator | serviceId) nonce for the unbond message // Push a pair of key defining variables into one key. Service Id or operator are not enough by themselves // as another service might use the operator address at the same time frame // operator occupies first 160 bits uint256 operatorService = uint256(uint160(operator)); // serviceId occupies next 32 bits operatorService |= serviceId << 160; uint256 nonce = mapOperatorUnbondNonces[operatorService]; // Get the unbond message hash bytes32 msgHash = getUnbondHash(operator, serviceOwner, serviceId, nonce); // Verify the signed hash against the operator address _verifySignedHash(operator, msgHash, signature); // Update corresponding nonce value nonce++; mapOperatorUnbondNonces[operatorService] = nonce; // Withdraw the ERC20 token if the service is token-based uint256 tokenRefund = IServiceTokenUtility(serviceRegistryTokenUtility).unbondTokenRefund(operator, serviceId); // Unbond with the regular service registry routine (success, refund) = IService(serviceRegistry).unbond(operator, serviceId); // If the service is token-based, the actual refund is provided via the serviceRegistryTokenUtility contract if (tokenRefund > 0) { refund = tokenRefund; } } /// @dev Registers agent instances of the operator by the service owner via the operator's pre-signed message hash. /// @notice Note that this function accounts for the operator being the EOA, or the contract that has an /// isValidSignature() function that would confirm the message hash was signed by the operator contract. /// Otherwise, if the message hash has been pre-approved, the corresponding map of hashes is going to /// to verify the signed hash, similar to the Safe contract implementation in v1.3.0: /// https://github.com/safe-global/safe-contracts/blob/186a21a74b327f17fc41217a927dea7064f74604/contracts/GnosisSafe.sol#L240-L304 /// Also note that only the service owner is able to call this function on behalf of the operator. /// @param operator Operator address that signed the register agents message hash. /// @param serviceId Service Id. /// @param agentInstances Agent instance addresses. /// @param agentIds Canonical Ids of the agent correspondent to the agent instance. /// @param signature Signature byte array associated with operator message hash signature. /// @return success True, if the the function executed successfully. function registerAgentsWithSignature( address operator, uint256 serviceId, address[] memory agentInstances, uint32[] memory agentIds, bytes memory signature ) external payable returns (bool success) { // Check the service owner address serviceOwner = IToken(serviceRegistry).ownerOf(serviceId); if (msg.sender != serviceOwner) { revert OwnerOnly(msg.sender, serviceOwner); } // Get the (operator | serviceId) nonce for the registerAgents message // Push a pair of key defining variables into one key. Service Id or operator are not enough by themselves // as another service might use the operator address at the same time frame // operator occupies first 160 bits uint256 operatorService = uint256(uint160(operator)); // serviceId occupies next 32 bits as serviceId is limited by the 2^32 - 1 value operatorService |= serviceId << 160; uint256 nonce = mapOperatorRegisterAgentsNonces[operatorService]; // Get register agents message hash bytes32 msgHash = getRegisterAgentsHash(operator, serviceOwner, serviceId, agentInstances, agentIds, nonce); // Verify the signed hash against the operator address _verifySignedHash(operator, msgHash, signature); // Update corresponding nonce value nonce++; mapOperatorRegisterAgentsNonces[operatorService] = nonce; // Record the actual ERC20 bond bool isTokenSecured = IServiceTokenUtility(serviceRegistryTokenUtility).registerAgentsTokenDeposit(operator, serviceId, agentIds); // Register agent instances in a main ServiceRegistry contract if (isTokenSecured) { // If the service Id is based on the ERC20 token, the provided value to the standard registration is 1 // multiplied by the number of agent instances success = IService(serviceRegistry).registerAgents{value: agentInstances.length * BOND_WRAPPER}(operator, serviceId, agentInstances, agentIds); } else { // Otherwise follow the standard msg.value path success = IService(serviceRegistry).registerAgents{value: msg.value}(operator, serviceId, agentInstances, agentIds); } } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; interface ISignatureValidator { /// @dev Should return whether the signature provided is valid for the provided hash. /// @notice MUST return the bytes4 magic value 0x1626ba7e when function passes. /// MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5). /// MUST allow external calls. /// @param hash Hash of the data to be signed. /// @param signature Signature byte array associated with hash. /// @return magicValue bytes4 magic value. function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue); } /// @dev Provided zero address. error ZeroOperatorAddress(); /// @dev Incorrect signature length provided. /// @param signature Signature bytes. /// @param provided Provided signature length. /// @param expected Expected signature length. error IncorrectSignatureLength(bytes signature, uint256 provided, uint256 expected); /// @dev Hash is not validated. /// @param operator Operator contract address. /// @param msgHash Message hash. /// @param signature Signature bytes associated with the message hash. error HashNotValidated(address operator, bytes32 msgHash, bytes signature); /// @dev Hash is not approved. /// @param operator Operator address. /// @param msgHash Message hash. /// @param signature Signature bytes associated with the message hash. error HashNotApproved(address operator, bytes32 msgHash, bytes signature); /// @dev Obtained wrong operator address. /// @param provided Provided address. /// @param expected Expected address. error WrongOperatorAddress(address provided, address expected); /// @title OperatorSignedHashes - Smart contract for managing operator signed hashes /// @author AL /// @author Aleksandr Kuperman - <[email protected]> contract OperatorSignedHashes { event OperatorHashApproved(address indexed operator, bytes32 hash); // Value for the contract signature validation: bytes4(keccak256("isValidSignature(bytes32,bytes)") bytes4 constant internal MAGIC_VALUE = 0x1626ba7e; // Domain separator type hash bytes32 public constant DOMAIN_SEPARATOR_TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); // Unbond type hash bytes32 public constant UNBOND_TYPE_HASH = keccak256("Unbond(address operator,address serviceOwner,uint256 serviceId,uint256 nonce)"); // Register agents type hash bytes32 public constant REGISTER_AGENTS_TYPE_HASH = keccak256("RegisterAgents(address operator,address serviceOwner,uint256 serviceId,bytes32 agentsData,uint256 nonce)"); // Original domain separator value bytes32 public immutable domainSeparator; // Original chain Id uint256 public immutable chainId; // Name hash bytes32 public immutable nameHash; // Version hash bytes32 public immutable versionHash; // Name of a signing domain string public name; // Version of a signing domain string public version; // Map of operator address and serviceId => unbond nonce mapping(uint256 => uint256) public mapOperatorUnbondNonces; // Map of operator address and serviceId => register agents nonce mapping(uint256 => uint256) public mapOperatorRegisterAgentsNonces; // Mapping operator address => approved hashes status mapping(address => mapping(bytes32 => bool)) public mapOperatorApprovedHashes; /// @dev Contract constructor. /// @param _name Name of a signing domain. /// @param _version Version of a signing domain. constructor(string memory _name, string memory _version) { name = _name; version = _version; nameHash = keccak256(bytes(_name)); versionHash = keccak256(bytes(_version)); chainId = block.chainid; domainSeparator = _computeDomainSeparator(); } /// @dev Verifies provided message hash against its signature. /// @param operator Operator address. /// @param msgHash Message hash. /// @param signature Signature bytes associated with the signed message hash. function _verifySignedHash(address operator, bytes32 msgHash, bytes memory signature) internal view { // Check for the operator zero address if (operator == address(0)) { revert ZeroOperatorAddress(); } // Check for the signature length if (signature.length != 65) { revert IncorrectSignatureLength(signature, signature.length, 65); } // Decode the signature uint8 v = uint8(signature[64]); // For the correct ecrecover() function execution, the v value must be set to {0,1} + 27 // Although v in a very rare case can be equal to {2,3} (with a probability of 3.73e-37%) // If v is set to just 0 or 1 when signing by the EOA, it is most likely signed by the ledger and must be adjusted if (v < 4 && operator.code.length == 0) { // In case of a non-contract, adjust v to follow the standard ecrecover case v += 27; } bytes32 r; bytes32 s; // solhint-disable-next-line no-inline-assembly assembly { r := mload(add(signature, 32)) s := mload(add(signature, 64)) } address recOperator; // Go through signature cases based on the value of v if (v == 4) { // Contract signature case, where the address of the contract is encoded into r recOperator = address(uint160(uint256(r))); // Check for the signature validity in the contract if (ISignatureValidator(recOperator).isValidSignature(msgHash, signature) != MAGIC_VALUE) { revert HashNotValidated(recOperator, msgHash, signature); } } else if (v == 5) { // Case of an approved hash, where the address of the operator is encoded into r recOperator = address(uint160(uint256(r))); // Hashes have been pre-approved by the operator via a separate message, see operatorApproveHash() function if (!mapOperatorApprovedHashes[recOperator][msgHash]) { revert HashNotApproved(recOperator, msgHash, signature); } } else { // Case of ecrecover with the message hash for EOA signatures recOperator = ecrecover(msgHash, v, r, s); } // Final check is for the operator address itself if (recOperator != operator) { revert WrongOperatorAddress(recOperator, operator); } } /// @dev Approves message hash for the operator address. /// @param hash Provided message hash to approve. function operatorApproveHash(bytes32 hash) external { mapOperatorApprovedHashes[msg.sender][hash] = true; emit OperatorHashApproved(msg.sender, hash); } /// @dev Computes domain separator hash. /// @return Hash of the domain separator based on its name, version, chain Id and contract address. function _computeDomainSeparator() internal view returns (bytes32) { return keccak256( abi.encode( DOMAIN_SEPARATOR_TYPE_HASH, nameHash, versionHash, block.chainid, address(this) ) ); } /// @dev Gets the already computed domain separator of recomputes one if the chain Id is different. /// @return Original or recomputed domain separator. function getDomainSeparator() public view returns (bytes32) { return block.chainid == chainId ? domainSeparator : _computeDomainSeparator(); } /// @dev Gets the unbond message hash for the operator. /// @param operator Operator address. /// @param serviceOwner Service owner address. /// @param serviceId Service Id. /// @param nonce Nonce for the unbond message from the pair of (operator | service Id). /// @return Computed message hash. function getUnbondHash( address operator, address serviceOwner, uint256 serviceId, uint256 nonce ) public view returns (bytes32) { return keccak256( abi.encodePacked( "\\x19\\x01", getDomainSeparator(), keccak256( abi.encode( UNBOND_TYPE_HASH, operator, serviceOwner, serviceId, nonce ) ) ) ); } /// @dev Gets the register agents message hash for the operator. /// @param operator Operator address. /// @param serviceOwner Service owner address. /// @param serviceId Service Id. /// @param agentInstances Agent instance addresses operator is going to register. /// @param agentIds Agent Ids corresponding to each agent instance address. /// @param nonce Nonce for the register agents message from the pair of (operator | service Id). /// @return Computed message hash. function getRegisterAgentsHash( address operator, address serviceOwner, uint256 serviceId, address[] memory agentInstances, uint32[] memory agentIds, uint256 nonce ) public view returns (bytes32) { return keccak256( abi.encodePacked( "\\x19\\x01", getDomainSeparator(), keccak256( abi.encode( REGISTER_AGENTS_TYPE_HASH, operator, serviceOwner, serviceId, keccak256(abi.encode(agentInstances, agentIds)), nonce ) ) ) ); } /// @dev Checks if the hash provided by the operator is approved. /// @param operator Operator address. /// @param hash Message hash. /// @return True, if the hash provided by the operator is approved. function isOperatorHashApproved(address operator, bytes32 hash) external view returns (bool) { return mapOperatorApprovedHashes[operator][hash]; } /// @dev Gets the (operator | service Id) nonce for the unbond message data. /// @param operator Operator address. /// @param serviceId Service Id. /// @return nonce Obtained nonce. function getOperatorUnbondNonce(address operator, uint256 serviceId) external view returns (uint256 nonce) { // operator occupies first 160 bits uint256 operatorService = uint256(uint160(operator)); // serviceId occupies next 32 bits as serviceId is limited by the 2^32 - 1 value operatorService |= serviceId << 160; nonce = mapOperatorUnbondNonces[operatorService]; } /// @dev Gets the (operator | service Id) nonce for the register agents message data. /// @param operator Operator address. /// @param serviceId Service Id. /// @return nonce Obtained nonce. function getOperatorRegisterAgentsNonce(address operator, uint256 serviceId) external view returns (uint256 nonce) { // operator occupies first 160 bits uint256 operatorService = uint256(uint160(operator)); // serviceId occupies next 32 bits as serviceId is limited by the 2^32 - 1 value operatorService |= serviceId << 160; nonce = mapOperatorRegisterAgentsNonces[operatorService]; } }
File 2 of 7: GnosisSafeProxy
// SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; /// @title IProxy - Helper interface to access masterCopy of the Proxy on-chain /// @author Richard Meissner - <[email protected]> interface IProxy { function masterCopy() external view returns (address); } /// @title GnosisSafeProxy - Generic proxy contract allows to execute all transactions applying the code of a master contract. /// @author Stefan George - <[email protected]> /// @author Richard Meissner - <[email protected]> contract GnosisSafeProxy { // singleton always needs to be first declared variable, to ensure that it is at the same location in the contracts to which calls are delegated. // To reduce deployment costs this variable is internal and needs to be retrieved via `getStorageAt` address internal singleton; /// @dev Constructor function sets address of singleton contract. /// @param _singleton Singleton address. constructor(address _singleton) { require(_singleton != address(0), "Invalid singleton address provided"); singleton = _singleton; } /// @dev Fallback function forwards all transactions and returns all received return data. fallback() external payable { // solhint-disable-next-line no-inline-assembly assembly { let _singleton := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff) // 0xa619486e == keccak("masterCopy()"). The value is right padded to 32-bytes with 0s if eq(calldataload(0), 0xa619486e00000000000000000000000000000000000000000000000000000000) { mstore(0, _singleton) return(0, 0x20) } calldatacopy(0, 0, calldatasize()) let success := delegatecall(gas(), _singleton, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) if eq(success, 0) { revert(0, returndatasize()) } return(0, returndatasize()) } } } /// @title Proxy Factory - Allows to create new proxy contact and execute a message call to the new proxy within one transaction. /// @author Stefan George - <[email protected]> contract GnosisSafeProxyFactory { event ProxyCreation(GnosisSafeProxy proxy, address singleton); /// @dev Allows to create new proxy contact and execute a message call to the new proxy within one transaction. /// @param singleton Address of singleton contract. /// @param data Payload for message call sent to new proxy contract. function createProxy(address singleton, bytes memory data) public returns (GnosisSafeProxy proxy) { proxy = new GnosisSafeProxy(singleton); if (data.length > 0) // solhint-disable-next-line no-inline-assembly assembly { if eq(call(gas(), proxy, 0, add(data, 0x20), mload(data), 0, 0), 0) { revert(0, 0) } } emit ProxyCreation(proxy, singleton); } /// @dev Allows to retrieve the runtime code of a deployed Proxy. This can be used to check that the expected Proxy was deployed. function proxyRuntimeCode() public pure returns (bytes memory) { return type(GnosisSafeProxy).runtimeCode; } /// @dev Allows to retrieve the creation code used for the Proxy deployment. With this it is easily possible to calculate predicted address. function proxyCreationCode() public pure returns (bytes memory) { return type(GnosisSafeProxy).creationCode; } /// @dev Allows to create new proxy contact using CREATE2 but it doesn't run the initializer. /// This method is only meant as an utility to be called from other methods /// @param _singleton Address of singleton contract. /// @param initializer Payload for message call sent to new proxy contract. /// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract. function deployProxyWithNonce( address _singleton, bytes memory initializer, uint256 saltNonce ) internal returns (GnosisSafeProxy proxy) { // If the initializer changes the proxy address should change too. Hashing the initializer data is cheaper than just concatinating it bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce)); bytes memory deploymentData = abi.encodePacked(type(GnosisSafeProxy).creationCode, uint256(uint160(_singleton))); // solhint-disable-next-line no-inline-assembly assembly { proxy := create2(0x0, add(0x20, deploymentData), mload(deploymentData), salt) } require(address(proxy) != address(0), "Create2 call failed"); } /// @dev Allows to create new proxy contact and execute a message call to the new proxy within one transaction. /// @param _singleton Address of singleton contract. /// @param initializer Payload for message call sent to new proxy contract. /// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract. function createProxyWithNonce( address _singleton, bytes memory initializer, uint256 saltNonce ) public returns (GnosisSafeProxy proxy) { proxy = deployProxyWithNonce(_singleton, initializer, saltNonce); if (initializer.length > 0) // solhint-disable-next-line no-inline-assembly assembly { if eq(call(gas(), proxy, 0, add(initializer, 0x20), mload(initializer), 0, 0), 0) { revert(0, 0) } } emit ProxyCreation(proxy, _singleton); } /// @dev Allows to create new proxy contact, execute a message call to the new proxy and call a specified callback within one transaction /// @param _singleton Address of singleton contract. /// @param initializer Payload for message call sent to new proxy contract. /// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract. /// @param callback Callback that will be invoced after the new proxy contract has been successfully deployed and initialized. function createProxyWithCallback( address _singleton, bytes memory initializer, uint256 saltNonce, IProxyCreationCallback callback ) public returns (GnosisSafeProxy proxy) { uint256 saltNonceWithCallback = uint256(keccak256(abi.encodePacked(saltNonce, callback))); proxy = createProxyWithNonce(_singleton, initializer, saltNonceWithCallback); if (address(callback) != address(0)) callback.proxyCreated(proxy, _singleton, initializer, saltNonce); } /// @dev Allows to get the address for a new proxy contact created via `createProxyWithNonce` /// This method is only meant for address calculation purpose when you use an initializer that would revert, /// therefore the response is returned with a revert. When calling this method set `from` to the address of the proxy factory. /// @param _singleton Address of singleton contract. /// @param initializer Payload for message call sent to new proxy contract. /// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract. function calculateCreateProxyWithNonceAddress( address _singleton, bytes calldata initializer, uint256 saltNonce ) external returns (GnosisSafeProxy proxy) { proxy = deployProxyWithNonce(_singleton, initializer, saltNonce); revert(string(abi.encodePacked(proxy))); } } interface IProxyCreationCallback { function proxyCreated( GnosisSafeProxy proxy, address _singleton, bytes calldata initializer, uint256 saltNonce ) external; }
File 3 of 7: GnosisSafeProxyFactory
// SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; /// @title IProxy - Helper interface to access masterCopy of the Proxy on-chain /// @author Richard Meissner - <[email protected]> interface IProxy { function masterCopy() external view returns (address); } /// @title GnosisSafeProxy - Generic proxy contract allows to execute all transactions applying the code of a master contract. /// @author Stefan George - <[email protected]> /// @author Richard Meissner - <[email protected]> contract GnosisSafeProxy { // singleton always needs to be first declared variable, to ensure that it is at the same location in the contracts to which calls are delegated. // To reduce deployment costs this variable is internal and needs to be retrieved via `getStorageAt` address internal singleton; /// @dev Constructor function sets address of singleton contract. /// @param _singleton Singleton address. constructor(address _singleton) { require(_singleton != address(0), "Invalid singleton address provided"); singleton = _singleton; } /// @dev Fallback function forwards all transactions and returns all received return data. fallback() external payable { // solhint-disable-next-line no-inline-assembly assembly { let _singleton := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff) // 0xa619486e == keccak("masterCopy()"). The value is right padded to 32-bytes with 0s if eq(calldataload(0), 0xa619486e00000000000000000000000000000000000000000000000000000000) { mstore(0, _singleton) return(0, 0x20) } calldatacopy(0, 0, calldatasize()) let success := delegatecall(gas(), _singleton, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) if eq(success, 0) { revert(0, returndatasize()) } return(0, returndatasize()) } } } // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; import "./GnosisSafeProxy.sol"; import "./IProxyCreationCallback.sol"; /// @title Proxy Factory - Allows to create new proxy contact and execute a message call to the new proxy within one transaction. /// @author Stefan George - <[email protected]> contract GnosisSafeProxyFactory { event ProxyCreation(GnosisSafeProxy proxy, address singleton); /// @dev Allows to create new proxy contact and execute a message call to the new proxy within one transaction. /// @param singleton Address of singleton contract. /// @param data Payload for message call sent to new proxy contract. function createProxy(address singleton, bytes memory data) public returns (GnosisSafeProxy proxy) { proxy = new GnosisSafeProxy(singleton); if (data.length > 0) // solhint-disable-next-line no-inline-assembly assembly { if eq(call(gas(), proxy, 0, add(data, 0x20), mload(data), 0, 0), 0) { revert(0, 0) } } emit ProxyCreation(proxy, singleton); } /// @dev Allows to retrieve the runtime code of a deployed Proxy. This can be used to check that the expected Proxy was deployed. function proxyRuntimeCode() public pure returns (bytes memory) { return type(GnosisSafeProxy).runtimeCode; } /// @dev Allows to retrieve the creation code used for the Proxy deployment. With this it is easily possible to calculate predicted address. function proxyCreationCode() public pure returns (bytes memory) { return type(GnosisSafeProxy).creationCode; } /// @dev Allows to create new proxy contact using CREATE2 but it doesn't run the initializer. /// This method is only meant as an utility to be called from other methods /// @param _singleton Address of singleton contract. /// @param initializer Payload for message call sent to new proxy contract. /// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract. function deployProxyWithNonce( address _singleton, bytes memory initializer, uint256 saltNonce ) internal returns (GnosisSafeProxy proxy) { // If the initializer changes the proxy address should change too. Hashing the initializer data is cheaper than just concatinating it bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce)); bytes memory deploymentData = abi.encodePacked(type(GnosisSafeProxy).creationCode, uint256(uint160(_singleton))); // solhint-disable-next-line no-inline-assembly assembly { proxy := create2(0x0, add(0x20, deploymentData), mload(deploymentData), salt) } require(address(proxy) != address(0), "Create2 call failed"); } /// @dev Allows to create new proxy contact and execute a message call to the new proxy within one transaction. /// @param _singleton Address of singleton contract. /// @param initializer Payload for message call sent to new proxy contract. /// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract. function createProxyWithNonce( address _singleton, bytes memory initializer, uint256 saltNonce ) public returns (GnosisSafeProxy proxy) { proxy = deployProxyWithNonce(_singleton, initializer, saltNonce); if (initializer.length > 0) // solhint-disable-next-line no-inline-assembly assembly { if eq(call(gas(), proxy, 0, add(initializer, 0x20), mload(initializer), 0, 0), 0) { revert(0, 0) } } emit ProxyCreation(proxy, _singleton); } /// @dev Allows to create new proxy contact, execute a message call to the new proxy and call a specified callback within one transaction /// @param _singleton Address of singleton contract. /// @param initializer Payload for message call sent to new proxy contract. /// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract. /// @param callback Callback that will be invoced after the new proxy contract has been successfully deployed and initialized. function createProxyWithCallback( address _singleton, bytes memory initializer, uint256 saltNonce, IProxyCreationCallback callback ) public returns (GnosisSafeProxy proxy) { uint256 saltNonceWithCallback = uint256(keccak256(abi.encodePacked(saltNonce, callback))); proxy = createProxyWithNonce(_singleton, initializer, saltNonceWithCallback); if (address(callback) != address(0)) callback.proxyCreated(proxy, _singleton, initializer, saltNonce); } /// @dev Allows to get the address for a new proxy contact created via `createProxyWithNonce` /// This method is only meant for address calculation purpose when you use an initializer that would revert, /// therefore the response is returned with a revert. When calling this method set `from` to the address of the proxy factory. /// @param _singleton Address of singleton contract. /// @param initializer Payload for message call sent to new proxy contract. /// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract. function calculateCreateProxyWithNonceAddress( address _singleton, bytes calldata initializer, uint256 saltNonce ) external returns (GnosisSafeProxy proxy) { proxy = deployProxyWithNonce(_singleton, initializer, saltNonce); revert(string(abi.encodePacked(proxy))); } } // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; import "./GnosisSafeProxy.sol"; interface IProxyCreationCallback { function proxyCreated( GnosisSafeProxy proxy, address _singleton, bytes calldata initializer, uint256 saltNonce ) external; }
File 4 of 7: ServiceRegistry
// SPDX-License-Identifier: MIT pragma solidity ^0.8.15; import "./GenericRegistry.sol"; import "./interfaces/IMultisig.sol"; import "./interfaces/IRegistry.sol"; // This struct is 128 bits in total struct AgentParams { // Number of agent instances. This number is limited by the number of agent instances uint32 slots; // Bond per agent instance. This is enough for 1b+ ETH or 1e27 uint96 bond; } // This struct is 192 bits in total struct AgentInstance { // Address of an agent instance address instance; // Canonical agent Id. This number is limited by the max number of agent Ids (see UnitRegistry contract) uint32 agentId; } /// @title Service Registry - Smart contract for registering services /// @author Aleksandr Kuperman - <[email protected]> contract ServiceRegistry is GenericRegistry { event DrainerUpdated(address indexed drainer); event Deposit(address indexed sender, uint256 amount); event Refund(address indexed receiver, uint256 amount); event CreateService(uint256 indexed serviceId); event UpdateService(uint256 indexed serviceId, bytes32 configHash); event RegisterInstance(address indexed operator, uint256 indexed serviceId, address indexed agentInstance, uint256 agentId); event CreateMultisigWithAgents(uint256 indexed serviceId, address indexed multisig); event ActivateRegistration(uint256 indexed serviceId); event TerminateService(uint256 indexed serviceId); event OperatorSlashed(uint256 amount, address indexed operator, uint256 indexed serviceId); event OperatorUnbond(address indexed operator, uint256 indexed serviceId); event DeployService(uint256 indexed serviceId); event Drain(address indexed drainer, uint256 amount); enum ServiceState { NonExistent, PreRegistration, ActiveRegistration, FinishedRegistration, Deployed, TerminatedBonded } // Service parameters struct Service { // Registration activation deposit // This is enough for 1b+ ETH or 1e27 uint96 securityDeposit; // Multisig address for agent instances address multisig; // IPFS hashes pointing to the config metadata bytes32 configHash; // Agent instance signers threshold: must no less than ceil((n * 2 + 1) / 3) of all the agent instances combined // This number will be enough to have ((2^32 - 1) * 3 - 1) / 2, which is bigger than 6.44b uint32 threshold; // Total number of agent instances. We assume that the number of instances is bounded by 2^32 - 1 uint32 maxNumAgentInstances; // Actual number of agent instances. This number is less or equal to maxNumAgentInstances uint32 numAgentInstances; // Service state ServiceState state; // Canonical agent Ids for the service. Individual agent Id is bounded by the max number of agent Id uint32[] agentIds; } // Agent Registry address address public immutable agentRegistry; // The amount of funds slashed. This is enough for 1b+ ETH or 1e27 uint96 public slashedFunds; // Drainer address: set by the government and is allowed to drain ETH funds accumulated in this contract address public drainer; // Service registry version number string public constant VERSION = "1.0.0"; // Map of service Id => set of IPFS hashes pointing to the config metadata mapping (uint256 => bytes32[]) public mapConfigHashes; // Map of operator address and serviceId => set of registered agent instance addresses mapping(uint256 => AgentInstance[]) public mapOperatorAndServiceIdAgentInstances; // Service Id and canonical agent Id => number of agent instances and correspondent instance registration bond mapping(uint256 => AgentParams) public mapServiceAndAgentIdAgentParams; // Actual agent instance addresses. Service Id and canonical agent Id => Set of agent instance addresses. mapping(uint256 => address[]) public mapServiceAndAgentIdAgentInstances; // Map of operator address and serviceId => agent instance bonding / escrow balance mapping(uint256 => uint96) public mapOperatorAndServiceIdOperatorBalances; // Map of agent instance address => service id it is registered with and operator address that supplied the instance mapping (address => address) public mapAgentInstanceOperators; // Map of service Id => set of unique component Ids // Updated during the service deployment via deploy() function mapping (uint256 => uint32[]) public mapServiceIdSetComponentIds; // Map of service Id => set of unique agent Ids mapping (uint256 => uint32[]) public mapServiceIdSetAgentIds; // Map of policy for multisig implementations mapping (address => bool) public mapMultisigs; // Map of service counter => service mapping (uint256 => Service) public mapServices; /// @dev Service registry constructor. /// @param _name Service contract name. /// @param _symbol Agent contract symbol. /// @param _baseURI Agent registry token base URI. /// @param _agentRegistry Agent registry address. constructor(string memory _name, string memory _symbol, string memory _baseURI, address _agentRegistry) ERC721(_name, _symbol) { baseURI = _baseURI; agentRegistry = _agentRegistry; owner = msg.sender; } /// @dev Changes the drainer. /// @param newDrainer Address of a drainer. function changeDrainer(address newDrainer) external { if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } // Check for the zero address if (newDrainer == address(0)) { revert ZeroAddress(); } drainer = newDrainer; emit DrainerUpdated(newDrainer); } /// @dev Going through basic initial service checks. /// @param configHash IPFS hash pointing to the config metadata. /// @param agentIds Canonical agent Ids. /// @param agentParams Number of agent instances and required required bond to register an instance in the service. function _initialChecks( bytes32 configHash, uint32[] memory agentIds, AgentParams[] memory agentParams ) private view { // Check for the non-zero hash value if (configHash == 0) { revert ZeroValue(); } // Checking for non-empty arrays and correct number of values in them if (agentIds.length == 0 || agentIds.length != agentParams.length) { revert WrongArrayLength(agentIds.length, agentParams.length); } // Check for duplicate canonical agent Ids uint256 agentTotalSupply = IRegistry(agentRegistry).totalSupply(); uint256 lastId; for (uint256 i = 0; i < agentIds.length; i++) { if (agentIds[i] < (lastId + 1) || agentIds[i] > agentTotalSupply) { revert WrongAgentId(agentIds[i]); } lastId = agentIds[i]; } } /// @dev Sets the service data. /// @param service A service instance to fill the data for. /// @param agentIds Canonical agent Ids. /// @param agentParams Number of agent instances and required required bond to register an instance in the service. /// @param size Size of a canonical agent ids set. /// @param serviceId ServiceId. function _setServiceData( Service memory service, uint32[] memory agentIds, AgentParams[] memory agentParams, uint256 size, uint256 serviceId ) private { // Security deposit uint96 securityDeposit; // Add canonical agent Ids for the service and the slots map service.agentIds = new uint32[](size); for (uint256 i = 0; i < size; i++) { service.agentIds[i] = agentIds[i]; // Push a pair of key defining variables into one key. Service or agent Ids are not enough by themselves // As with other units, we assume that the system is not expected to support more than than 2^32-1 services // Need to carefully check pairings, since it's hard to find if something is incorrectly misplaced bitwise // serviceId occupies first 32 bits uint256 serviceAgent = serviceId; // agentId takes the second 32 bits serviceAgent |= uint256(agentIds[i]) << 32; mapServiceAndAgentIdAgentParams[serviceAgent] = agentParams[i]; service.maxNumAgentInstances += agentParams[i].slots; // Security deposit is the maximum of the canonical agent registration bond if (agentParams[i].bond > securityDeposit) { securityDeposit = agentParams[i].bond; } } service.securityDeposit = securityDeposit; // Check for the correct threshold: no less than ceil((n * 2 + 1) / 3) of all the agent instances combined uint256 checkThreshold = uint256(service.maxNumAgentInstances * 2 + 1); if (checkThreshold % 3 == 0) { checkThreshold = checkThreshold / 3; } else { checkThreshold = checkThreshold / 3 + 1; } if (service.threshold < checkThreshold || service.threshold > service.maxNumAgentInstances) { revert WrongThreshold(service.threshold, checkThreshold, service.maxNumAgentInstances); } } /// @dev Creates a new service. /// @param serviceOwner Individual that creates and controls a service. /// @param configHash IPFS hash pointing to the config metadata. /// @param agentIds Canonical agent Ids in a sorted ascending order. /// @param agentParams Number of agent instances and required required bond to register an instance in the service. /// @param threshold Signers threshold for a multisig composed by agent instances. /// @return serviceId Created service Id. function create( address serviceOwner, bytes32 configHash, uint32[] memory agentIds, AgentParams[] memory agentParams, uint32 threshold ) external returns (uint256 serviceId) { // Reentrancy guard if (_locked > 1) { revert ReentrancyGuard(); } _locked = 2; // Check for the manager privilege for a service management if (manager != msg.sender) { revert ManagerOnly(msg.sender, manager); } // Check for the non-empty service owner address if (serviceOwner == address(0)) { revert ZeroAddress(); } // Execute initial checks _initialChecks(configHash, agentIds, agentParams); // Check that there are no zero number of slots for a specific canonical agent id and no zero registration bond for (uint256 i = 0; i < agentIds.length; i++) { if (agentParams[i].slots == 0 || agentParams[i].bond == 0) { revert ZeroValue(); } } // Create a new service Id serviceId = totalSupply; serviceId++; // Set high-level data components of the service instance Service memory service; // Updating high-level data components of the service service.threshold = threshold; // Assigning the initial hash service.configHash = configHash; // Set the initial service state service.state = ServiceState.PreRegistration; // Set service data _setServiceData(service, agentIds, agentParams, agentIds.length, serviceId); mapServices[serviceId] = service; totalSupply = serviceId; // Mint the service instance to the service owner and record the service structure _safeMint(serviceOwner, serviceId); emit CreateService(serviceId); _locked = 1; } /// @dev Updates a service in a CRUD way. /// @param serviceOwner Individual that creates and controls a service. /// @param configHash IPFS hash pointing to the config metadata. /// @param agentIds Canonical agent Ids in a sorted ascending order. /// @param agentParams Number of agent instances and required required bond to register an instance in the service. /// @param threshold Signers threshold for a multisig composed by agent instances. /// @param serviceId Service Id to be updated. /// @return success True, if function executed successfully. function update( address serviceOwner, bytes32 configHash, uint32[] memory agentIds, AgentParams[] memory agentParams, uint32 threshold, uint256 serviceId ) external returns (bool success) { // Check for the manager privilege for a service management if (manager != msg.sender) { revert ManagerOnly(msg.sender, manager); } // Check for the service ownership address actualOwner = ownerOf(serviceId); if (actualOwner != serviceOwner) { revert OwnerOnly(serviceOwner, actualOwner); } Service memory service = mapServices[serviceId]; if (service.state != ServiceState.PreRegistration) { revert WrongServiceState(uint256(service.state), serviceId); } // Execute initial checks _initialChecks(configHash, agentIds, agentParams); // Updating high-level data components of the service service.threshold = threshold; service.maxNumAgentInstances = 0; // Collect non-zero canonical agent ids and slots / costs, remove any canonical agent Ids from the params map uint32[] memory newAgentIds = new uint32[](agentIds.length); AgentParams[] memory newAgentParams = new AgentParams[](agentIds.length); uint256 size; for (uint256 i = 0; i < agentIds.length; i++) { if (agentParams[i].slots == 0) { // Push a pair of key defining variables into one key. Service or agent Ids are not enough by themselves // serviceId occupies first 32 bits, agentId gets the next 32 bits uint256 serviceAgent = serviceId; serviceAgent |= uint256(agentIds[i]) << 32; delete mapServiceAndAgentIdAgentParams[serviceAgent]; } else { newAgentIds[size] = agentIds[i]; newAgentParams[size] = agentParams[i]; size++; } } // Check if the previous hash is the same / hash was not updated bytes32 lastConfigHash = service.configHash; if (lastConfigHash != configHash) { mapConfigHashes[serviceId].push(lastConfigHash); service.configHash = configHash; } // Set service data and record the modified service struct _setServiceData(service, newAgentIds, newAgentParams, size, serviceId); mapServices[serviceId] = service; emit UpdateService(serviceId, configHash); success = true; } /// @dev Activates the service. /// @param serviceOwner Individual that creates and controls a service. /// @param serviceId Correspondent service Id. /// @return success True, if function executed successfully. function activateRegistration(address serviceOwner, uint256 serviceId) external payable returns (bool success) { // Check for the manager privilege for a service management if (manager != msg.sender) { revert ManagerOnly(msg.sender, manager); } // Check for the service ownership address actualOwner = ownerOf(serviceId); if (actualOwner != serviceOwner) { revert OwnerOnly(serviceOwner, actualOwner); } Service storage service = mapServices[serviceId]; // Service must be inactive if (service.state != ServiceState.PreRegistration) { revert ServiceMustBeInactive(serviceId); } if (msg.value != service.securityDeposit) { revert IncorrectRegistrationDepositValue(msg.value, service.securityDeposit, serviceId); } // Activate the agent instance registration service.state = ServiceState.ActiveRegistration; emit ActivateRegistration(serviceId); success = true; } /// @dev Registers agent instances. /// @param operator Address of the operator. /// @param serviceId Service Id to be updated. /// @param agentInstances Agent instance addresses. /// @param agentIds Canonical Ids of the agent correspondent to the agent instance. /// @return success True, if function executed successfully. function registerAgents( address operator, uint256 serviceId, address[] memory agentInstances, uint32[] memory agentIds ) external payable returns (bool success) { // Check for the manager privilege for a service management if (manager != msg.sender) { revert ManagerOnly(msg.sender, manager); } // Check if the length of canonical agent instance addresses array and ids array have the same length if (agentInstances.length != agentIds.length) { revert WrongArrayLength(agentInstances.length, agentIds.length); } Service storage service = mapServices[serviceId]; // The service has to be active to register agents if (service.state != ServiceState.ActiveRegistration) { revert WrongServiceState(uint256(service.state), serviceId); } // Check for the sufficient amount of bond fee is provided uint256 numAgents = agentInstances.length; uint256 totalBond = 0; for (uint256 i = 0; i < numAgents; ++i) { // Check if canonical agent Id exists in the service // Push a pair of key defining variables into one key. Service or agent Ids are not enough by themselves // serviceId occupies first 32 bits, agentId gets the next 32 bits uint256 serviceAgent = serviceId; serviceAgent |= uint256(agentIds[i]) << 32; AgentParams memory agentParams = mapServiceAndAgentIdAgentParams[serviceAgent]; if (agentParams.slots == 0) { revert AgentNotInService(agentIds[i], serviceId); } totalBond += agentParams.bond; } if (msg.value != totalBond) { revert IncorrectAgentBondingValue(msg.value, totalBond, serviceId); } // Operator address must not be used as an agent instance anywhere else if (mapAgentInstanceOperators[operator] != address(0)) { revert WrongOperator(serviceId); } // Push a pair of key defining variables into one key. Service Id or operator are not enough by themselves // operator occupies first 160 bits uint256 operatorService = uint256(uint160(operator)); // serviceId occupies next 32 bits assuming it is not greater than 2^32 - 1 in value operatorService |= serviceId << 160; for (uint256 i = 0; i < numAgents; ++i) { address agentInstance = agentInstances[i]; uint32 agentId = agentIds[i]; // Operator address must be different from agent instance one if (operator == agentInstance) { revert WrongOperator(serviceId); } // Check if the agent instance is already engaged with another service if (mapAgentInstanceOperators[agentInstance] != address(0)) { revert AgentInstanceRegistered(mapAgentInstanceOperators[agentInstance]); } // Check if there is an empty slot for the agent instance in this specific service // serviceId occupies first 32 bits, agentId gets the next 32 bits uint256 serviceAgent = serviceId; serviceAgent |= uint256(agentIds[i]) << 32; if (mapServiceAndAgentIdAgentInstances[serviceAgent].length == mapServiceAndAgentIdAgentParams[serviceAgent].slots) { revert AgentInstancesSlotsFilled(serviceId); } // Add agent instance and operator and set the instance engagement mapServiceAndAgentIdAgentInstances[serviceAgent].push(agentInstance); mapOperatorAndServiceIdAgentInstances[operatorService].push(AgentInstance(agentInstance, agentId)); service.numAgentInstances++; mapAgentInstanceOperators[agentInstance] = operator; emit RegisterInstance(operator, serviceId, agentInstance, agentId); } // If the service agent instance capacity is reached, the service becomes finished-registration if (service.numAgentInstances == service.maxNumAgentInstances) { service.state = ServiceState.FinishedRegistration; } // Update operator's bonding balance mapOperatorAndServiceIdOperatorBalances[operatorService] += uint96(msg.value); emit Deposit(operator, msg.value); success = true; } /// @dev Creates multisig instance controlled by the set of service agent instances and deploys the service. /// @param serviceOwner Individual that creates and controls a service. /// @param serviceId Correspondent service Id. /// @param multisigImplementation Multisig implementation address. /// @param data Data payload for the multisig creation. /// @return multisig Address of the created multisig. function deploy( address serviceOwner, uint256 serviceId, address multisigImplementation, bytes memory data ) external returns (address multisig) { // Reentrancy guard if (_locked > 1) { revert ReentrancyGuard(); } _locked = 2; // Check for the manager privilege for a service management if (manager != msg.sender) { revert ManagerOnly(msg.sender, manager); } // Check for the service ownership address actualOwner = ownerOf(serviceId); if (actualOwner != serviceOwner) { revert OwnerOnly(serviceOwner, actualOwner); } // Check for the whitelisted multisig implementation if (!mapMultisigs[multisigImplementation]) { revert UnauthorizedMultisig(multisigImplementation); } Service storage service = mapServices[serviceId]; if (service.state != ServiceState.FinishedRegistration) { revert WrongServiceState(uint256(service.state), serviceId); } // Get all agent instances for the multisig address[] memory agentInstances = _getAgentInstances(service, serviceId); // Create a multisig with agent instances multisig = IMultisig(multisigImplementation).create(agentInstances, service.threshold, data); // Update maps of service Id to subcomponent and agent Ids mapServiceIdSetAgentIds[serviceId] = service.agentIds; mapServiceIdSetComponentIds[serviceId] = IRegistry(agentRegistry).calculateSubComponents(service.agentIds); service.multisig = multisig; service.state = ServiceState.Deployed; emit CreateMultisigWithAgents(serviceId, multisig); emit DeployService(serviceId); _locked = 1; } /// @dev Slashes a specified agent instance. /// @param agentInstances Agent instances to slash. /// @param amounts Correspondent amounts to slash. /// @param serviceId Service Id. /// @return success True, if function executed successfully. function slash(address[] memory agentInstances, uint96[] memory amounts, uint256 serviceId) external returns (bool success) { // Check if the service is deployed // Since we do not kill (burn) services, we want this check to happen in a right service state. // If the service is deployed, it definitely exists and is running. We do not want this function to be abused // when the service was deployed, then terminated, then in a sleep mode or before next deployment somebody // could use this function and try to slash operators. Service memory service = mapServices[serviceId]; if (service.state != ServiceState.Deployed) { revert WrongServiceState(uint256(service.state), serviceId); } // Check for the array size if (agentInstances.length != amounts.length) { revert WrongArrayLength(agentInstances.length, amounts.length); } // Only the multisig of a correspondent address can slash its agent instances if (msg.sender != service.multisig) { revert OnlyOwnServiceMultisig(msg.sender, service.multisig, serviceId); } // Loop over each agent instance uint256 numInstancesToSlash = agentInstances.length; for (uint256 i = 0; i < numInstancesToSlash; ++i) { // Get the service Id from the agentInstance map address operator = mapAgentInstanceOperators[agentInstances[i]]; // Push a pair of key defining variables into one key. Service Id or operator are not enough by themselves // operator occupies first 160 bits uint256 operatorService = uint256(uint160(operator)); // serviceId occupies next 32 bits operatorService |= serviceId << 160; // Slash the balance of the operator, make sure it does not go below zero uint96 balance = mapOperatorAndServiceIdOperatorBalances[operatorService]; if ((amounts[i] + 1) > balance) { // We cannot add to the slashed amount more than the balance of the operator slashedFunds += balance; balance = 0; } else { slashedFunds += amounts[i]; balance -= amounts[i]; } mapOperatorAndServiceIdOperatorBalances[operatorService] = balance; emit OperatorSlashed(amounts[i], operator, serviceId); } success = true; } /// @dev Terminates the service. /// @param serviceOwner Owner of the service. /// @param serviceId Service Id to be updated. /// @return success True, if function executed successfully. /// @return refund Refund to return to the service owner. function terminate(address serviceOwner, uint256 serviceId) external returns (bool success, uint256 refund) { // Reentrancy guard if (_locked > 1) { revert ReentrancyGuard(); } _locked = 2; // Check for the manager privilege for a service management if (manager != msg.sender) { revert ManagerOnly(msg.sender, manager); } // Check for the service ownership address actualOwner = ownerOf(serviceId); if (actualOwner != serviceOwner) { revert OwnerOnly(serviceOwner, actualOwner); } Service storage service = mapServices[serviceId]; // Check if the service is already terminated if (service.state == ServiceState.PreRegistration || service.state == ServiceState.TerminatedBonded) { revert WrongServiceState(uint256(service.state), serviceId); } // Define the state of the service depending on the number of bonded agent instances if (service.numAgentInstances > 0) { service.state = ServiceState.TerminatedBonded; } else { service.state = ServiceState.PreRegistration; } // Delete the sensitive data delete mapServiceIdSetComponentIds[serviceId]; delete mapServiceIdSetAgentIds[serviceId]; for (uint256 i = 0; i < service.agentIds.length; ++i) { // serviceId occupies first 32 bits, agentId gets the next 32 bits uint256 serviceAgent = serviceId; serviceAgent |= uint256(service.agentIds[i]) << 32; delete mapServiceAndAgentIdAgentInstances[serviceAgent]; } // Return registration deposit back to the service owner refund = service.securityDeposit; // By design, the refund is always a non-zero value, so no check is needed here fo that (bool result, ) = serviceOwner.call{value: refund}(""); if (!result) { revert TransferFailed(address(0), address(this), serviceOwner, refund); } emit Refund(serviceOwner, refund); emit TerminateService(serviceId); success = true; _locked = 1; } /// @dev Unbonds agent instances of the operator from the service. /// @param operator Operator of agent instances. /// @param serviceId Service Id. /// @return success True, if function executed successfully. /// @return refund The amount of refund returned to the operator. function unbond(address operator, uint256 serviceId) external returns (bool success, uint256 refund) { // Reentrancy guard if (_locked > 1) { revert ReentrancyGuard(); } _locked = 2; // Check for the manager privilege for a service management if (manager != msg.sender) { revert ManagerOnly(msg.sender, manager); } // Checks if the operator address is not zero if (operator == address(0)) { revert ZeroAddress(); } Service storage service = mapServices[serviceId]; // Service can only be in the terminated-bonded state or expired-registration in order to proceed if (service.state != ServiceState.TerminatedBonded) { revert WrongServiceState(uint256(service.state), serviceId); } // Check for the operator and unbond all its agent instances // Push a pair of key defining variables into one key. Service Id or operator are not enough by themselves // operator occupies first 160 bits uint256 operatorService = uint256(uint160(operator)); // serviceId occupies next 32 bits operatorService |= serviceId << 160; AgentInstance[] memory agentInstances = mapOperatorAndServiceIdAgentInstances[operatorService]; uint256 numAgentsUnbond = agentInstances.length; if (numAgentsUnbond == 0) { revert OperatorHasNoInstances(operator, serviceId); } // Subtract number of unbonded agent instances service.numAgentInstances -= uint32(numAgentsUnbond); // When number of instances is equal to zero, all the operators have unbonded and the service is moved into // the PreRegistration state, from where it can be updated / start registration / get deployed again if (service.numAgentInstances == 0) { service.state = ServiceState.PreRegistration; } // else condition is redundant here, since the service is either in the TerminatedBonded state, or moved // into the PreRegistration state and unbonding is not possible before the new TerminatedBonded state is reached // Calculate registration refund and free all agent instances for (uint256 i = 0; i < numAgentsUnbond; i++) { // serviceId occupies first 32 bits, agentId gets the next 32 bits uint256 serviceAgent = serviceId; serviceAgent |= uint256(agentInstances[i].agentId) << 32; refund += mapServiceAndAgentIdAgentParams[serviceAgent].bond; // Clean-up the sensitive data such that it is not reused later delete mapAgentInstanceOperators[agentInstances[i].instance]; } // Clean all the operator agent instances records for this service delete mapOperatorAndServiceIdAgentInstances[operatorService]; // Calculate the refund uint96 balance = mapOperatorAndServiceIdOperatorBalances[operatorService]; // This situation is possible if the operator was slashed for the agent instance misbehavior if (refund > balance) { refund = balance; } // Refund the operator if (refund > 0) { // Operator's balance is essentially zero after the refund mapOperatorAndServiceIdOperatorBalances[operatorService] = 0; // Send the refund (bool result, ) = operator.call{value: refund}(""); if (!result) { revert TransferFailed(address(0), address(this), operator, refund); } emit Refund(operator, refund); } emit OperatorUnbond(operator, serviceId); success = true; _locked = 1; } /// @dev Gets the service instance. /// @param serviceId Service Id. /// @return service Corresponding Service struct. function getService(uint256 serviceId) external view returns (Service memory service) { service = mapServices[serviceId]; } /// @dev Gets service agent parameters: number of agent instances (slots) and a bond amount. /// @param serviceId Service Id. /// @return numAgentIds Number of canonical agent Ids in the service. /// @return agentParams Set of agent parameters for each canonical agent Id. function getAgentParams(uint256 serviceId) external view returns (uint256 numAgentIds, AgentParams[] memory agentParams) { Service memory service = mapServices[serviceId]; numAgentIds = service.agentIds.length; agentParams = new AgentParams[](numAgentIds); for (uint256 i = 0; i < numAgentIds; ++i) { uint256 serviceAgent = serviceId; serviceAgent |= uint256(service.agentIds[i]) << 32; agentParams[i] = mapServiceAndAgentIdAgentParams[serviceAgent]; } } /// @dev Lists all the instances of a given canonical agent Id if the service. /// @param serviceId Service Id. /// @param agentId Canonical agent Id. /// @return numAgentInstances Number of agent instances. /// @return agentInstances Set of agent instances for a specified canonical agent Id. function getInstancesForAgentId(uint256 serviceId, uint256 agentId) external view returns (uint256 numAgentInstances, address[] memory agentInstances) { uint256 serviceAgent = serviceId; serviceAgent |= agentId << 32; numAgentInstances = mapServiceAndAgentIdAgentInstances[serviceAgent].length; agentInstances = new address[](numAgentInstances); for (uint256 i = 0; i < numAgentInstances; i++) { agentInstances[i] = mapServiceAndAgentIdAgentInstances[serviceAgent][i]; } } /// @dev Gets all agent instances. /// @param service Service instance. /// @param serviceId ServiceId. /// @return agentInstances Pre-allocated list of agent instance addresses. function _getAgentInstances(Service memory service, uint256 serviceId) private view returns (address[] memory agentInstances) { agentInstances = new address[](service.numAgentInstances); uint256 count; for (uint256 i = 0; i < service.agentIds.length; i++) { // serviceId occupies first 32 bits, agentId gets the next 32 bits uint256 serviceAgent = serviceId; serviceAgent |= uint256(service.agentIds[i]) << 32; for (uint256 j = 0; j < mapServiceAndAgentIdAgentInstances[serviceAgent].length; j++) { agentInstances[count] = mapServiceAndAgentIdAgentInstances[serviceAgent][j]; count++; } } } /// @dev Gets service agent instances. /// @param serviceId ServiceId. /// @return numAgentInstances Number of agent instances. /// @return agentInstances Pre-allocated list of agent instance addresses. function getAgentInstances(uint256 serviceId) external view returns (uint256 numAgentInstances, address[] memory agentInstances) { Service memory service = mapServices[serviceId]; agentInstances = _getAgentInstances(service, serviceId); numAgentInstances = agentInstances.length; } /// @dev Gets previous service config hashes. /// @param serviceId Service Id. /// @return numHashes Number of hashes. /// @return configHashes The list of previous component hashes (excluding the current one). function getPreviousHashes(uint256 serviceId) external view returns (uint256 numHashes, bytes32[] memory configHashes) { configHashes = mapConfigHashes[serviceId]; numHashes = configHashes.length; } /// @dev Gets the full set of linearized components / canonical agent Ids for a specified service. /// @notice The service must be / have been deployed in order to get the actual data. /// @param serviceId Service Id. /// @return numUnitIds Number of component / agent Ids. /// @return unitIds Set of component / agent Ids. function getUnitIdsOfService(IRegistry.UnitType unitType, uint256 serviceId) external view returns (uint256 numUnitIds, uint32[] memory unitIds) { if (unitType == IRegistry.UnitType.Component) { unitIds = mapServiceIdSetComponentIds[serviceId]; } else { unitIds = mapServiceIdSetAgentIds[serviceId]; } numUnitIds = unitIds.length; } /// @dev Gets the operator's balance in a specific service. /// @param operator Operator address. /// @param serviceId Service Id. /// @return balance The balance of the operator. function getOperatorBalance(address operator, uint256 serviceId) external view returns (uint256 balance) { uint256 operatorService = uint256(uint160(operator)); operatorService |= serviceId << 160; balance = mapOperatorAndServiceIdOperatorBalances[operatorService]; } /// @dev Controls multisig implementation address permission. /// @param multisig Address of a multisig implementation. /// @param permission Grant or revoke permission. /// @return success True, if function executed successfully. function changeMultisigPermission(address multisig, bool permission) external returns (bool success) { // Check for the contract ownership if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } if (multisig == address(0)) { revert ZeroAddress(); } mapMultisigs[multisig] = permission; success = true; } /// @dev Drains slashed funds. /// @return amount Drained amount. function drain() external returns (uint256 amount) { // Reentrancy guard if (_locked > 1) { revert ReentrancyGuard(); } _locked = 2; // Check for the drainer address if (msg.sender != drainer) { revert ManagerOnly(msg.sender, drainer); } // Drain the slashed funds amount = slashedFunds; if (amount > 0) { slashedFunds = 0; // Send the refund (bool result, ) = msg.sender.call{value: amount}(""); if (!result) { revert TransferFailed(address(0), address(this), msg.sender, amount); } emit Drain(msg.sender, amount); } _locked = 1; } /// @dev Gets the hash of the service. /// @param serviceId Service Id. /// @return Service hash. function _getUnitHash(uint256 serviceId) internal view override returns (bytes32) { return mapServices[serviceId].configHash; } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; import "../lib/solmate/src/tokens/ERC721.sol"; import "./interfaces/IErrorsRegistries.sol"; /// @title Generic Registry - Smart contract for generic registry template /// @author Aleksandr Kuperman - <[email protected]> abstract contract GenericRegistry is IErrorsRegistries, ERC721 { event OwnerUpdated(address indexed owner); event ManagerUpdated(address indexed manager); event BaseURIChanged(string baseURI); // Owner address address public owner; // Unit manager address public manager; // Base URI string public baseURI; // Unit counter uint256 public totalSupply; // Reentrancy lock uint256 internal _locked = 1; // To better understand the CID anatomy, please refer to: https://proto.school/anatomy-of-a-cid/05 // CID = <multibase_encoding>multibase_encoding(<cid-version><multicodec><multihash-algorithm><multihash-length><multihash-hash>) // CID prefix = <multibase_encoding>multibase_encoding(<cid-version><multicodec><multihash-algorithm><multihash-length>) // to complement the multibase_encoding(<multihash-hash>) // multibase_encoding = base16 = "f" // cid-version = version 1 = "0x01" // multicodec = dag-pb = "0x70" // multihash-algorithm = sha2-256 = "0x12" // multihash-length = 256 bits = "0x20" string public constant CID_PREFIX = "f01701220"; /// @dev Changes the owner address. /// @param newOwner Address of a new owner. function changeOwner(address newOwner) external virtual { // Check for the ownership if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } // Check for the zero address if (newOwner == address(0)) { revert ZeroAddress(); } owner = newOwner; emit OwnerUpdated(newOwner); } /// @dev Changes the unit manager. /// @param newManager Address of a new unit manager. function changeManager(address newManager) external virtual { if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } // Check for the zero address if (newManager == address(0)) { revert ZeroAddress(); } manager = newManager; emit ManagerUpdated(newManager); } /// @dev Checks for the unit existence. /// @notice Unit counter starts from 1. /// @param unitId Unit Id. /// @return true if the unit exists, false otherwise. function exists(uint256 unitId) external view virtual returns (bool) { return unitId > 0 && unitId < (totalSupply + 1); } /// @dev Sets unit base URI. /// @param bURI Base URI string. function setBaseURI(string memory bURI) external virtual { // Check for the ownership if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } // Check for the zero value if (bytes(bURI).length == 0) { revert ZeroValue(); } baseURI = bURI; emit BaseURIChanged(bURI); } /// @dev Gets the valid unit Id from the provided index. /// @notice Unit counter starts from 1. /// @param id Unit counter. /// @return unitId Unit Id. function tokenByIndex(uint256 id) external view virtual returns (uint256 unitId) { unitId = id + 1; if (unitId > totalSupply) { revert Overflow(unitId, totalSupply); } } // Open sourced from: https://stackoverflow.com/questions/67893318/solidity-how-to-represent-bytes32-as-string /// @dev Converts bytes16 input data to hex16. /// @notice This method converts bytes into the same bytes-character hex16 representation. /// @param data bytes16 input data. /// @return result hex16 conversion from the input bytes16 data. function _toHex16(bytes16 data) internal pure returns (bytes32 result) { result = bytes32 (data) & 0xFFFFFFFFFFFFFFFF000000000000000000000000000000000000000000000000 | (bytes32 (data) & 0x0000000000000000FFFFFFFFFFFFFFFF00000000000000000000000000000000) >> 64; result = result & 0xFFFFFFFF000000000000000000000000FFFFFFFF000000000000000000000000 | (result & 0x00000000FFFFFFFF000000000000000000000000FFFFFFFF0000000000000000) >> 32; result = result & 0xFFFF000000000000FFFF000000000000FFFF000000000000FFFF000000000000 | (result & 0x0000FFFF000000000000FFFF000000000000FFFF000000000000FFFF00000000) >> 16; result = result & 0xFF000000FF000000FF000000FF000000FF000000FF000000FF000000FF000000 | (result & 0x00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000) >> 8; result = (result & 0xF000F000F000F000F000F000F000F000F000F000F000F000F000F000F000F000) >> 4 | (result & 0x0F000F000F000F000F000F000F000F000F000F000F000F000F000F000F000F00) >> 8; result = bytes32 (0x3030303030303030303030303030303030303030303030303030303030303030 + uint256 (result) + (uint256 (result) + 0x0606060606060606060606060606060606060606060606060606060606060606 >> 4 & 0x0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F) * 39); } /// @dev Gets the hash of the unit. /// @param unitId Unit Id. /// @return Unit hash. function _getUnitHash(uint256 unitId) internal view virtual returns (bytes32); /// @dev Returns unit token URI. /// @notice Expected multicodec: dag-pb; hashing function: sha2-256, with base16 encoding and leading CID_PREFIX removed. /// @param unitId Unit Id. /// @return Unit token URI string. function tokenURI(uint256 unitId) public view virtual override returns (string memory) { bytes32 unitHash = _getUnitHash(unitId); // Parse 2 parts of bytes32 into left and right hex16 representation, and concatenate into string // adding the base URI and a cid prefix for the full base16 multibase prefix IPFS hash representation return string(abi.encodePacked(baseURI, CID_PREFIX, _toHex16(bytes16(unitHash)), _toHex16(bytes16(unitHash << 128)))); } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; /// @dev Generic multisig. interface IMultisig { /// @dev Creates a multisig. /// @param owners Set of multisig owners. /// @param threshold Number of required confirmations for a multisig transaction. /// @param data Packed data related to the creation of a chosen multisig. /// @return multisig Address of a created multisig. function create( address[] memory owners, uint256 threshold, bytes memory data ) external returns (address multisig); }// SPDX-License-Identifier: MIT pragma solidity ^0.8.15; /// @dev Required interface for the component / agent manipulation. interface IRegistry { enum UnitType { Component, Agent } /// @dev Creates component / agent. /// @param unitOwner Owner of the component / agent. /// @param unitHash IPFS hash of the component / agent. /// @param dependencies Set of component dependencies in a sorted ascending order. /// @return The id of a minted component / agent. function create( address unitOwner, bytes32 unitHash, uint32[] memory dependencies ) external returns (uint256); /// @dev Updates the component / agent hash. /// @param owner Owner of the component / agent. /// @param unitId Unit Id. /// @param unitHash Updated IPFS hash of the component / agent. /// @return success True, if function executed successfully. function updateHash(address owner, uint256 unitId, bytes32 unitHash) external returns (bool success); /// @dev Gets subcomponents of a provided unit Id from a local public map. /// @param unitId Unit Id. /// @return subComponentIds Set of subcomponents. /// @return numSubComponents Number of subcomponents. function getLocalSubComponents(uint256 unitId) external view returns (uint32[] memory subComponentIds, uint256 numSubComponents); /// @dev Calculates the set of subcomponent Ids. /// @param unitIds Set of unit Ids. /// @return subComponentIds Subcomponent Ids. function calculateSubComponents(uint32[] memory unitIds) external view returns (uint32[] memory subComponentIds); /// @dev Gets updated component / agent hashes. /// @param unitId Unit Id. /// @return numHashes Number of hashes. /// @return unitHashes The list of component / agent hashes. function getUpdatedHashes(uint256 unitId) external view returns (uint256 numHashes, bytes32[] memory unitHashes); /// @dev Gets the total supply of components / agents. /// @return Total supply. function totalSupply() external view returns (uint256); } // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; /// @notice Modern, minimalist, and gas efficient ERC-721 implementation. /// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC721.sol) abstract contract ERC721 { /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ event Transfer(address indexed from, address indexed to, uint256 indexed id); event Approval(address indexed owner, address indexed spender, uint256 indexed id); event ApprovalForAll(address indexed owner, address indexed operator, bool approved); /*////////////////////////////////////////////////////////////// METADATA STORAGE/LOGIC //////////////////////////////////////////////////////////////*/ string public name; string public symbol; function tokenURI(uint256 id) public view virtual returns (string memory); /*////////////////////////////////////////////////////////////// ERC721 BALANCE/OWNER STORAGE //////////////////////////////////////////////////////////////*/ mapping(uint256 => address) internal _ownerOf; mapping(address => uint256) internal _balanceOf; function ownerOf(uint256 id) public view virtual returns (address owner) { require((owner = _ownerOf[id]) != address(0), "NOT_MINTED"); } function balanceOf(address owner) public view virtual returns (uint256) { require(owner != address(0), "ZERO_ADDRESS"); return _balanceOf[owner]; } /*////////////////////////////////////////////////////////////// ERC721 APPROVAL STORAGE //////////////////////////////////////////////////////////////*/ mapping(uint256 => address) public getApproved; mapping(address => mapping(address => bool)) public isApprovedForAll; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ constructor(string memory _name, string memory _symbol) { name = _name; symbol = _symbol; } /*////////////////////////////////////////////////////////////// ERC721 LOGIC //////////////////////////////////////////////////////////////*/ function approve(address spender, uint256 id) public virtual { address owner = _ownerOf[id]; require(msg.sender == owner || isApprovedForAll[owner][msg.sender], "NOT_AUTHORIZED"); getApproved[id] = spender; emit Approval(owner, spender, id); } function setApprovalForAll(address operator, bool approved) public virtual { isApprovedForAll[msg.sender][operator] = approved; emit ApprovalForAll(msg.sender, operator, approved); } function transferFrom( address from, address to, uint256 id ) public virtual { require(from == _ownerOf[id], "WRONG_FROM"); require(to != address(0), "INVALID_RECIPIENT"); require( msg.sender == from || isApprovedForAll[from][msg.sender] || msg.sender == getApproved[id], "NOT_AUTHORIZED" ); // Underflow of the sender's balance is impossible because we check for // ownership above and the recipient's balance can't realistically overflow. unchecked { _balanceOf[from]--; _balanceOf[to]++; } _ownerOf[id] = to; delete getApproved[id]; emit Transfer(from, to, id); } function safeTransferFrom( address from, address to, uint256 id ) public virtual { transferFrom(from, to, id); if (to.code.length != 0) require( ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); } function safeTransferFrom( address from, address to, uint256 id, bytes calldata data ) public virtual { transferFrom(from, to, id); if (to.code.length != 0) require( ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); } /*////////////////////////////////////////////////////////////// ERC165 LOGIC //////////////////////////////////////////////////////////////*/ function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { return interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata } /*////////////////////////////////////////////////////////////// INTERNAL MINT/BURN LOGIC //////////////////////////////////////////////////////////////*/ function _mint(address to, uint256 id) internal virtual { require(to != address(0), "INVALID_RECIPIENT"); require(_ownerOf[id] == address(0), "ALREADY_MINTED"); // Counter overflow is incredibly unrealistic. unchecked { _balanceOf[to]++; } _ownerOf[id] = to; emit Transfer(address(0), to, id); } function _burn(uint256 id) internal virtual { address owner = _ownerOf[id]; require(owner != address(0), "NOT_MINTED"); // Ownership check above ensures no underflow. unchecked { _balanceOf[owner]--; } delete _ownerOf[id]; delete getApproved[id]; emit Transfer(owner, address(0), id); } /*////////////////////////////////////////////////////////////// INTERNAL SAFE MINT LOGIC //////////////////////////////////////////////////////////////*/ function _safeMint(address to, uint256 id) internal virtual { _mint(to, id); if (to.code.length != 0) require( ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, "") == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); } function _safeMint( address to, uint256 id, bytes memory data ) internal virtual { _mint(to, id); if (to.code.length != 0) require( ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, data) == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); } } /// @notice A generic interface for a contract which properly accepts ERC721 tokens. /// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC721.sol) abstract contract ERC721TokenReceiver { function onERC721Received( address, address, uint256, bytes calldata ) external virtual returns (bytes4) { return ERC721TokenReceiver.onERC721Received.selector; } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; /// @dev Errors. interface IErrorsRegistries { /// @dev Only `manager` has a privilege, but the `sender` was provided. /// @param sender Sender address. /// @param manager Required sender address as a manager. error ManagerOnly(address sender, address manager); /// @dev Only `owner` has a privilege, but the `sender` was provided. /// @param sender Sender address. /// @param owner Required sender address as an owner. error OwnerOnly(address sender, address owner); /// @dev Hash already exists in the records. error HashExists(); /// @dev Provided zero address. error ZeroAddress(); /// @dev Agent Id is not correctly provided for the current routine. /// @param agentId Component Id. error WrongAgentId(uint256 agentId); /// @dev Wrong length of two arrays. /// @param numValues1 Number of values in a first array. /// @param numValues2 Numberf of values in a second array. error WrongArrayLength(uint256 numValues1, uint256 numValues2); /// @dev Canonical agent Id is not found. /// @param agentId Canonical agent Id. error AgentNotFound(uint256 agentId); /// @dev Component Id is not found. /// @param componentId Component Id. error ComponentNotFound(uint256 componentId); /// @dev Multisig threshold is out of bounds. /// @param currentThreshold Current threshold value. /// @param minThreshold Minimum possible threshold value. /// @param maxThreshold Maximum possible threshold value. error WrongThreshold(uint256 currentThreshold, uint256 minThreshold, uint256 maxThreshold); /// @dev Agent instance is already registered with a specified `operator`. /// @param operator Operator that registered an instance. error AgentInstanceRegistered(address operator); /// @dev Wrong operator is specified when interacting with a specified `serviceId`. /// @param serviceId Service Id. error WrongOperator(uint256 serviceId); /// @dev Operator has no registered instances in the service. /// @param operator Operator address. /// @param serviceId Service Id. error OperatorHasNoInstances(address operator, uint256 serviceId); /// @dev Canonical `agentId` is not found as a part of `serviceId`. /// @param agentId Canonical agent Id. /// @param serviceId Service Id. error AgentNotInService(uint256 agentId, uint256 serviceId); /// @dev The contract is paused. error Paused(); /// @dev Zero value when it has to be different from zero. error ZeroValue(); /// @dev Value overflow. /// @param provided Overflow value. /// @param max Maximum possible value. error Overflow(uint256 provided, uint256 max); /// @dev Service must be inactive. /// @param serviceId Service Id. error ServiceMustBeInactive(uint256 serviceId); /// @dev All the agent instance slots for a specific `serviceId` are filled. /// @param serviceId Service Id. error AgentInstancesSlotsFilled(uint256 serviceId); /// @dev Wrong state of a service. /// @param state Service state. /// @param serviceId Service Id. error WrongServiceState(uint256 state, uint256 serviceId); /// @dev Only own service multisig is allowed. /// @param provided Provided address. /// @param expected Expected multisig address. /// @param serviceId Service Id. error OnlyOwnServiceMultisig(address provided, address expected, uint256 serviceId); /// @dev Multisig is not whitelisted. /// @param multisig Address of a multisig implementation. error UnauthorizedMultisig(address multisig); /// @dev Incorrect deposit provided for the registration activation. /// @param sent Sent amount. /// @param expected Expected amount. /// @param serviceId Service Id. error IncorrectRegistrationDepositValue(uint256 sent, uint256 expected, uint256 serviceId); /// @dev Insufficient value provided for the agent instance bonding. /// @param sent Sent amount. /// @param expected Expected amount. /// @param serviceId Service Id. error IncorrectAgentBondingValue(uint256 sent, uint256 expected, uint256 serviceId); /// @dev Failure of a transfer. /// @param token Address of a token. /// @param from Address `from`. /// @param to Address `to`. /// @param value Value. error TransferFailed(address token, address from, address to, uint256 value); /// @dev Caught reentrancy violation. error ReentrancyGuard(); }
File 5 of 7: GnosisSafeMultisig
// SPDX-License-Identifier: MIT pragma solidity ^0.8.15; // Gnosis Safe Proxy Factory interface extracted from the mainnet: https://etherscan.io/address/0xa6b71e26c5e0845f74c812102ca7114b6a896ab2#code#F2#L61 interface IGnosisSafeProxyFactory { /// @dev Allows to create new proxy contact and execute a message call to the new proxy within one transaction. /// @param _singleton Address of singleton contract. /// @param initializer Payload for message call sent to new proxy contract. /// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract. function createProxyWithNonce( address _singleton, bytes memory initializer, uint256 saltNonce ) external returns (address proxy); } /// @dev Provided incorrect data length. /// @param expected Expected minimum data length. /// @param provided Provided data length. error IncorrectDataLength(uint256 expected, uint256 provided); /// @title Gnosis Safe - Smart contract for Gnosis Safe multisig implementation of a generic multisig interface /// @author Aleksandr Kuperman - <[email protected]> contract GnosisSafeMultisig { // Selector of the Gnosis Safe setup function bytes4 public constant GNOSIS_SAFE_SETUP_SELECTOR = 0xb63e800d; // Default data size to be parsed and passed to the Gnosis Safe Factory without payload uint256 public constant DEFAULT_DATA_LENGTH = 144; // Gnosis Safe address payable public immutable gnosisSafe; // Gnosis Safe Factory address public immutable gnosisSafeProxyFactory; /// @dev GnosisSafeMultisig constructor. /// @param _gnosisSafe Gnosis Safe address. /// @param _gnosisSafeProxyFactory Gnosis Safe proxy factory address. constructor (address payable _gnosisSafe, address _gnosisSafeProxyFactory) { gnosisSafe = _gnosisSafe; gnosisSafeProxyFactory = _gnosisSafeProxyFactory; } /// @dev Parses (unpacks) the data to gnosis safe specific parameters. /// @notice If the provided data is not empty, its length must be at least 144 bytes to be parsed correctly. /// @param data Packed data related to the creation of a gnosis safe multisig. function _parseData(bytes memory data) internal pure returns (address to, address fallbackHandler, address paymentToken, address payable paymentReceiver, uint256 payment, uint256 nonce, bytes memory payload) { uint256 dataLength = data.length; if (dataLength > 0) { // Check for the correct data length if (dataLength < DEFAULT_DATA_LENGTH) { revert IncorrectDataLength(DEFAULT_DATA_LENGTH, dataLength); } // Read the first 144 bytes of data assembly { // Read all the addresses first (80 bytes) let offset := 20 to := mload(add(data, offset)) offset := add(offset, 20) fallbackHandler := mload(add(data, offset)) offset := add(offset, 20) paymentToken := mload(add(data, offset)) offset := add(offset, 20) paymentReceiver := mload(add(data, offset)) // Read all the uints (64 more bytes, a total of 144 bytes) offset := add(offset, 32) payment := mload(add(data, offset)) offset := add(offset, 32) nonce := mload(add(data, offset)) } // Read the payload, if provided if (dataLength > DEFAULT_DATA_LENGTH) { uint256 payloadLength = dataLength - DEFAULT_DATA_LENGTH; payload = new bytes(payloadLength); for (uint256 i = 0; i < payloadLength; ++i) { payload[i] = data[i + DEFAULT_DATA_LENGTH]; } } } } /// @dev Creates a gnosis safe multisig. /// @param owners Set of multisig owners. /// @param threshold Number of required confirmations for a multisig transaction. /// @param data Packed data related to the creation of a chosen multisig. /// @return multisig Address of a created multisig. function create( address[] memory owners, uint256 threshold, bytes memory data ) external returns (address multisig) { // Parse the data into gnosis-specific set of variables (address to, address fallbackHandler, address paymentToken, address payable paymentReceiver, uint256 payment, uint256 nonce, bytes memory payload) = _parseData(data); // Encode the gnosis setup function parameters bytes memory safeParams = abi.encodeWithSelector(GNOSIS_SAFE_SETUP_SELECTOR, owners, threshold, to, payload, fallbackHandler, paymentToken, payment, paymentReceiver); // Create a gnosis safe multisig via the proxy factory multisig = IGnosisSafeProxyFactory(gnosisSafeProxyFactory).createProxyWithNonce(gnosisSafe, safeParams, nonce); } }
File 6 of 7: GnosisSafe
// SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; import "./base/ModuleManager.sol"; import "./base/OwnerManager.sol"; import "./base/FallbackManager.sol"; import "./base/GuardManager.sol"; import "./common/EtherPaymentFallback.sol"; import "./common/Singleton.sol"; import "./common/SignatureDecoder.sol"; import "./common/SecuredTokenTransfer.sol"; import "./common/StorageAccessible.sol"; import "./interfaces/ISignatureValidator.sol"; import "./external/GnosisSafeMath.sol"; /// @title Gnosis Safe - A multisignature wallet with support for confirmations using signed messages based on ERC191. /// @author Stefan George - <[email protected]> /// @author Richard Meissner - <[email protected]> contract GnosisSafe is EtherPaymentFallback, Singleton, ModuleManager, OwnerManager, SignatureDecoder, SecuredTokenTransfer, ISignatureValidatorConstants, FallbackManager, StorageAccessible, GuardManager { using GnosisSafeMath for uint256; string public constant VERSION = "1.3.0"; // keccak256( // "EIP712Domain(uint256 chainId,address verifyingContract)" // ); bytes32 private constant DOMAIN_SEPARATOR_TYPEHASH = 0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218; // keccak256( // "SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)" // ); bytes32 private constant SAFE_TX_TYPEHASH = 0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8; event SafeSetup(address indexed initiator, address[] owners, uint256 threshold, address initializer, address fallbackHandler); event ApproveHash(bytes32 indexed approvedHash, address indexed owner); event SignMsg(bytes32 indexed msgHash); event ExecutionFailure(bytes32 txHash, uint256 payment); event ExecutionSuccess(bytes32 txHash, uint256 payment); uint256 public nonce; bytes32 private _deprecatedDomainSeparator; // Mapping to keep track of all message hashes that have been approve by ALL REQUIRED owners mapping(bytes32 => uint256) public signedMessages; // Mapping to keep track of all hashes (message or transaction) that have been approve by ANY owners mapping(address => mapping(bytes32 => uint256)) public approvedHashes; // This constructor ensures that this contract can only be used as a master copy for Proxy contracts constructor() { // By setting the threshold it is not possible to call setup anymore, // so we create a Safe with 0 owners and threshold 1. // This is an unusable Safe, perfect for the singleton threshold = 1; } /// @dev Setup function sets initial storage of contract. /// @param _owners List of Safe owners. /// @param _threshold Number of required confirmations for a Safe transaction. /// @param to Contract address for optional delegate call. /// @param data Data payload for optional delegate call. /// @param fallbackHandler Handler for fallback calls to this contract /// @param paymentToken Token that should be used for the payment (0 is ETH) /// @param payment Value that should be paid /// @param paymentReceiver Adddress that should receive the payment (or 0 if tx.origin) function setup( address[] calldata _owners, uint256 _threshold, address to, bytes calldata data, address fallbackHandler, address paymentToken, uint256 payment, address payable paymentReceiver ) external { // setupOwners checks if the Threshold is already set, therefore preventing that this method is called twice setupOwners(_owners, _threshold); if (fallbackHandler != address(0)) internalSetFallbackHandler(fallbackHandler); // As setupOwners can only be called if the contract has not been initialized we don't need a check for setupModules setupModules(to, data); if (payment > 0) { // To avoid running into issues with EIP-170 we reuse the handlePayment function (to avoid adjusting code of that has been verified we do not adjust the method itself) // baseGas = 0, gasPrice = 1 and gas = payment => amount = (payment + 0) * 1 = payment handlePayment(payment, 0, 1, paymentToken, paymentReceiver); } emit SafeSetup(msg.sender, _owners, _threshold, to, fallbackHandler); } /// @dev Allows to execute a Safe transaction confirmed by required number of owners and then pays the account that submitted the transaction. /// Note: The fees are always transferred, even if the user transaction fails. /// @param to Destination address of Safe transaction. /// @param value Ether value of Safe transaction. /// @param data Data payload of Safe transaction. /// @param operation Operation type of Safe transaction. /// @param safeTxGas Gas that should be used for the Safe transaction. /// @param baseGas Gas costs that are independent of the transaction execution(e.g. base transaction fee, signature check, payment of the refund) /// @param gasPrice Gas price that should be used for the payment calculation. /// @param gasToken Token address (or 0 if ETH) that is used for the payment. /// @param refundReceiver Address of receiver of gas payment (or 0 if tx.origin). /// @param signatures Packed signature data ({bytes32 r}{bytes32 s}{uint8 v}) function execTransaction( address to, uint256 value, bytes calldata data, Enum.Operation operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address payable refundReceiver, bytes memory signatures ) public payable virtual returns (bool success) { bytes32 txHash; // Use scope here to limit variable lifetime and prevent `stack too deep` errors { bytes memory txHashData = encodeTransactionData( // Transaction info to, value, data, operation, safeTxGas, // Payment info baseGas, gasPrice, gasToken, refundReceiver, // Signature info nonce ); // Increase nonce and execute transaction. nonce++; txHash = keccak256(txHashData); checkSignatures(txHash, txHashData, signatures); } address guard = getGuard(); { if (guard != address(0)) { Guard(guard).checkTransaction( // Transaction info to, value, data, operation, safeTxGas, // Payment info baseGas, gasPrice, gasToken, refundReceiver, // Signature info signatures, msg.sender ); } } // We require some gas to emit the events (at least 2500) after the execution and some to perform code until the execution (500) // We also include the 1/64 in the check that is not send along with a call to counteract potential shortings because of EIP-150 require(gasleft() >= ((safeTxGas * 64) / 63).max(safeTxGas + 2500) + 500, "GS010"); // Use scope here to limit variable lifetime and prevent `stack too deep` errors { uint256 gasUsed = gasleft(); // If the gasPrice is 0 we assume that nearly all available gas can be used (it is always more than safeTxGas) // We only substract 2500 (compared to the 3000 before) to ensure that the amount passed is still higher than safeTxGas success = execute(to, value, data, operation, gasPrice == 0 ? (gasleft() - 2500) : safeTxGas); gasUsed = gasUsed.sub(gasleft()); // If no safeTxGas and no gasPrice was set (e.g. both are 0), then the internal tx is required to be successful // This makes it possible to use `estimateGas` without issues, as it searches for the minimum gas where the tx doesn't revert require(success || safeTxGas != 0 || gasPrice != 0, "GS013"); // We transfer the calculated tx costs to the tx.origin to avoid sending it to intermediate contracts that have made calls uint256 payment = 0; if (gasPrice > 0) { payment = handlePayment(gasUsed, baseGas, gasPrice, gasToken, refundReceiver); } if (success) emit ExecutionSuccess(txHash, payment); else emit ExecutionFailure(txHash, payment); } { if (guard != address(0)) { Guard(guard).checkAfterExecution(txHash, success); } } } function handlePayment( uint256 gasUsed, uint256 baseGas, uint256 gasPrice, address gasToken, address payable refundReceiver ) private returns (uint256 payment) { // solhint-disable-next-line avoid-tx-origin address payable receiver = refundReceiver == address(0) ? payable(tx.origin) : refundReceiver; if (gasToken == address(0)) { // For ETH we will only adjust the gas price to not be higher than the actual used gas price payment = gasUsed.add(baseGas).mul(gasPrice < tx.gasprice ? gasPrice : tx.gasprice); require(receiver.send(payment), "GS011"); } else { payment = gasUsed.add(baseGas).mul(gasPrice); require(transferToken(gasToken, receiver, payment), "GS012"); } } /** * @dev Checks whether the signature provided is valid for the provided data, hash. Will revert otherwise. * @param dataHash Hash of the data (could be either a message hash or transaction hash) * @param data That should be signed (this is passed to an external validator contract) * @param signatures Signature data that should be verified. Can be ECDSA signature, contract signature (EIP-1271) or approved hash. */ function checkSignatures( bytes32 dataHash, bytes memory data, bytes memory signatures ) public view { // Load threshold to avoid multiple storage loads uint256 _threshold = threshold; // Check that a threshold is set require(_threshold > 0, "GS001"); checkNSignatures(dataHash, data, signatures, _threshold); } /** * @dev Checks whether the signature provided is valid for the provided data, hash. Will revert otherwise. * @param dataHash Hash of the data (could be either a message hash or transaction hash) * @param data That should be signed (this is passed to an external validator contract) * @param signatures Signature data that should be verified. Can be ECDSA signature, contract signature (EIP-1271) or approved hash. * @param requiredSignatures Amount of required valid signatures. */ function checkNSignatures( bytes32 dataHash, bytes memory data, bytes memory signatures, uint256 requiredSignatures ) public view { // Check that the provided signature data is not too short require(signatures.length >= requiredSignatures.mul(65), "GS020"); // There cannot be an owner with address 0. address lastOwner = address(0); address currentOwner; uint8 v; bytes32 r; bytes32 s; uint256 i; for (i = 0; i < requiredSignatures; i++) { (v, r, s) = signatureSplit(signatures, i); if (v == 0) { // If v is 0 then it is a contract signature // When handling contract signatures the address of the contract is encoded into r currentOwner = address(uint160(uint256(r))); // Check that signature data pointer (s) is not pointing inside the static part of the signatures bytes // This check is not completely accurate, since it is possible that more signatures than the threshold are send. // Here we only check that the pointer is not pointing inside the part that is being processed require(uint256(s) >= requiredSignatures.mul(65), "GS021"); // Check that signature data pointer (s) is in bounds (points to the length of data -> 32 bytes) require(uint256(s).add(32) <= signatures.length, "GS022"); // Check if the contract signature is in bounds: start of data is s + 32 and end is start + signature length uint256 contractSignatureLen; // solhint-disable-next-line no-inline-assembly assembly { contractSignatureLen := mload(add(add(signatures, s), 0x20)) } require(uint256(s).add(32).add(contractSignatureLen) <= signatures.length, "GS023"); // Check signature bytes memory contractSignature; // solhint-disable-next-line no-inline-assembly assembly { // The signature data for contract signatures is appended to the concatenated signatures and the offset is stored in s contractSignature := add(add(signatures, s), 0x20) } require(ISignatureValidator(currentOwner).isValidSignature(data, contractSignature) == EIP1271_MAGIC_VALUE, "GS024"); } else if (v == 1) { // If v is 1 then it is an approved hash // When handling approved hashes the address of the approver is encoded into r currentOwner = address(uint160(uint256(r))); // Hashes are automatically approved by the sender of the message or when they have been pre-approved via a separate transaction require(msg.sender == currentOwner || approvedHashes[currentOwner][dataHash] != 0, "GS025"); } else if (v > 30) { // If v > 30 then default va (27,28) has been adjusted for eth_sign flow // To support eth_sign and similar we adjust v and hash the messageHash with the Ethereum message prefix before applying ecrecover currentOwner = ecrecover(keccak256(abi.encodePacked("\\x19Ethereum Signed Message:\ 32", dataHash)), v - 4, r, s); } else { // Default is the ecrecover flow with the provided data hash // Use ecrecover with the messageHash for EOA signatures currentOwner = ecrecover(dataHash, v, r, s); } require(currentOwner > lastOwner && owners[currentOwner] != address(0) && currentOwner != SENTINEL_OWNERS, "GS026"); lastOwner = currentOwner; } } /// @dev Allows to estimate a Safe transaction. /// This method is only meant for estimation purpose, therefore the call will always revert and encode the result in the revert data. /// Since the `estimateGas` function includes refunds, call this method to get an estimated of the costs that are deducted from the safe with `execTransaction` /// @param to Destination address of Safe transaction. /// @param value Ether value of Safe transaction. /// @param data Data payload of Safe transaction. /// @param operation Operation type of Safe transaction. /// @return Estimate without refunds and overhead fees (base transaction and payload data gas costs). /// @notice Deprecated in favor of common/StorageAccessible.sol and will be removed in next version. function requiredTxGas( address to, uint256 value, bytes calldata data, Enum.Operation operation ) external returns (uint256) { uint256 startGas = gasleft(); // We don't provide an error message here, as we use it to return the estimate require(execute(to, value, data, operation, gasleft())); uint256 requiredGas = startGas - gasleft(); // Convert response to string and return via error message revert(string(abi.encodePacked(requiredGas))); } /** * @dev Marks a hash as approved. This can be used to validate a hash that is used by a signature. * @param hashToApprove The hash that should be marked as approved for signatures that are verified by this contract. */ function approveHash(bytes32 hashToApprove) external { require(owners[msg.sender] != address(0), "GS030"); approvedHashes[msg.sender][hashToApprove] = 1; emit ApproveHash(hashToApprove, msg.sender); } /// @dev Returns the chain id used by this contract. function getChainId() public view returns (uint256) { uint256 id; // solhint-disable-next-line no-inline-assembly assembly { id := chainid() } return id; } function domainSeparator() public view returns (bytes32) { return keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, getChainId(), this)); } /// @dev Returns the bytes that are hashed to be signed by owners. /// @param to Destination address. /// @param value Ether value. /// @param data Data payload. /// @param operation Operation type. /// @param safeTxGas Gas that should be used for the safe transaction. /// @param baseGas Gas costs for that are independent of the transaction execution(e.g. base transaction fee, signature check, payment of the refund) /// @param gasPrice Maximum gas price that should be used for this transaction. /// @param gasToken Token address (or 0 if ETH) that is used for the payment. /// @param refundReceiver Address of receiver of gas payment (or 0 if tx.origin). /// @param _nonce Transaction nonce. /// @return Transaction hash bytes. function encodeTransactionData( address to, uint256 value, bytes calldata data, Enum.Operation operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, uint256 _nonce ) public view returns (bytes memory) { bytes32 safeTxHash = keccak256( abi.encode( SAFE_TX_TYPEHASH, to, value, keccak256(data), operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, _nonce ) ); return abi.encodePacked(bytes1(0x19), bytes1(0x01), domainSeparator(), safeTxHash); } /// @dev Returns hash to be signed by owners. /// @param to Destination address. /// @param value Ether value. /// @param data Data payload. /// @param operation Operation type. /// @param safeTxGas Fas that should be used for the safe transaction. /// @param baseGas Gas costs for data used to trigger the safe transaction. /// @param gasPrice Maximum gas price that should be used for this transaction. /// @param gasToken Token address (or 0 if ETH) that is used for the payment. /// @param refundReceiver Address of receiver of gas payment (or 0 if tx.origin). /// @param _nonce Transaction nonce. /// @return Transaction hash. function getTransactionHash( address to, uint256 value, bytes calldata data, Enum.Operation operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, uint256 _nonce ) public view returns (bytes32) { return keccak256(encodeTransactionData(to, value, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, _nonce)); } } // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; import "../common/Enum.sol"; /// @title Executor - A contract that can execute transactions /// @author Richard Meissner - <[email protected]> contract Executor { function execute( address to, uint256 value, bytes memory data, Enum.Operation operation, uint256 txGas ) internal returns (bool success) { if (operation == Enum.Operation.DelegateCall) { // solhint-disable-next-line no-inline-assembly assembly { success := delegatecall(txGas, to, add(data, 0x20), mload(data), 0, 0) } } else { // solhint-disable-next-line no-inline-assembly assembly { success := call(txGas, to, value, add(data, 0x20), mload(data), 0, 0) } } } } // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; import "../common/SelfAuthorized.sol"; /// @title Fallback Manager - A contract that manages fallback calls made to this contract /// @author Richard Meissner - <[email protected]> contract FallbackManager is SelfAuthorized { event ChangedFallbackHandler(address handler); // keccak256("fallback_manager.handler.address") bytes32 internal constant FALLBACK_HANDLER_STORAGE_SLOT = 0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5; function internalSetFallbackHandler(address handler) internal { bytes32 slot = FALLBACK_HANDLER_STORAGE_SLOT; // solhint-disable-next-line no-inline-assembly assembly { sstore(slot, handler) } } /// @dev Allows to add a contract to handle fallback calls. /// Only fallback calls without value and with data will be forwarded. /// This can only be done via a Safe transaction. /// @param handler contract to handle fallbacks calls. function setFallbackHandler(address handler) public authorized { internalSetFallbackHandler(handler); emit ChangedFallbackHandler(handler); } // solhint-disable-next-line payable-fallback,no-complex-fallback fallback() external { bytes32 slot = FALLBACK_HANDLER_STORAGE_SLOT; // solhint-disable-next-line no-inline-assembly assembly { let handler := sload(slot) if iszero(handler) { return(0, 0) } calldatacopy(0, 0, calldatasize()) // The msg.sender address is shifted to the left by 12 bytes to remove the padding // Then the address without padding is stored right after the calldata mstore(calldatasize(), shl(96, caller())) // Add 20 bytes for the address appended add the end let success := call(gas(), handler, 0, 0, add(calldatasize(), 20), 0, 0) returndatacopy(0, 0, returndatasize()) if iszero(success) { revert(0, returndatasize()) } return(0, returndatasize()) } } } // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; import "../common/Enum.sol"; import "../common/SelfAuthorized.sol"; interface Guard { function checkTransaction( address to, uint256 value, bytes memory data, Enum.Operation operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address payable refundReceiver, bytes memory signatures, address msgSender ) external; function checkAfterExecution(bytes32 txHash, bool success) external; } /// @title Fallback Manager - A contract that manages fallback calls made to this contract /// @author Richard Meissner - <[email protected]> contract GuardManager is SelfAuthorized { event ChangedGuard(address guard); // keccak256("guard_manager.guard.address") bytes32 internal constant GUARD_STORAGE_SLOT = 0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8; /// @dev Set a guard that checks transactions before execution /// @param guard The address of the guard to be used or the 0 address to disable the guard function setGuard(address guard) external authorized { bytes32 slot = GUARD_STORAGE_SLOT; // solhint-disable-next-line no-inline-assembly assembly { sstore(slot, guard) } emit ChangedGuard(guard); } function getGuard() internal view returns (address guard) { bytes32 slot = GUARD_STORAGE_SLOT; // solhint-disable-next-line no-inline-assembly assembly { guard := sload(slot) } } } // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; import "../common/Enum.sol"; import "../common/SelfAuthorized.sol"; import "./Executor.sol"; /// @title Module Manager - A contract that manages modules that can execute transactions via this contract /// @author Stefan George - <[email protected]> /// @author Richard Meissner - <[email protected]> contract ModuleManager is SelfAuthorized, Executor { event EnabledModule(address module); event DisabledModule(address module); event ExecutionFromModuleSuccess(address indexed module); event ExecutionFromModuleFailure(address indexed module); address internal constant SENTINEL_MODULES = address(0x1); mapping(address => address) internal modules; function setupModules(address to, bytes memory data) internal { require(modules[SENTINEL_MODULES] == address(0), "GS100"); modules[SENTINEL_MODULES] = SENTINEL_MODULES; if (to != address(0)) // Setup has to complete successfully or transaction fails. require(execute(to, 0, data, Enum.Operation.DelegateCall, gasleft()), "GS000"); } /// @dev Allows to add a module to the whitelist. /// This can only be done via a Safe transaction. /// @notice Enables the module `module` for the Safe. /// @param module Module to be whitelisted. function enableModule(address module) public authorized { // Module address cannot be null or sentinel. require(module != address(0) && module != SENTINEL_MODULES, "GS101"); // Module cannot be added twice. require(modules[module] == address(0), "GS102"); modules[module] = modules[SENTINEL_MODULES]; modules[SENTINEL_MODULES] = module; emit EnabledModule(module); } /// @dev Allows to remove a module from the whitelist. /// This can only be done via a Safe transaction. /// @notice Disables the module `module` for the Safe. /// @param prevModule Module that pointed to the module to be removed in the linked list /// @param module Module to be removed. function disableModule(address prevModule, address module) public authorized { // Validate module address and check that it corresponds to module index. require(module != address(0) && module != SENTINEL_MODULES, "GS101"); require(modules[prevModule] == module, "GS103"); modules[prevModule] = modules[module]; modules[module] = address(0); emit DisabledModule(module); } /// @dev Allows a Module to execute a Safe transaction without any further confirmations. /// @param to Destination address of module transaction. /// @param value Ether value of module transaction. /// @param data Data payload of module transaction. /// @param operation Operation type of module transaction. function execTransactionFromModule( address to, uint256 value, bytes memory data, Enum.Operation operation ) public virtual returns (bool success) { // Only whitelisted modules are allowed. require(msg.sender != SENTINEL_MODULES && modules[msg.sender] != address(0), "GS104"); // Execute transaction without further confirmations. success = execute(to, value, data, operation, gasleft()); if (success) emit ExecutionFromModuleSuccess(msg.sender); else emit ExecutionFromModuleFailure(msg.sender); } /// @dev Allows a Module to execute a Safe transaction without any further confirmations and return data /// @param to Destination address of module transaction. /// @param value Ether value of module transaction. /// @param data Data payload of module transaction. /// @param operation Operation type of module transaction. function execTransactionFromModuleReturnData( address to, uint256 value, bytes memory data, Enum.Operation operation ) public returns (bool success, bytes memory returnData) { success = execTransactionFromModule(to, value, data, operation); // solhint-disable-next-line no-inline-assembly assembly { // Load free memory location let ptr := mload(0x40) // We allocate memory for the return data by setting the free memory location to // current free memory location + data size + 32 bytes for data size value mstore(0x40, add(ptr, add(returndatasize(), 0x20))) // Store the size mstore(ptr, returndatasize()) // Store the data returndatacopy(add(ptr, 0x20), 0, returndatasize()) // Point the return data to the correct memory location returnData := ptr } } /// @dev Returns if an module is enabled /// @return True if the module is enabled function isModuleEnabled(address module) public view returns (bool) { return SENTINEL_MODULES != module && modules[module] != address(0); } /// @dev Returns array of modules. /// @param start Start of the page. /// @param pageSize Maximum number of modules that should be returned. /// @return array Array of modules. /// @return next Start of the next page. function getModulesPaginated(address start, uint256 pageSize) external view returns (address[] memory array, address next) { // Init array with max page size array = new address[](pageSize); // Populate return array uint256 moduleCount = 0; address currentModule = modules[start]; while (currentModule != address(0x0) && currentModule != SENTINEL_MODULES && moduleCount < pageSize) { array[moduleCount] = currentModule; currentModule = modules[currentModule]; moduleCount++; } next = currentModule; // Set correct size of returned array // solhint-disable-next-line no-inline-assembly assembly { mstore(array, moduleCount) } } } // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; import "../common/SelfAuthorized.sol"; /// @title OwnerManager - Manages a set of owners and a threshold to perform actions. /// @author Stefan George - <[email protected]> /// @author Richard Meissner - <[email protected]> contract OwnerManager is SelfAuthorized { event AddedOwner(address owner); event RemovedOwner(address owner); event ChangedThreshold(uint256 threshold); address internal constant SENTINEL_OWNERS = address(0x1); mapping(address => address) internal owners; uint256 internal ownerCount; uint256 internal threshold; /// @dev Setup function sets initial storage of contract. /// @param _owners List of Safe owners. /// @param _threshold Number of required confirmations for a Safe transaction. function setupOwners(address[] memory _owners, uint256 _threshold) internal { // Threshold can only be 0 at initialization. // Check ensures that setup function can only be called once. require(threshold == 0, "GS200"); // Validate that threshold is smaller than number of added owners. require(_threshold <= _owners.length, "GS201"); // There has to be at least one Safe owner. require(_threshold >= 1, "GS202"); // Initializing Safe owners. address currentOwner = SENTINEL_OWNERS; for (uint256 i = 0; i < _owners.length; i++) { // Owner address cannot be null. address owner = _owners[i]; require(owner != address(0) && owner != SENTINEL_OWNERS && owner != address(this) && currentOwner != owner, "GS203"); // No duplicate owners allowed. require(owners[owner] == address(0), "GS204"); owners[currentOwner] = owner; currentOwner = owner; } owners[currentOwner] = SENTINEL_OWNERS; ownerCount = _owners.length; threshold = _threshold; } /// @dev Allows to add a new owner to the Safe and update the threshold at the same time. /// This can only be done via a Safe transaction. /// @notice Adds the owner `owner` to the Safe and updates the threshold to `_threshold`. /// @param owner New owner address. /// @param _threshold New threshold. function addOwnerWithThreshold(address owner, uint256 _threshold) public authorized { // Owner address cannot be null, the sentinel or the Safe itself. require(owner != address(0) && owner != SENTINEL_OWNERS && owner != address(this), "GS203"); // No duplicate owners allowed. require(owners[owner] == address(0), "GS204"); owners[owner] = owners[SENTINEL_OWNERS]; owners[SENTINEL_OWNERS] = owner; ownerCount++; emit AddedOwner(owner); // Change threshold if threshold was changed. if (threshold != _threshold) changeThreshold(_threshold); } /// @dev Allows to remove an owner from the Safe and update the threshold at the same time. /// This can only be done via a Safe transaction. /// @notice Removes the owner `owner` from the Safe and updates the threshold to `_threshold`. /// @param prevOwner Owner that pointed to the owner to be removed in the linked list /// @param owner Owner address to be removed. /// @param _threshold New threshold. function removeOwner( address prevOwner, address owner, uint256 _threshold ) public authorized { // Only allow to remove an owner, if threshold can still be reached. require(ownerCount - 1 >= _threshold, "GS201"); // Validate owner address and check that it corresponds to owner index. require(owner != address(0) && owner != SENTINEL_OWNERS, "GS203"); require(owners[prevOwner] == owner, "GS205"); owners[prevOwner] = owners[owner]; owners[owner] = address(0); ownerCount--; emit RemovedOwner(owner); // Change threshold if threshold was changed. if (threshold != _threshold) changeThreshold(_threshold); } /// @dev Allows to swap/replace an owner from the Safe with another address. /// This can only be done via a Safe transaction. /// @notice Replaces the owner `oldOwner` in the Safe with `newOwner`. /// @param prevOwner Owner that pointed to the owner to be replaced in the linked list /// @param oldOwner Owner address to be replaced. /// @param newOwner New owner address. function swapOwner( address prevOwner, address oldOwner, address newOwner ) public authorized { // Owner address cannot be null, the sentinel or the Safe itself. require(newOwner != address(0) && newOwner != SENTINEL_OWNERS && newOwner != address(this), "GS203"); // No duplicate owners allowed. require(owners[newOwner] == address(0), "GS204"); // Validate oldOwner address and check that it corresponds to owner index. require(oldOwner != address(0) && oldOwner != SENTINEL_OWNERS, "GS203"); require(owners[prevOwner] == oldOwner, "GS205"); owners[newOwner] = owners[oldOwner]; owners[prevOwner] = newOwner; owners[oldOwner] = address(0); emit RemovedOwner(oldOwner); emit AddedOwner(newOwner); } /// @dev Allows to update the number of required confirmations by Safe owners. /// This can only be done via a Safe transaction. /// @notice Changes the threshold of the Safe to `_threshold`. /// @param _threshold New threshold. function changeThreshold(uint256 _threshold) public authorized { // Validate that threshold is smaller than number of owners. require(_threshold <= ownerCount, "GS201"); // There has to be at least one Safe owner. require(_threshold >= 1, "GS202"); threshold = _threshold; emit ChangedThreshold(threshold); } function getThreshold() public view returns (uint256) { return threshold; } function isOwner(address owner) public view returns (bool) { return owner != SENTINEL_OWNERS && owners[owner] != address(0); } /// @dev Returns array of owners. /// @return Array of Safe owners. function getOwners() public view returns (address[] memory) { address[] memory array = new address[](ownerCount); // populate return array uint256 index = 0; address currentOwner = owners[SENTINEL_OWNERS]; while (currentOwner != SENTINEL_OWNERS) { array[index] = currentOwner; currentOwner = owners[currentOwner]; index++; } return array; } } // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; /// @title Enum - Collection of enums /// @author Richard Meissner - <[email protected]> contract Enum { enum Operation {Call, DelegateCall} } // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; /// @title EtherPaymentFallback - A contract that has a fallback to accept ether payments /// @author Richard Meissner - <[email protected]> contract EtherPaymentFallback { event SafeReceived(address indexed sender, uint256 value); /// @dev Fallback function accepts Ether transactions. receive() external payable { emit SafeReceived(msg.sender, msg.value); } } // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; /// @title SecuredTokenTransfer - Secure token transfer /// @author Richard Meissner - <[email protected]> contract SecuredTokenTransfer { /// @dev Transfers a token and returns if it was a success /// @param token Token that should be transferred /// @param receiver Receiver to whom the token should be transferred /// @param amount The amount of tokens that should be transferred function transferToken( address token, address receiver, uint256 amount ) internal returns (bool transferred) { // 0xa9059cbb - keccack("transfer(address,uint256)") bytes memory data = abi.encodeWithSelector(0xa9059cbb, receiver, amount); // solhint-disable-next-line no-inline-assembly assembly { // We write the return value to scratch space. // See https://docs.soliditylang.org/en/v0.7.6/internals/layout_in_memory.html#layout-in-memory let success := call(sub(gas(), 10000), token, 0, add(data, 0x20), mload(data), 0, 0x20) switch returndatasize() case 0 { transferred := success } case 0x20 { transferred := iszero(or(iszero(success), iszero(mload(0)))) } default { transferred := 0 } } } } // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; /// @title SelfAuthorized - authorizes current contract to perform actions /// @author Richard Meissner - <[email protected]> contract SelfAuthorized { function requireSelfCall() private view { require(msg.sender == address(this), "GS031"); } modifier authorized() { // This is a function call as it minimized the bytecode size requireSelfCall(); _; } } // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; /// @title SignatureDecoder - Decodes signatures that a encoded as bytes /// @author Richard Meissner - <[email protected]> contract SignatureDecoder { /// @dev divides bytes signature into `uint8 v, bytes32 r, bytes32 s`. /// @notice Make sure to peform a bounds check for @param pos, to avoid out of bounds access on @param signatures /// @param pos which signature to read. A prior bounds check of this parameter should be performed, to avoid out of bounds access /// @param signatures concatenated rsv signatures function signatureSplit(bytes memory signatures, uint256 pos) internal pure returns ( uint8 v, bytes32 r, bytes32 s ) { // The signature format is a compact form of: // {bytes32 r}{bytes32 s}{uint8 v} // Compact means, uint8 is not padded to 32 bytes. // solhint-disable-next-line no-inline-assembly assembly { let signaturePos := mul(0x41, pos) r := mload(add(signatures, add(signaturePos, 0x20))) s := mload(add(signatures, add(signaturePos, 0x40))) // Here we are loading the last 32 bytes, including 31 bytes // of 's'. There is no 'mload8' to do this. // // 'byte' is not working due to the Solidity parser, so lets // use the second best option, 'and' v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff) } } } // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; /// @title Singleton - Base for singleton contracts (should always be first super contract) /// This contract is tightly coupled to our proxy contract (see `proxies/GnosisSafeProxy.sol`) /// @author Richard Meissner - <[email protected]> contract Singleton { // singleton always needs to be first declared variable, to ensure that it is at the same location as in the Proxy contract. // It should also always be ensured that the address is stored alone (uses a full word) address private singleton; } // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; /// @title StorageAccessible - generic base contract that allows callers to access all internal storage. /// @notice See https://github.com/gnosis/util-contracts/blob/bb5fe5fb5df6d8400998094fb1b32a178a47c3a1/contracts/StorageAccessible.sol contract StorageAccessible { /** * @dev Reads `length` bytes of storage in the currents contract * @param offset - the offset in the current contract's storage in words to start reading from * @param length - the number of words (32 bytes) of data to read * @return the bytes that were read. */ function getStorageAt(uint256 offset, uint256 length) public view returns (bytes memory) { bytes memory result = new bytes(length * 32); for (uint256 index = 0; index < length; index++) { // solhint-disable-next-line no-inline-assembly assembly { let word := sload(add(offset, index)) mstore(add(add(result, 0x20), mul(index, 0x20)), word) } } return result; } /** * @dev Performs a delegetecall on a targetContract in the context of self. * Internally reverts execution to avoid side effects (making it static). * * This method reverts with data equal to `abi.encode(bool(success), bytes(response))`. * Specifically, the `returndata` after a call to this method will be: * `success:bool || response.length:uint256 || response:bytes`. * * @param targetContract Address of the contract containing the code to execute. * @param calldataPayload Calldata that should be sent to the target contract (encoded method name and arguments). */ function simulateAndRevert(address targetContract, bytes memory calldataPayload) external { // solhint-disable-next-line no-inline-assembly assembly { let success := delegatecall(gas(), targetContract, add(calldataPayload, 0x20), mload(calldataPayload), 0, 0) mstore(0x00, success) mstore(0x20, returndatasize()) returndatacopy(0x40, 0, returndatasize()) revert(0, add(returndatasize(), 0x40)) } } } // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; /** * @title GnosisSafeMath * @dev Math operations with safety checks that revert on error * Renamed from SafeMath to GnosisSafeMath to avoid conflicts * TODO: remove once open zeppelin update to solc 0.5.0 */ library GnosisSafeMath { /** * @dev Multiplies two numbers, reverts on overflow. */ function mul(uint256 a, uint256 b) internal pure returns (uint256) { // Gas optimization: this is cheaper than requiring 'a' not being zero, but the // benefit is lost if 'b' is also tested. // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522 if (a == 0) { return 0; } uint256 c = a * b; require(c / a == b); return c; } /** * @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend). */ function sub(uint256 a, uint256 b) internal pure returns (uint256) { require(b <= a); uint256 c = a - b; return c; } /** * @dev Adds two numbers, reverts on overflow. */ function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; require(c >= a); return c; } /** * @dev Returns the largest of two numbers. */ function max(uint256 a, uint256 b) internal pure returns (uint256) { return a >= b ? a : b; } } // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.7.0 <0.9.0; contract ISignatureValidatorConstants { // bytes4(keccak256("isValidSignature(bytes,bytes)") bytes4 internal constant EIP1271_MAGIC_VALUE = 0x20c13b0b; } abstract contract ISignatureValidator is ISignatureValidatorConstants { /** * @dev Should return whether the signature provided is valid for the provided data * @param _data Arbitrary length data signed on the behalf of address(this) * @param _signature Signature byte array associated with _data * * MUST return the bytes4 magic value 0x20c13b0b when function passes. * MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5) * MUST allow external calls */ function isValidSignature(bytes memory _data, bytes memory _signature) public view virtual returns (bytes4); }
File 7 of 7: AgentRegistry
// SPDX-License-Identifier: MIT pragma solidity ^0.8.15; import "./UnitRegistry.sol"; import "./interfaces/IRegistry.sol"; /// @title Agent Registry - Smart contract for registering agents /// @author Aleksandr Kuperman - <[email protected]> contract AgentRegistry is UnitRegistry { // Component registry address public immutable componentRegistry; // Agent registry version number string public constant VERSION = "1.0.0"; /// @dev Agent registry constructor. /// @param _name Agent registry contract name. /// @param _symbol Agent registry contract symbol. /// @param _baseURI Agent registry token base URI. /// @param _componentRegistry Component registry address. constructor(string memory _name, string memory _symbol, string memory _baseURI, address _componentRegistry) UnitRegistry(UnitType.Agent) ERC721(_name, _symbol) { baseURI = _baseURI; componentRegistry = _componentRegistry; owner = msg.sender; } /// @dev Checks provided component dependencies. /// @param dependencies Set of component dependencies. function _checkDependencies(uint32[] memory dependencies, uint32) internal virtual override { // Check that the agent has at least one component if (dependencies.length == 0) { revert ZeroValue(); } // Get the components total supply uint32 componentTotalSupply = uint32(IRegistry(componentRegistry).totalSupply()); uint32 lastId; for (uint256 iDep = 0; iDep < dependencies.length; ++iDep) { if (dependencies[iDep] < (lastId + 1) || dependencies[iDep] > componentTotalSupply) { revert ComponentNotFound(dependencies[iDep]); } lastId = dependencies[iDep]; } } /// @dev Gets linearized set of subcomponents of a provided unit Id and a type of a component. /// @notice (0) For components this means getting the linearized map of components from the componentRegistry contract. /// @notice (1) For agents this means getting the linearized map of components from the local map of subcomponents. /// @param subcomponentsFromType Type of the unit: component or agent. /// @param unitId Component Id. /// @return subComponentIds Set of subcomponents. function _getSubComponents(UnitType subcomponentsFromType, uint32 unitId) internal view virtual override returns (uint32[] memory subComponentIds) { // Self contract (agent registry) can only call subcomponents calculation from the component level (0) // Otherwise, the subcomponents are already written into the corresponding subcomponents map if (subcomponentsFromType == UnitType.Component) { (subComponentIds, ) = IRegistry(componentRegistry).getLocalSubComponents(uint256(unitId)); } else { subComponentIds = mapSubComponents[uint256(unitId)]; } } /// @dev Calculates the set of subcomponent Ids. /// @notice We assume that the external callers calculate subcomponents from the higher unit hierarchy level: agents. /// @param unitIds Unit Ids. /// @return subComponentIds Subcomponent Ids. function calculateSubComponents(uint32[] memory unitIds) external view returns (uint32[] memory subComponentIds) { subComponentIds = _calculateSubComponents(UnitType.Agent, unitIds); } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; import "./GenericRegistry.sol"; /// @title Unit Registry - Smart contract for registering generalized units / units /// @author Aleksandr Kuperman - <[email protected]> abstract contract UnitRegistry is GenericRegistry { event CreateUnit(uint256 unitId, UnitType uType, bytes32 unitHash); event UpdateUnitHash(uint256 unitId, UnitType uType, bytes32 unitHash); enum UnitType { Component, Agent } // Unit parameters struct Unit { // Primary IPFS hash of the unit bytes32 unitHash; // Set of component dependencies (agents are also based on components) // We assume that the system is expected to support no more than 2^32-1 components uint32[] dependencies; } // Type of the unit: component or unit UnitType public immutable unitType; // Map of unit Id => set of updated IPFS hashes mapping(uint256 => bytes32[]) public mapUnitIdHashes; // Map of unit Id => set of subcomponents (possible to derive from any registry) mapping(uint256 => uint32[]) public mapSubComponents; // Map of unit Id => unit mapping(uint256 => Unit) public mapUnits; constructor(UnitType _unitType) { unitType = _unitType; } /// @dev Checks the provided component dependencies. /// @param dependencies Set of component dependencies. /// @param maxUnitId Maximum unit Id. function _checkDependencies(uint32[] memory dependencies, uint32 maxUnitId) internal virtual; /// @dev Creates unit. /// @param unitOwner Owner of the unit. /// @param unitHash IPFS CID hash of the unit. /// @param dependencies Set of unit dependencies in a sorted ascending order (unit Ids). /// @return unitId The id of a minted unit. function create(address unitOwner, bytes32 unitHash, uint32[] memory dependencies) external virtual returns (uint256 unitId) { // Reentrancy guard if (_locked > 1) { revert ReentrancyGuard(); } _locked = 2; // Check for the manager privilege for a unit creation if (manager != msg.sender) { revert ManagerOnly(msg.sender, manager); } // Checks for a non-zero owner address if(unitOwner == address(0)) { revert ZeroAddress(); } // Check for the non-zero hash value if (unitHash == 0) { revert ZeroValue(); } // Check for dependencies validity: must be already allocated, must not repeat unitId = totalSupply; _checkDependencies(dependencies, uint32(unitId)); // Unit with Id = 0 is left empty not to do additional checks for the index zero unitId++; // Initialize the unit and mint its token Unit storage unit = mapUnits[unitId]; unit.unitHash = unitHash; unit.dependencies = dependencies; // Update the map of subcomponents with calculated subcomponents for the new unit Id // In order to get the correct set of subcomponents, we need to differentiate between the callers of this function // Self contract (unit registry) can only call subcomponents calculation from the component level uint32[] memory subComponentIds = _calculateSubComponents(UnitType.Component, dependencies); // We need to add a current component Id to the set of subcomponents if the unit is a component // For example, if component 3 (c3) has dependencies of [c1, c2], then the subcomponents will return [c1, c2]. // The resulting set will be [c1, c2, c3]. So we write into the map of component subcomponents: c3=>[c1, c2, c3]. // This is done such that the subcomponents start getting explored, and when the agent calls its subcomponents, // it would have [c1, c2, c3] right away instead of adding c3 manually and then (for services) checking // if another agent also has c3 as a component dependency. The latter will consume additional computation. if (unitType == UnitType.Component) { uint256 numSubComponents = subComponentIds.length; uint32[] memory addSubComponentIds = new uint32[](numSubComponents + 1); for (uint256 i = 0; i < numSubComponents; ++i) { addSubComponentIds[i] = subComponentIds[i]; } // Adding self component Id addSubComponentIds[numSubComponents] = uint32(unitId); subComponentIds = addSubComponentIds; } mapSubComponents[unitId] = subComponentIds; // Set total supply to the unit Id number totalSupply = unitId; // Safe mint is needed since contracts can create units as well _safeMint(unitOwner, unitId); emit CreateUnit(unitId, unitType, unitHash); _locked = 1; } /// @dev Updates the unit hash. /// @param unitOwner Owner of the unit. /// @param unitId Unit Id. /// @param unitHash Updated IPFS hash of the unit. /// @return success True, if function executed successfully. function updateHash(address unitOwner, uint256 unitId, bytes32 unitHash) external virtual returns (bool success) { // Check the manager privilege for a unit modification if (manager != msg.sender) { revert ManagerOnly(msg.sender, manager); } // Checking the unit ownership if (ownerOf(unitId) != unitOwner) { if (unitType == UnitType.Component) { revert ComponentNotFound(unitId); } else { revert AgentNotFound(unitId); } } // Check for the hash value if (unitHash == 0) { revert ZeroValue(); } mapUnitIdHashes[unitId].push(unitHash); success = true; emit UpdateUnitHash(unitId, unitType, unitHash); } /// @dev Gets the unit instance. /// @param unitId Unit Id. /// @return unit Corresponding Unit struct. function getUnit(uint256 unitId) external view virtual returns (Unit memory unit) { unit = mapUnits[unitId]; } /// @dev Gets unit dependencies. /// @param unitId Unit Id. /// @return numDependencies The number of units in the dependency list. /// @return dependencies The list of unit dependencies. function getDependencies(uint256 unitId) external view virtual returns (uint256 numDependencies, uint32[] memory dependencies) { Unit memory unit = mapUnits[unitId]; return (unit.dependencies.length, unit.dependencies); } /// @dev Gets updated unit hashes. /// @param unitId Unit Id. /// @return numHashes Number of hashes. /// @return unitHashes The list of updated unit hashes (without the primary one). function getUpdatedHashes(uint256 unitId) external view virtual returns (uint256 numHashes, bytes32[] memory unitHashes) { unitHashes = mapUnitIdHashes[unitId]; return (unitHashes.length, unitHashes); } /// @dev Gets the set of subcomponent Ids from a local map of subcomponent. /// @param unitId Component Id. /// @return subComponentIds Set of subcomponent Ids. /// @return numSubComponents Number of subcomponents. function getLocalSubComponents(uint256 unitId) external view returns (uint32[] memory subComponentIds, uint256 numSubComponents) { subComponentIds = mapSubComponents[uint256(unitId)]; numSubComponents = subComponentIds.length; } /// @dev Gets subcomponents of a provided unit Id. /// @param subcomponentsFromType Type of the unit: component or agent. /// @param unitId Unit Id. /// @return subComponentIds Set of subcomponents. function _getSubComponents(UnitType subcomponentsFromType, uint32 unitId) internal view virtual returns (uint32[] memory subComponentIds); /// @dev Calculates the set of subcomponent Ids. /// @param subcomponentsFromType Type of the unit: component or agent. /// @param unitIds Unit Ids. /// @return subComponentIds Subcomponent Ids. function _calculateSubComponents(UnitType subcomponentsFromType, uint32[] memory unitIds) internal view virtual returns (uint32[] memory subComponentIds) { uint32 numUnits = uint32(unitIds.length); // Array of numbers of components per each unit Id uint32[] memory numComponents = new uint32[](numUnits); // 2D array of all the sets of components per each unit Id uint32[][] memory components = new uint32[][](numUnits); // Get total possible number of components and lists of components uint32 maxNumComponents; for (uint32 i = 0; i < numUnits; ++i) { // Get subcomponents for each unit Id based on the subcomponentsFromType components[i] = _getSubComponents(subcomponentsFromType, unitIds[i]); numComponents[i] = uint32(components[i].length); maxNumComponents += numComponents[i]; } // Lists of components are sorted, take unique values in ascending order uint32[] memory allComponents = new uint32[](maxNumComponents); // Processed component counter uint32[] memory processedComponents = new uint32[](numUnits); // Minimal component Id uint32 minComponent; // Overall component counter uint32 counter; // Iterate until we process all components, at the maximum of the sum of all the components in all units for (counter = 0; counter < maxNumComponents; ++counter) { // Index of a minimal component uint32 minIdxComponent; // Amount of components identified as the next minimal component number uint32 numComponentsCheck; uint32 tryMinComponent = type(uint32).max; // Assemble an array of all first components from each component array for (uint32 i = 0; i < numUnits; ++i) { // Either get a component that has a higher id than the last one ore reach the end of the processed Ids for (; processedComponents[i] < numComponents[i]; ++processedComponents[i]) { if (minComponent < components[i][processedComponents[i]]) { // Out of those component Ids that are higher than the last one, pick the minimal one if (components[i][processedComponents[i]] < tryMinComponent) { tryMinComponent = components[i][processedComponents[i]]; minIdxComponent = i; } // If we found a minimal component Id, we increase the counter and break to start the search again numComponentsCheck++; break; } } } minComponent = tryMinComponent; // If minimal component Id is greater than the last one, it should be added, otherwise we reached the end if (numComponentsCheck > 0) { allComponents[counter] = minComponent; processedComponents[minIdxComponent]++; } else { break; } } // Return the exact set of found subcomponents with the counter length subComponentIds = new uint32[](counter); for (uint32 i = 0; i < counter; ++i) { subComponentIds[i] = allComponents[i]; } } /// @dev Gets the hash of the unit. /// @param unitId Unit Id. /// @return Unit hash. function _getUnitHash(uint256 unitId) internal view override returns (bytes32) { return mapUnits[unitId].unitHash; } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; /// @dev Required interface for the component / agent manipulation. interface IRegistry { enum UnitType { Component, Agent } /// @dev Creates component / agent. /// @param unitOwner Owner of the component / agent. /// @param unitHash IPFS hash of the component / agent. /// @param dependencies Set of component dependencies in a sorted ascending order. /// @return The id of a minted component / agent. function create( address unitOwner, bytes32 unitHash, uint32[] memory dependencies ) external returns (uint256); /// @dev Updates the component / agent hash. /// @param owner Owner of the component / agent. /// @param unitId Unit Id. /// @param unitHash Updated IPFS hash of the component / agent. /// @return success True, if function executed successfully. function updateHash(address owner, uint256 unitId, bytes32 unitHash) external returns (bool success); /// @dev Gets subcomponents of a provided unit Id from a local public map. /// @param unitId Unit Id. /// @return subComponentIds Set of subcomponents. /// @return numSubComponents Number of subcomponents. function getLocalSubComponents(uint256 unitId) external view returns (uint32[] memory subComponentIds, uint256 numSubComponents); /// @dev Calculates the set of subcomponent Ids. /// @param unitIds Set of unit Ids. /// @return subComponentIds Subcomponent Ids. function calculateSubComponents(uint32[] memory unitIds) external view returns (uint32[] memory subComponentIds); /// @dev Gets updated component / agent hashes. /// @param unitId Unit Id. /// @return numHashes Number of hashes. /// @return unitHashes The list of component / agent hashes. function getUpdatedHashes(uint256 unitId) external view returns (uint256 numHashes, bytes32[] memory unitHashes); /// @dev Gets the total supply of components / agents. /// @return Total supply. function totalSupply() external view returns (uint256); } // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; import "../lib/solmate/src/tokens/ERC721.sol"; import "./interfaces/IErrorsRegistries.sol"; /// @title Generic Registry - Smart contract for generic registry template /// @author Aleksandr Kuperman - <[email protected]> abstract contract GenericRegistry is IErrorsRegistries, ERC721 { event OwnerUpdated(address indexed owner); event ManagerUpdated(address indexed manager); event BaseURIChanged(string baseURI); // Owner address address public owner; // Unit manager address public manager; // Base URI string public baseURI; // Unit counter uint256 public totalSupply; // Reentrancy lock uint256 internal _locked = 1; // To better understand the CID anatomy, please refer to: https://proto.school/anatomy-of-a-cid/05 // CID = <multibase_encoding>multibase_encoding(<cid-version><multicodec><multihash-algorithm><multihash-length><multihash-hash>) // CID prefix = <multibase_encoding>multibase_encoding(<cid-version><multicodec><multihash-algorithm><multihash-length>) // to complement the multibase_encoding(<multihash-hash>) // multibase_encoding = base16 = "f" // cid-version = version 1 = "0x01" // multicodec = dag-pb = "0x70" // multihash-algorithm = sha2-256 = "0x12" // multihash-length = 256 bits = "0x20" string public constant CID_PREFIX = "f01701220"; /// @dev Changes the owner address. /// @param newOwner Address of a new owner. function changeOwner(address newOwner) external virtual { // Check for the ownership if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } // Check for the zero address if (newOwner == address(0)) { revert ZeroAddress(); } owner = newOwner; emit OwnerUpdated(newOwner); } /// @dev Changes the unit manager. /// @param newManager Address of a new unit manager. function changeManager(address newManager) external virtual { if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } // Check for the zero address if (newManager == address(0)) { revert ZeroAddress(); } manager = newManager; emit ManagerUpdated(newManager); } /// @dev Checks for the unit existence. /// @notice Unit counter starts from 1. /// @param unitId Unit Id. /// @return true if the unit exists, false otherwise. function exists(uint256 unitId) external view virtual returns (bool) { return unitId > 0 && unitId < (totalSupply + 1); } /// @dev Sets unit base URI. /// @param bURI Base URI string. function setBaseURI(string memory bURI) external virtual { // Check for the ownership if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } // Check for the zero value if (bytes(bURI).length == 0) { revert ZeroValue(); } baseURI = bURI; emit BaseURIChanged(bURI); } /// @dev Gets the valid unit Id from the provided index. /// @notice Unit counter starts from 1. /// @param id Unit counter. /// @return unitId Unit Id. function tokenByIndex(uint256 id) external view virtual returns (uint256 unitId) { unitId = id + 1; if (unitId > totalSupply) { revert Overflow(unitId, totalSupply); } } // Open sourced from: https://stackoverflow.com/questions/67893318/solidity-how-to-represent-bytes32-as-string /// @dev Converts bytes16 input data to hex16. /// @notice This method converts bytes into the same bytes-character hex16 representation. /// @param data bytes16 input data. /// @return result hex16 conversion from the input bytes16 data. function _toHex16(bytes16 data) internal pure returns (bytes32 result) { result = bytes32 (data) & 0xFFFFFFFFFFFFFFFF000000000000000000000000000000000000000000000000 | (bytes32 (data) & 0x0000000000000000FFFFFFFFFFFFFFFF00000000000000000000000000000000) >> 64; result = result & 0xFFFFFFFF000000000000000000000000FFFFFFFF000000000000000000000000 | (result & 0x00000000FFFFFFFF000000000000000000000000FFFFFFFF0000000000000000) >> 32; result = result & 0xFFFF000000000000FFFF000000000000FFFF000000000000FFFF000000000000 | (result & 0x0000FFFF000000000000FFFF000000000000FFFF000000000000FFFF00000000) >> 16; result = result & 0xFF000000FF000000FF000000FF000000FF000000FF000000FF000000FF000000 | (result & 0x00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000) >> 8; result = (result & 0xF000F000F000F000F000F000F000F000F000F000F000F000F000F000F000F000) >> 4 | (result & 0x0F000F000F000F000F000F000F000F000F000F000F000F000F000F000F000F00) >> 8; result = bytes32 (0x3030303030303030303030303030303030303030303030303030303030303030 + uint256 (result) + (uint256 (result) + 0x0606060606060606060606060606060606060606060606060606060606060606 >> 4 & 0x0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F) * 39); } /// @dev Gets the hash of the unit. /// @param unitId Unit Id. /// @return Unit hash. function _getUnitHash(uint256 unitId) internal view virtual returns (bytes32); /// @dev Returns unit token URI. /// @notice Expected multicodec: dag-pb; hashing function: sha2-256, with base16 encoding and leading CID_PREFIX removed. /// @param unitId Unit Id. /// @return Unit token URI string. function tokenURI(uint256 unitId) public view virtual override returns (string memory) { bytes32 unitHash = _getUnitHash(unitId); // Parse 2 parts of bytes32 into left and right hex16 representation, and concatenate into string // adding the base URI and a cid prefix for the full base16 multibase prefix IPFS hash representation return string(abi.encodePacked(baseURI, CID_PREFIX, _toHex16(bytes16(unitHash)), _toHex16(bytes16(unitHash << 128)))); } } // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; /// @notice Modern, minimalist, and gas efficient ERC-721 implementation. /// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC721.sol) abstract contract ERC721 { /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ event Transfer(address indexed from, address indexed to, uint256 indexed id); event Approval(address indexed owner, address indexed spender, uint256 indexed id); event ApprovalForAll(address indexed owner, address indexed operator, bool approved); /*////////////////////////////////////////////////////////////// METADATA STORAGE/LOGIC //////////////////////////////////////////////////////////////*/ string public name; string public symbol; function tokenURI(uint256 id) public view virtual returns (string memory); /*////////////////////////////////////////////////////////////// ERC721 BALANCE/OWNER STORAGE //////////////////////////////////////////////////////////////*/ mapping(uint256 => address) internal _ownerOf; mapping(address => uint256) internal _balanceOf; function ownerOf(uint256 id) public view virtual returns (address owner) { require((owner = _ownerOf[id]) != address(0), "NOT_MINTED"); } function balanceOf(address owner) public view virtual returns (uint256) { require(owner != address(0), "ZERO_ADDRESS"); return _balanceOf[owner]; } /*////////////////////////////////////////////////////////////// ERC721 APPROVAL STORAGE //////////////////////////////////////////////////////////////*/ mapping(uint256 => address) public getApproved; mapping(address => mapping(address => bool)) public isApprovedForAll; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ constructor(string memory _name, string memory _symbol) { name = _name; symbol = _symbol; } /*////////////////////////////////////////////////////////////// ERC721 LOGIC //////////////////////////////////////////////////////////////*/ function approve(address spender, uint256 id) public virtual { address owner = _ownerOf[id]; require(msg.sender == owner || isApprovedForAll[owner][msg.sender], "NOT_AUTHORIZED"); getApproved[id] = spender; emit Approval(owner, spender, id); } function setApprovalForAll(address operator, bool approved) public virtual { isApprovedForAll[msg.sender][operator] = approved; emit ApprovalForAll(msg.sender, operator, approved); } function transferFrom( address from, address to, uint256 id ) public virtual { require(from == _ownerOf[id], "WRONG_FROM"); require(to != address(0), "INVALID_RECIPIENT"); require( msg.sender == from || isApprovedForAll[from][msg.sender] || msg.sender == getApproved[id], "NOT_AUTHORIZED" ); // Underflow of the sender's balance is impossible because we check for // ownership above and the recipient's balance can't realistically overflow. unchecked { _balanceOf[from]--; _balanceOf[to]++; } _ownerOf[id] = to; delete getApproved[id]; emit Transfer(from, to, id); } function safeTransferFrom( address from, address to, uint256 id ) public virtual { transferFrom(from, to, id); if (to.code.length != 0) require( ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); } function safeTransferFrom( address from, address to, uint256 id, bytes calldata data ) public virtual { transferFrom(from, to, id); if (to.code.length != 0) require( ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); } /*////////////////////////////////////////////////////////////// ERC165 LOGIC //////////////////////////////////////////////////////////////*/ function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { return interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata } /*////////////////////////////////////////////////////////////// INTERNAL MINT/BURN LOGIC //////////////////////////////////////////////////////////////*/ function _mint(address to, uint256 id) internal virtual { require(to != address(0), "INVALID_RECIPIENT"); require(_ownerOf[id] == address(0), "ALREADY_MINTED"); // Counter overflow is incredibly unrealistic. unchecked { _balanceOf[to]++; } _ownerOf[id] = to; emit Transfer(address(0), to, id); } function _burn(uint256 id) internal virtual { address owner = _ownerOf[id]; require(owner != address(0), "NOT_MINTED"); // Ownership check above ensures no underflow. unchecked { _balanceOf[owner]--; } delete _ownerOf[id]; delete getApproved[id]; emit Transfer(owner, address(0), id); } /*////////////////////////////////////////////////////////////// INTERNAL SAFE MINT LOGIC //////////////////////////////////////////////////////////////*/ function _safeMint(address to, uint256 id) internal virtual { _mint(to, id); if (to.code.length != 0) require( ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, "") == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); } function _safeMint( address to, uint256 id, bytes memory data ) internal virtual { _mint(to, id); if (to.code.length != 0) require( ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, data) == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); } } /// @notice A generic interface for a contract which properly accepts ERC721 tokens. /// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC721.sol) abstract contract ERC721TokenReceiver { function onERC721Received( address, address, uint256, bytes calldata ) external virtual returns (bytes4) { return ERC721TokenReceiver.onERC721Received.selector; } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; /// @dev Errors. interface IErrorsRegistries { /// @dev Only `manager` has a privilege, but the `sender` was provided. /// @param sender Sender address. /// @param manager Required sender address as a manager. error ManagerOnly(address sender, address manager); /// @dev Only `owner` has a privilege, but the `sender` was provided. /// @param sender Sender address. /// @param owner Required sender address as an owner. error OwnerOnly(address sender, address owner); /// @dev Hash already exists in the records. error HashExists(); /// @dev Provided zero address. error ZeroAddress(); /// @dev Agent Id is not correctly provided for the current routine. /// @param agentId Component Id. error WrongAgentId(uint256 agentId); /// @dev Wrong length of two arrays. /// @param numValues1 Number of values in a first array. /// @param numValues2 Numberf of values in a second array. error WrongArrayLength(uint256 numValues1, uint256 numValues2); /// @dev Canonical agent Id is not found. /// @param agentId Canonical agent Id. error AgentNotFound(uint256 agentId); /// @dev Component Id is not found. /// @param componentId Component Id. error ComponentNotFound(uint256 componentId); /// @dev Multisig threshold is out of bounds. /// @param currentThreshold Current threshold value. /// @param minThreshold Minimum possible threshold value. /// @param maxThreshold Maximum possible threshold value. error WrongThreshold(uint256 currentThreshold, uint256 minThreshold, uint256 maxThreshold); /// @dev Agent instance is already registered with a specified `operator`. /// @param operator Operator that registered an instance. error AgentInstanceRegistered(address operator); /// @dev Wrong operator is specified when interacting with a specified `serviceId`. /// @param serviceId Service Id. error WrongOperator(uint256 serviceId); /// @dev Operator has no registered instances in the service. /// @param operator Operator address. /// @param serviceId Service Id. error OperatorHasNoInstances(address operator, uint256 serviceId); /// @dev Canonical `agentId` is not found as a part of `serviceId`. /// @param agentId Canonical agent Id. /// @param serviceId Service Id. error AgentNotInService(uint256 agentId, uint256 serviceId); /// @dev The contract is paused. error Paused(); /// @dev Zero value when it has to be different from zero. error ZeroValue(); /// @dev Value overflow. /// @param provided Overflow value. /// @param max Maximum possible value. error Overflow(uint256 provided, uint256 max); /// @dev Service must be inactive. /// @param serviceId Service Id. error ServiceMustBeInactive(uint256 serviceId); /// @dev All the agent instance slots for a specific `serviceId` are filled. /// @param serviceId Service Id. error AgentInstancesSlotsFilled(uint256 serviceId); /// @dev Wrong state of a service. /// @param state Service state. /// @param serviceId Service Id. error WrongServiceState(uint256 state, uint256 serviceId); /// @dev Only own service multisig is allowed. /// @param provided Provided address. /// @param expected Expected multisig address. /// @param serviceId Service Id. error OnlyOwnServiceMultisig(address provided, address expected, uint256 serviceId); /// @dev Multisig is not whitelisted. /// @param multisig Address of a multisig implementation. error UnauthorizedMultisig(address multisig); /// @dev Incorrect deposit provided for the registration activation. /// @param sent Sent amount. /// @param expected Expected amount. /// @param serviceId Service Id. error IncorrectRegistrationDepositValue(uint256 sent, uint256 expected, uint256 serviceId); /// @dev Insufficient value provided for the agent instance bonding. /// @param sent Sent amount. /// @param expected Expected amount. /// @param serviceId Service Id. error IncorrectAgentBondingValue(uint256 sent, uint256 expected, uint256 serviceId); /// @dev Failure of a transfer. /// @param token Address of a token. /// @param from Address `from`. /// @param to Address `to`. /// @param value Value. error TransferFailed(address token, address from, address to, uint256 value); /// @dev Caught reentrancy violation. error ReentrancyGuard(); }