This repository has been archived by the owner on Nov 27, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
/
VertexPolicy.sol
400 lines (333 loc) · 18.1 KB
/
VertexPolicy.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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {LibString} from "@solady/utils/LibString.sol";
import {Base64} from "@openzeppelin/utils/Base64.sol";
import {VertexFactory} from "src/VertexFactory.sol";
import {ERC721NonTransferableMinimalProxy} from "src/lib/ERC721NonTransferableMinimalProxy.sol";
import {Checkpoints} from "src/lib/Checkpoints.sol";
import {RoleHolderData, RolePermissionData} from "src/lib/Structs.sol";
import {RoleDescription} from "src/lib/UDVTs.sol";
/// @title Vertex Policy
/// @author Llama (vertex@llama.xyz)
/// @notice An ERC721 contract where each token is non-transferable and has roles assigned to create, approve and
/// disapprove actions.
/// @dev TODO Add comments here around limitations/expectations of this contract, namely the "total
/// supply issue", the fact that quantities cannot be larger than 1, and burning a policy.
/// @dev The roles determine how the token can interact with the Vertex Core contract.
contract VertexPolicy is ERC721NonTransferableMinimalProxy {
using Checkpoints for Checkpoints.History;
// ======================================
// ======== Errors and Modifiers ========
// ======================================
error AlreadyInitialized();
error CallReverted(uint256 index, bytes revertData);
error InvalidInput();
error MissingAdmin();
error NonTransferableToken();
error OnlyVertex();
error RoleNotInitialized(uint8 role);
modifier onlyVertex() {
if (msg.sender != vertex) revert OnlyVertex();
_;
}
modifier nonTransferableToken() {
_; // We put this ahead of the revert so we don't get an unreachable code warning. TODO Confirm this is safe.
revert NonTransferableToken();
}
// ========================
// ======== Events ========
// ========================
event RoleAssigned(address indexed user, uint8 indexed role, uint256 expiration, uint256 roleSupply);
event RoleInitialized(uint8 indexed role, RoleDescription description);
event RolePermissionAssigned(uint8 indexed role, bytes32 indexed permissionId, bool hasPermission);
// =============================================================
// ======== Constants, Immutables and Storage Variables ========
// =============================================================
/// @notice A special role used to reference all policy holders.
/// @dev DO NOT assign users this role directly. Doing so can result in the wrong total supply
/// values for this role.
uint8 public constant ALL_HOLDERS_ROLE = 0; // TODO Confirm zero is safe here.
/// @notice Returns true if the `role` can create actions with the given `permissionId`.
mapping(uint8 role => mapping(bytes32 permissionId => bool)) public canCreateAction;
/// @notice Checkpoints a token ID's "balance" (quantity) of a given role. The quantity of the
/// role is how much weight the role-holder gets when approving/disapproving (regardless of
/// strategy).
/// @dev The current implementation does not allow a user's quantity to be anything other than 1.
mapping(uint256 tokenId => mapping(uint8 role => Checkpoints.History)) internal roleBalanceCkpts;
/// @notice Checkpoints the total supply of a given role.
/// @dev At a given timestamp, the total supply of a role must equal the sum of the quantity of
/// the role for each token ID that holds the role.
mapping(uint8 role => Checkpoints.History) internal roleSupplyCkpts;
/// @notice The highest role ID that has been initialized.
uint8 public numRoles;
/// @notice The address of the `VertexCore` instance that governs this contract.
address public vertex;
/// @notice The address of the `VertexFactory` contract.
VertexFactory public factory;
// ======================================================
// ======== Contract Creation and Initialization ========
// ======================================================
constructor() initializer {}
function initialize(
string calldata _name,
RoleDescription[] calldata roleDescriptions,
RoleHolderData[] calldata roleHolders,
RolePermissionData[] calldata rolePermissions
) external initializer {
__initializeERC721MinimalProxy(_name, string.concat("V_", LibString.slice(_name, 0, 3)));
factory = VertexFactory(msg.sender);
for (uint256 i = 0; i < roleDescriptions.length; i = _uncheckedIncrement(i)) {
_initializeRole(roleDescriptions[i]);
}
for (uint256 i = 0; i < roleHolders.length; i = _uncheckedIncrement(i)) {
_setRoleHolder(roleHolders[i].role, roleHolders[i].user, roleHolders[i].quantity, roleHolders[i].expiration);
}
for (uint256 i = 0; i < rolePermissions.length; i = _uncheckedIncrement(i)) {
_setRolePermission(rolePermissions[i].role, rolePermissions[i].permissionId, rolePermissions[i].hasPermission);
}
// Must have assigned roles during initialization, otherwise the system cannot be used. However,
// we do not check that roles were assigned "properly" as there is no single correct way, so
// this is more of a sanity check, not a guarantee that the system will work after initialization.
if (numRoles == 0 || getSupply(ALL_HOLDERS_ROLE) == 0) revert InvalidInput();
}
// ===========================================
// ======== External and Public Logic ========
// ===========================================
/// @notice Sets the address of the `VertexCore` contract.
/// @dev This method can only be called once.
/// @param _vertex The address of the `VertexCore` contract.
function setVertex(address _vertex) external {
if (vertex != address(0)) revert AlreadyInitialized();
vertex = _vertex;
}
// -------- Role and Permission Management --------
/// @notice Aggregate calls of multiple functions in the current contract into a single call.
/// @dev The `msg.value` should not be trusted for any method callable from this method. No
/// methods in this contract are `payable` so this should not be an issue, but it's mentioned
/// here in case this contract is modified in the future.
/// @param calls ABI-encoded array of calls to be executed in order.
/// @return returnData The return data of each call.
function aggregate(bytes[] calldata calls) external onlyVertex returns (bytes[] memory returnData) {
returnData = new bytes[](calls.length);
for (uint256 i = 0; i < calls.length; i = _uncheckedIncrement(i)) {
(bool success, bytes memory response) = address(this).delegatecall(calls[i]);
if (!success) revert CallReverted(i, response);
returnData[i] = response;
}
}
/// @notice Initializes a new role with the given `role` ID and `description`
function initializeRole(RoleDescription description) external onlyVertex {
_initializeRole(description);
}
/// @notice Assigns a role to a user.
/// @param role ID of the role to set (uint8 ensures on-chain enumerability when burning policies).
/// @param user User to assign the role to.
/// @param quantity Quantity of the role to assign to the user, i.e. their (dis)approval weight.
/// @param expiration When the role expires.
function setRoleHolder(uint8 role, address user, uint128 quantity, uint64 expiration) external onlyVertex {
_setRoleHolder(role, user, quantity, expiration);
}
/// @notice Assigns a permission to a role.
/// @param role Name of the role to set.
/// @param permissionId Permission ID to assign to the role.
/// @param hasPermission Whether to assign the permission or remove the permission.
function setRolePermission(uint8 role, bytes32 permissionId, bool hasPermission) external onlyVertex {
_setRolePermission(role, permissionId, hasPermission);
}
/// @notice Revokes an expired role.
/// @param role Role that has expired.
/// @param user User that held the role.
/// @dev WARNING: The contract cannot enumerate all expired roles for a user, so the caller MUST
/// provide the full list of expired roles to revoke. Not properly providing this data can result
/// in an inconsistent internal state. It is expected that roles are revoked as needed before
/// creating an action that uses that role as the `approvalRole` or `disapprovalRole`. Not doing
/// so would mean the total supply is higher than expected. Depending on the strategy
/// configuration this may not be a big deal, or it may mean it's impossible to reach quorum. It's
/// not a big issue if quorum cannot be reached, because a new action can be created.
function revokeExpiredRole(uint8 role, address user) external {
_revokeExpiredRole(role, user);
}
/// @notice Revokes all roles from the `user` and burns their policy.
function revokePolicy(address user) external onlyVertex {
for (uint256 i = 0; i <= numRoles; i = _uncheckedIncrement(i)) {
_setRoleHolder(uint8(i), user, 0, 0);
}
_burn(_tokenId(user));
}
/// @notice Revokes all `roles` from the `user` and burns their policy.
/// @dev WARNING: The contract cannot enumerate all roles for a user, so the caller MUST provide
/// the full list of roles held by user. Not properly providing this data can result in an
/// inconsistent internal state. It is expected that policies are revoked as needed before
/// creating an action using the `ALL_HOLDERS_ROLE`.
/// @dev This method only exists to ensure policies can still be revoked in the case where the
/// other `revokePolicy` method cannot be executed due to needed more gas than the block gas limit.
function revokePolicy(address user, uint8[] calldata roles) external onlyVertex {
for (uint256 i = 0; i < roles.length; i = _uncheckedIncrement(i)) {
_setRoleHolder(roles[i], user, 0, 0);
}
_burn(_tokenId(user));
}
// -------- Role and Permission Getters --------
/// @notice Returns the quantity of the `role` for the given `user`. The returned value is the
/// weight of the role when approving/disapproving (regardless of strategy).
function getWeight(address user, uint8 role) external view returns (uint256) {
uint256 tokenId = _tokenId(user);
return roleBalanceCkpts[tokenId][role].latest();
}
/// @notice Returns the quantity of the `role` for the given `user` at `timestamp`. The returned
/// value is the weight of the role when approving/disapproving (regardless of strategy).
function getPastWeight(address user, uint8 role, uint256 timestamp) external view returns (uint256) {
uint256 tokenId = _tokenId(user);
return roleBalanceCkpts[tokenId][role].getAtTimestamp(timestamp);
}
/// @notice Returns the total supply of `role` holders at the given `timestamp`. The returned
/// value is the value used to determine if quorum has been reached when approving/disapproving.
/// @dev The value returned by this method must equal the sum of the quantity of the role
/// across all policyholders at that timestamp.
function getSupply(uint8 role) public view returns (uint256) {
(,,, uint128 quantity) = roleSupplyCkpts[role].latestCheckpoint();
return quantity;
}
/// @notice Returns the total supply of `role` holders at the given `timestamp`. The returned
/// value is the value used to determine if quorum has been reached when approving/disapproving.
/// @dev The value returned by this method must equal the sum of the quantity of the role
/// across all policyholders at that timestamp.
function getPastSupply(uint8 role, uint256 timestamp) external view returns (uint256) {
return roleSupplyCkpts[role].getAtTimestamp(timestamp);
}
/// @notice Returns all checkpoints for the given `user` and `role`.
function roleBalanceCheckpoints(address user, uint8 role) external view returns (Checkpoints.History memory) {
uint256 tokenId = _tokenId(user);
return roleBalanceCkpts[tokenId][role];
}
/// @notice Returns all supply checkpoints for the given `role`.
function roleSupplyCheckpoints(uint8 role) external view returns (Checkpoints.History memory) {
return roleSupplyCkpts[role];
}
/// @notice Returns true if the `user` has the `role`, false otherwise.
function hasRole(address user, uint8 role) external view returns (bool) {
(bool exists,, uint64 expiration, uint128 quantity) = roleBalanceCkpts[_tokenId(user)][role].latestCheckpoint();
return exists && quantity > 0 && expiration > block.timestamp;
}
/// @notice Returns true if the `user` has the `role` at `timestamp`, false otherwise.
function hasRole(address user, uint8 role, uint256 timestamp) external view returns (bool) {
uint256 quantity = roleBalanceCkpts[_tokenId(user)][role].getAtTimestamp(timestamp);
return quantity > 0;
}
/// @notice Returns true if the given `user` has a given `permissionId` under the `role`,
/// false otherwise.
function hasPermissionId(address user, uint8 role, bytes32 permissionId) external view returns (bool) {
uint128 quantity = roleBalanceCkpts[_tokenId(user)][role].latest();
return quantity > 0 && canCreateAction[role][permissionId];
}
/// @notice Returns the total number of policies in existence.
/// @dev This is just an alias for convenience/familiarity.
function totalSupply() public view returns (uint256) {
return getSupply(ALL_HOLDERS_ROLE);
}
// -------- ERC-721 Getters --------
/// @notice Returns the location of the policy metadata.
/// @param tokenId The ID of the policy token.
function tokenURI(uint256 tokenId) public view override returns (string memory) {
return factory.tokenURI(name, symbol, tokenId);
}
// -------- ERC-721 Methods --------
/// @dev overriding transferFrom to disable transfers
function transferFrom(address, /* from */ address, /* to */ uint256 /* policyId */ )
public
pure
override
nonTransferableToken
{}
/// @dev overriding safeTransferFrom to disable transfers
function safeTransferFrom(address, /* from */ address, /* to */ uint256 /* id */ )
public
pure
override
nonTransferableToken
{}
/// @dev overriding safeTransferFrom to disable transfers
function safeTransferFrom(address, /* from */ address, /* to */ uint256, /* policyId */ bytes calldata /* data */ )
public
pure
override
nonTransferableToken
{}
/// @dev overriding approve to disable approvals
function approve(address, /* spender */ uint256 /* id */ ) public pure override nonTransferableToken {}
/// @dev overriding approve to disable approvals
function setApprovalForAll(address, /* operator */ bool /* approved */ ) public pure override nonTransferableToken {}
// ================================
// ======== Internal Logic ========
// ================================
function _initializeRole(RoleDescription description) internal {
numRoles += 1;
emit RoleInitialized(numRoles, description);
}
function _setRoleHolder(uint8 role, address user, uint128 quantity, uint64 expiration) internal {
// Scope to avoid stack too deep.
{
// Ensure role is initialized.
if (role > numRoles) revert RoleNotInitialized(role);
// An expiration of zero is only allowed if the role is being removed. Roles are removed when
// the quantity is zero. In other words, the relationships that are required between the role
// quantity and expiration fields are:
// - quantity > 0 && expiration > block.timestamp: This means you are adding a role
// - quantity == 0 && expiration == 0: This means you are removing a role
bool case1 = quantity > 0 && expiration > block.timestamp;
bool case2 = quantity == 0 && expiration == 0;
if (!(case1 || case2)) revert InvalidInput();
}
// Save off whether or not the user has a nonzero quantity of this role. This is used below when
// updating the total supply of the role.
uint256 tokenId = _tokenId(user);
uint128 initialQuantity = roleBalanceCkpts[tokenId][role].latest();
bool hadRoleQuantity = initialQuantity > 0;
bool willHaveRole = quantity > 0 && expiration > block.timestamp;
// Now we update the user's role balance checkpoint.
roleBalanceCkpts[tokenId][role].push(willHaveRole ? quantity : 0, expiration);
if (balanceOf(user) == 0) _mint(user);
// Lastly we update the total supply of the role. If the expiration is zero, it means the role
// was removed. Determining how to update total supply requires knowing if the user currently
// has a nonzero quantity of this role. This is strictly a quantity check and ignores the
// expiration because this is used to determine whether or not to update the total supply.
uint128 quantityDiff = initialQuantity > quantity ? initialQuantity - quantity : quantity - initialQuantity;
uint128 currentRoleSupply = roleSupplyCkpts[role].latest();
uint128 newRoleSupply;
if (hadRoleQuantity && !willHaveRole) newRoleSupply = currentRoleSupply - quantityDiff;
else if (!hadRoleQuantity && willHaveRole) newRoleSupply = currentRoleSupply + quantityDiff;
else newRoleSupply = currentRoleSupply;
roleSupplyCkpts[role].push(newRoleSupply);
emit RoleAssigned(user, role, expiration, newRoleSupply);
}
function _setRolePermission(uint8 role, bytes32 permissionId, bool hasPermission) internal {
canCreateAction[role][permissionId] = hasPermission;
emit RolePermissionAssigned(role, permissionId, hasPermission);
}
function _revokeExpiredRole(uint8 role, address user) internal {
// Read the most recent checkpoint for the user's role balance.
uint256 tokenId = _tokenId(user);
(,, uint64 expiration, uint128 quantity) = roleBalanceCkpts[tokenId][role].latestCheckpoint();
if (quantity == 0 || expiration == 0 || expiration > block.timestamp) revert InvalidInput();
_setRoleHolder(role, user, 0, 0);
}
function _mint(address user) internal {
uint256 tokenId = _tokenId(user);
_mint(user, tokenId);
roleSupplyCkpts[ALL_HOLDERS_ROLE].push(roleSupplyCkpts[ALL_HOLDERS_ROLE].latest() + 1);
roleBalanceCkpts[tokenId][ALL_HOLDERS_ROLE].push(1);
}
function _burn(uint256 tokenId) internal override {
ERC721NonTransferableMinimalProxy._burn(tokenId);
roleSupplyCkpts[ALL_HOLDERS_ROLE].push(roleSupplyCkpts[ALL_HOLDERS_ROLE].latest() - 1);
roleBalanceCkpts[tokenId][ALL_HOLDERS_ROLE].push(0);
}
function _tokenId(address user) internal pure returns (uint256) {
return uint256(uint160(user));
}
function _uncheckedIncrement(uint256 i) internal pure returns (uint256) {
unchecked {
return i + 1;
}
}
}