-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathOperatorSignedHashes.sol
275 lines (249 loc) · 11.3 KB
/
OperatorSignedHashes.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
// 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 - <aleksandr.kuperman@valory.xyz>
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];
}
}